Bug 1445197 - part 4: Implement application panel UI to display all workers;r=nchevobbe,Honza draft
authorJulian Descottes <jdescottes@mozilla.com>
Thu, 05 Apr 2018 21:39:10 +0200
changeset 785650 b6e3933bd36c89524f707c528375afc410992622
parent 785649 a36c31ece12b53481edf63e178d64bf00bb9b6aa
child 785955 73e5f73be441e11a160276344fcd14cc6b379149
push id107294
push userjdescottes@mozilla.com
push dateFri, 20 Apr 2018 15:52:30 +0000
reviewersnchevobbe, Honza
bugs1445197
milestone61.0a1
Bug 1445197 - part 4: Implement application panel UI to display all workers;r=nchevobbe,Honza Add redux, a store, listen to events that can lead to a change in the workers list to update the store. MozReview-Commit-ID: Fo0jn7Cldep
devtools/client/application/application.css
devtools/client/application/index.html
devtools/client/application/initializer.js
devtools/client/application/moz.build
devtools/client/application/panel.js
devtools/client/application/src/actions/index.js
devtools/client/application/src/actions/moz.build
devtools/client/application/src/actions/workers.js
devtools/client/application/src/components/App.css
devtools/client/application/src/components/App.js
devtools/client/application/src/components/Worker.css
devtools/client/application/src/components/Worker.js
devtools/client/application/src/components/WorkerList.css
devtools/client/application/src/components/WorkerList.js
devtools/client/application/src/components/moz.build
devtools/client/application/src/constants.js
devtools/client/application/src/create-store.js
devtools/client/application/src/moz.build
devtools/client/application/src/reducers/index.js
devtools/client/application/src/reducers/moz.build
devtools/client/application/src/reducers/workers-state.js
new file mode 100644
--- /dev/null
+++ b/devtools/client/application/application.css
@@ -0,0 +1,23 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@import "resource://devtools/client/application/src/components/App.css";
+@import "resource://devtools/client/application/src/components/Worker.css";
+@import "resource://devtools/client/application/src/components/WorkerList.css";
+
+* {
+  box-sizing: border-box;
+}
+
+html,
+body,
+#mount {
+  height: 100%;
+}
+
+ul {
+  list-style: none;
+  margin: 0;
+  padding: 0;
+}
\ No newline at end of file
--- a/devtools/client/application/index.html
+++ b/devtools/client/application/index.html
@@ -1,13 +1,14 @@
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 <!DOCTYPE html>
 <html dir="">
   <head>
+    <link rel="stylesheet" type="text/css" href="resource://devtools/client/application/application.css" />
   </head>
   <body class="theme-body" role="application">
     <div id="mount"></div>
     <script src="chrome://devtools/content/shared/theme-switching.js"></script>
     <script src="initializer.js"></script>
   </body>
 </html>
--- a/devtools/client/application/initializer.js
+++ b/devtools/client/application/initializer.js
@@ -1,36 +1,74 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const { BrowserLoader } = ChromeUtils.import("resource://devtools/client/shared/browser-loader.js", {});
-const require = window.windowRequire = BrowserLoader({
+const require = BrowserLoader({
   baseURI: "resource://devtools/client/application/",
   window,
 }).require;
 
 const { createFactory } = require("devtools/client/shared/vendor/react");
 const { render, unmountComponentAtNode } = require("devtools/client/shared/vendor/react-dom");
+const Provider = createFactory(require("devtools/client/shared/vendor/react-redux").Provider);
+const { bindActionCreators } = require("devtools/client/shared/vendor/redux");
+
+const { configureStore } = require("./src/create-store");
+const actions = require("./src/actions/index");
 
 const App = createFactory(require("./src/components/App"));
 
 /**
  * Global Application object in this panel. This object is expected by panel.js and is
  * called to start the UI for the panel.
  */
 window.Application = {
-  bootstrap({ toolbox, panel }) {
+  async bootstrap({ toolbox, panel }) {
+    this.updateWorkers = this.updateWorkers.bind(this);
+
     this.mount = document.querySelector("#mount");
+    this.toolbox = toolbox;
+    this.client = toolbox.target.client;
+
+    this.store = configureStore();
+    this.actions = bindActionCreators(actions, this.store.dispatch);
+
+    const serviceContainer = {
+      openAboutDebugging() {
+        let win = toolbox.doc.defaultView.top;
+        win.openUILinkIn("about:debugging#workers", "tab", { relatedToCurrent: true });
+      }
+    };
 
     // Render the root Application component.
-    const app = App();
+    const app = App({ client: this.client, serviceContainer });
+    render(Provider({ store: this.store }, app), this.mount);
 
-    render(app, this.mount);
+    this.client.addListener("workerListChanged", this.updateWorkers);
+    this.client.addListener("serviceWorkerRegistrationListChanged", this.updateWorkers);
+    this.client.addListener("registration-changed", this.updateWorkers);
+    this.client.addListener("processListChanged", this.updateWorkers);
+
+    await this.updateWorkers();
+  },
+
+  async updateWorkers() {
+    let { service } = await this.client.mainRoot.listAllWorkers();
+    this.actions.updateWorkers(service);
   },
 
   destroy() {
+    this.client.removeListener("workerListChanged", this.updateWorkers);
+    this.client.removeListener("serviceWorkerRegistrationListChanged",
+      this.updateWorkers);
+    this.client.removeListener("registration-changed", this.updateWorkers);
+    this.client.removeListener("processListChanged", this.updateWorkers);
+
     unmountComponentAtNode(this.mount);
     this.mount = null;
+    this.toolbox = null;
+    this.client = null;
   },
 };
--- a/devtools/client/application/moz.build
+++ b/devtools/client/application/moz.build
@@ -2,10 +2,11 @@
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DIRS += [
     'src',
 ]
 
 DevToolsModules(
+    'application.css',
     'panel.js'
 )
--- a/devtools/client/application/panel.js
+++ b/devtools/client/application/panel.js
@@ -22,17 +22,17 @@ class ApplicationPanel {
     this.toolbox = toolbox;
   }
 
   async open() {
     if (!this.toolbox.target.isRemote) {
       await this.toolbox.target.makeRemote();
     }
 
-    this.panelWin.Application.bootstrap({
+    await this.panelWin.Application.bootstrap({
       toolbox: this.toolbox,
       panel: this,
     });
     this.emit("ready");
     this.isReady = true;
     return this;
   }
 
new file mode 100644
--- /dev/null
+++ b/devtools/client/application/src/actions/index.js
@@ -0,0 +1,11 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const workers = require("./workers");
+
+Object.assign(exports,
+  workers,
+);
new file mode 100644
--- /dev/null
+++ b/devtools/client/application/src/actions/moz.build
@@ -0,0 +1,8 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+    'index.js',
+    'workers.js',
+)
new file mode 100644
--- /dev/null
+++ b/devtools/client/application/src/actions/workers.js
@@ -0,0 +1,20 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+  UPDATE_WORKERS,
+} = require("../constants");
+
+function updateWorkers(workers) {
+  return {
+    type: UPDATE_WORKERS,
+    workers
+  };
+}
+
+module.exports = {
+  updateWorkers,
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/application/src/components/App.css
@@ -0,0 +1,44 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * The current layout of the application panel is
+ *
+ *  +---------------------------------------------+
+ *  | (header) "Service workers"                  |
+ *  +---------------------------------------------+
+ *  | Service worker 1                            |
+ *  |   (...)                                     |
+ *  | Service worker N           (see Worker.css) |
+ *  +---------------------------------------------+
+ *  |                     Link to about:debugging |
+ *  +---------------------------------------------+
+ */
+.application {
+  height: 100%;
+  padding: 0 0 0 20px;
+  overflow: auto;
+  display: flex;
+  flex-direction: column;
+}
+
+h1 {
+  font-size: 22px;
+  font-weight: normal;
+}
+
+a,
+a:hover,
+a:visited {
+  color: var(--blue-60) !important;
+  margin: 0 10px;
+  cursor: pointer;
+}
+
+a.disabled,
+a.disabled:hover,
+a.disabled:visited {
+  color: var(--grey-30) !important;
+  cursor: default;
+}
--- a/devtools/client/application/src/components/App.js
+++ b/devtools/client/application/src/components/App.js
@@ -1,18 +1,38 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-const { Component } = require("devtools/client/shared/vendor/react");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const { createFactory, Component } = require("devtools/client/shared/vendor/react");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
 const { div } = require("devtools/client/shared/vendor/react-dom-factories");
 
+const WorkerList = createFactory(require("./WorkerList"));
+
+/**
+ * This is the main component for the application panel.
+ */
 class App extends Component {
+  static get propTypes() {
+    return {
+      client: PropTypes.object.isRequired,
+      workers: PropTypes.object.isRequired,
+      serviceContainer: PropTypes.object.isRequired,
+    };
+  }
+
   render() {
-    return (
-      div({className: "application"}, "application panel content")
-    );
+    let { workers, client, serviceContainer } = this.props;
+
+    return div({className: "application"},
+      WorkerList({ workers, client, serviceContainer }));
   }
 }
 
-module.exports = App;
+// Exports
+
+module.exports = connect(
+  (state) => ({ workers: state.workers.list }),
+)(App);
new file mode 100644
--- /dev/null
+++ b/devtools/client/application/src/components/Worker.css
@@ -0,0 +1,47 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+ /*
+ * The current layout of a service worker item is
+ *
+ *  +----------------------------+----------------+
+ *  | Service worker scope       | Unregister btn |
+ *  +---+----------+-------------+----------------|
+ *  |   | "Source" | script name | debug link     |
+ *  |   |----------+-------------+----------------|
+ *  |   | "Status" | status      | start link     |
+ *  +---+----------+-------------+----------------|
+ */
+.service-worker-container {
+  margin-bottom: 20px;
+  width: 100%;
+  max-width: 600px;
+  position: relative;
+  line-height: 1.5;
+  font-size: 13px;
+}
+
+.service-worker-container > .service-worker-scope {
+  padding-inline-start: 30px;
+}
+
+.service-worker-container > :not(.service-worker-scope) {
+  padding-inline-start: 70px;
+}
+
+.service-worker-scope {
+  font-weight: bold;
+}
+
+.service-worker-meta-name {
+  color: var(--grey-50);
+  min-width: 50px;
+  margin-inline-end: 10px;
+  display: inline-block;
+}
+
+.unregister-button {
+  position: absolute;
+  right: 0;
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/application/src/components/Worker.js
@@ -0,0 +1,157 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { Component } = require("devtools/client/shared/vendor/react");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const { a, button, div, li, span } = require("devtools/client/shared/vendor/react-dom-factories");
+const Services = require("Services");
+
+loader.lazyRequireGetter(this, "DebuggerClient",
+  "devtools/shared/client/debugger-client", true);
+loader.lazyRequireGetter(this, "gDevToolsBrowser",
+  "devtools/client/framework/devtools-browser", true);
+
+const Strings = Services.strings.createBundle(
+  "chrome://devtools/locale/aboutdebugging.properties");
+
+/**
+ * This component is dedicated to display a worker, more accurately a service worker, in
+ * the list of workers displayed in the application panel. It displays information about
+ * the worker as well as action links and buttons to interact with the worker (e.g. debug,
+ * unregister, update etc...).
+ */
+class Worker extends Component {
+  static get propTypes() {
+    return {
+      client: PropTypes.instanceOf(DebuggerClient).isRequired,
+      debugDisabled: PropTypes.bool,
+      worker: PropTypes.shape({
+        active: PropTypes.bool,
+        name: PropTypes.string.isRequired,
+        scope: PropTypes.string.isRequired,
+        // registrationActor can be missing in e10s.
+        registrationActor: PropTypes.string,
+        workerActor: PropTypes.string
+      }).isRequired
+    };
+  }
+
+  constructor(props) {
+    super(props);
+
+    this.debug = this.debug.bind(this);
+    this.start = this.start.bind(this);
+    this.unregister = this.unregister.bind(this);
+  }
+
+  debug() {
+    if (!this.isRunning()) {
+      console.log("Service workers cannot be debugged if they are not running");
+      return;
+    }
+
+    let { client, worker } = this.props;
+    gDevToolsBrowser.openWorkerToolbox(client, worker.workerActor);
+  }
+
+  start() {
+    if (!this.isActive() || this.isRunning()) {
+      console.log("Running or inactive service workers cannot be started");
+      return;
+    }
+
+    let { client, worker } = this.props;
+    client.request({
+      to: worker.registrationActor,
+      type: "start"
+    });
+  }
+
+  unregister() {
+    let { client, worker } = this.props;
+    client.request({
+      to: worker.registrationActor,
+      type: "unregister"
+    });
+  }
+
+  isRunning() {
+    // We know the worker is running if it has a worker actor.
+    return !!this.props.worker.workerActor;
+  }
+
+  isActive() {
+    return this.props.worker.active;
+  }
+
+  getServiceWorkerStatus() {
+    if (this.isActive() && this.isRunning()) {
+      return "running";
+    } else if (this.isActive()) {
+      return "stopped";
+    }
+    // We cannot get service worker registrations unless the registration is in
+    // ACTIVE state. Unable to know the actual state ("installing", "waiting"), we
+    // display a custom state "registering" for now. See Bug 1153292.
+    return "registering";
+  }
+
+  formatScope(scope) {
+    let [, remainder] = scope.split("://");
+    return remainder || scope;
+  }
+
+  formatSource(source) {
+    let parts = source.split("/");
+    return parts[parts.length - 1];
+  }
+
+  render() {
+    let { worker } = this.props;
+    let status = this.getServiceWorkerStatus();
+
+    const unregisterButton = this.isActive() ?
+      button({
+        onClick: this.unregister,
+        className: "devtools-button unregister-button",
+        "data-standalone": true
+      },
+        Strings.GetStringFromName("unregister"))
+      : null;
+
+    const debugLinkDisabled = this.isRunning() ? "" : "disabled";
+    const debugLink = a({
+      onClick: this.isRunning() ? this.debug : null,
+      title: this.isRunning() ? null : "Only running service workers can be debugged",
+      className: `${debugLinkDisabled} debug-link`
+    },
+      Strings.GetStringFromName("debug"));
+
+    const startLink = !this.isRunning() ?
+      a({ onClick: this.start, className: "start-link" },
+        Strings.GetStringFromName("start"))
+      : null;
+
+    return li({ className: "service-worker-container" },
+      div(
+        { className: "service-worker-scope" },
+        span({ title: worker.scope }, this.formatScope(worker.scope)),
+        unregisterButton),
+      div(
+        { className: "service-worker-source" },
+        span({ className: "service-worker-meta-name" }, "Source"),
+        span({ title: worker.scope }, this.formatSource(worker.url)),
+        debugLink),
+      div(
+        { className: `service-worker-status service-worker-status-${status}` },
+        span({ className: "service-worker-meta-name" }, "Status"),
+        Strings.GetStringFromName(status).toLowerCase(),
+        startLink)
+    );
+  }
+}
+
+module.exports = Worker;
new file mode 100644
--- /dev/null
+++ b/devtools/client/application/src/components/WorkerList.css
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+.application-aboutdebugging-plug {
+  text-align: right;
+  padding: 5px 0;
+}
+
+.application-workers-container {
+  flex-grow: 1;
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/application/src/components/WorkerList.js
@@ -0,0 +1,51 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const { createFactory, Component } = require("devtools/client/shared/vendor/react");
+const { a, div, h1, ul, li } = require("devtools/client/shared/vendor/react-dom-factories");
+const Worker = createFactory(require("./Worker"));
+
+/**
+ * This component handles the list of service workers displayed in the application panel
+ * and also displays a suggestion to use about debugging for debugging other service
+ * workers.
+ */
+class WorkerList extends Component {
+  static get propTypes() {
+    return {
+      client: PropTypes.object.isRequired,
+      workers: PropTypes.object.isRequired,
+      serviceContainer: PropTypes.object.isRequired,
+    };
+  }
+
+  render() {
+    const { workers, client, serviceContainer } = this.props;
+    const { openAboutDebugging } = serviceContainer;
+
+    return [
+      ul({ className: "application-workers-container" },
+        li({},
+          h1({ className: "application-title" }, "Service Workers")
+        ),
+        workers.map(worker => Worker({
+          client,
+          debugDisabled: false,
+          worker,
+        }))
+      ),
+      div({ className: "application-aboutdebugging-plug" },
+        "See about:debugging for Service Workers from other domains",
+        a({ onClick: () => openAboutDebugging() }, "Open about:debugging")
+      )
+    ];
+  }
+}
+
+// Exports
+
+module.exports = WorkerList;
--- a/devtools/client/application/src/components/moz.build
+++ b/devtools/client/application/src/components/moz.build
@@ -1,7 +1,12 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
+    'App.css',
     'App.js',
+    'Worker.css',
+    'Worker.js',
+    'WorkerList.css',
+    'WorkerList.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/application/src/constants.js
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const actionTypes = {
+  UPDATE_WORKERS: "UPDATE_WORKERS",
+};
+
+// flatten constants
+module.exports = Object.assign({}, actionTypes);
new file mode 100644
--- /dev/null
+++ b/devtools/client/application/src/create-store.js
@@ -0,0 +1,22 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { createStore } = require("devtools/client/shared/vendor/redux");
+
+// Reducers
+const rootReducer = require("./reducers/index");
+const { WorkersState } = require("./reducers/workers-state");
+
+function configureStore() {
+  // Prepare initial state.
+  const initialState = {
+    workers: new WorkersState(),
+  };
+
+  return createStore(rootReducer, initialState);
+}
+
+exports.configureStore = configureStore;
--- a/devtools/client/application/src/moz.build
+++ b/devtools/client/application/src/moz.build
@@ -1,7 +1,14 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DIRS += [
+    'actions',
     'components',
-]
\ No newline at end of file
+    'reducers',
+]
+
+DevToolsModules(
+    'constants.js',
+    'create-store.js',
+)
new file mode 100644
--- /dev/null
+++ b/devtools/client/application/src/reducers/index.js
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { combineReducers } = require("devtools/client/shared/vendor/redux");
+const { workersReducer } = require("./workers-state");
+
+module.exports = combineReducers({
+  workers: workersReducer,
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/application/src/reducers/moz.build
@@ -0,0 +1,8 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+    'index.js',
+    'workers-state.js',
+)
new file mode 100644
--- /dev/null
+++ b/devtools/client/application/src/reducers/workers-state.js
@@ -0,0 +1,33 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+  UPDATE_WORKERS,
+} = require("../constants");
+
+function WorkersState() {
+  return {
+    // Array of all service workers
+    list: [],
+  };
+}
+
+function workersReducer(state = WorkersState(), action) {
+  switch (action.type) {
+    case UPDATE_WORKERS: {
+      let { workers } = action;
+      return { list: workers };
+    }
+
+    default:
+      return state;
+  }
+}
+
+module.exports = {
+  WorkersState,
+  workersReducer,
+};