Bug 1445197 - part 4: Implement application panel UI to display all workers;r=Honza,nchevobbe,sole
authorJulian Descottes <jdescottes@mozilla.com>
Thu, 05 Apr 2018 21:39:10 +0200
changeset 414699 f0e5fbd3927460e8de5ee92f93c978c1a58200f1
parent 414698 42759c2366615c8d87ea74c8be00d2c8232af4b2
child 414700 25adabc13a5bb417ddb9ce5529cba2630d9a708e
push id62995
push userjdescottes@mozilla.com
push dateFri, 20 Apr 2018 16:25:38 +0000
treeherderautoland@f0e5fbd39274 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersHonza, nchevobbe, sole
bugs1445197
milestone61.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1445197 - part 4: Implement application panel UI to display all workers;r=Honza,nchevobbe,sole 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,
+};