Bug 1459962 - Part 3: Split workers into separate Registration and Worker components r=jdescottes
authorBelén Albeza <balbeza@mozilla.com>
Fri, 24 Apr 2020 16:56:01 +0000
changeset 525973 84937d890e28541877f554f1ed05f5470d3b704d
parent 525972 9f1bbab4d4a5e45c8792a10874da35aee294b7bd
child 525974 55485522c1b06d5e79acc9fac9230d73f8bab4ba
push id113985
push userbalbeza@mozilla.com
push dateFri, 24 Apr 2020 17:36:19 +0000
treeherderautoland@1428daf2817d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjdescottes
bugs1459962
milestone77.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 1459962 - Part 3: Split workers into separate Registration and Worker components r=jdescottes In this patch we actually have separate `Worker` and `Registration` components. All workers belonging to the same registration appear together under it. {F2134654} Note: I left the `Debug` button shown for every worker because sometimes we do have a target even though the worker might be waiting. I'm not sure when/how this happens, but I was able to debug it as well. {F2134655} BTW I found a quick –and more reliable? I seems I don't need to reload- way of testing this. **STR**: - Start a local HTTP server at `mozilla-central/devtools/client/application/test/browser/resources/service-workers` - Open `localhost:8080/simple.html` (or whatever the port) and open devtools there - Open `localhost:8080/debug.html` (or whatever the port) and open devtools there You should be able to see both workers under the same registration scope. Differential Revision: https://phabricator.services.mozilla.com/D72173
devtools/client/application/application.css
devtools/client/application/src/components/service-workers/Registration.css
devtools/client/application/src/components/service-workers/Registration.js
devtools/client/application/src/components/service-workers/RegistrationList.css
devtools/client/application/src/components/service-workers/RegistrationList.js
devtools/client/application/src/components/service-workers/Worker.css
devtools/client/application/src/components/service-workers/Worker.js
devtools/client/application/src/components/service-workers/WorkersPage.js
devtools/client/application/src/components/service-workers/moz.build
devtools/client/application/src/reducers/workers-state.js
devtools/client/application/src/types/service-workers.js
devtools/client/application/test/node/components/service-workers/__snapshots__/components_application_panel-RegistrationList.test.js.snap
devtools/client/application/test/node/components/service-workers/__snapshots__/components_application_panel-WorkersPage.test.js.snap
devtools/client/application/test/node/components/service-workers/components_application_panel-RegistrationList.test.js
devtools/client/application/test/node/fixtures/data/constants.js
devtools/client/application/test/xpcshell/test_workers_reducer.js
devtools/client/fronts/root.js
--- a/devtools/client/application/application.css
+++ b/devtools/client/application/application.css
@@ -17,16 +17,17 @@
 @import "resource://devtools/client/application/src/components/manifest/ManifestIssueList.css";
 @import "resource://devtools/client/application/src/components/manifest/ManifestItem.css";
 @import "resource://devtools/client/application/src/components/manifest/ManifestJsonLink.css";
 @import "resource://devtools/client/application/src/components/manifest/ManifestLoader.css";
 @import "resource://devtools/client/application/src/components/manifest/ManifestSection.css";
 @import "resource://devtools/client/application/src/components/routing/PageSwitcher.css";
 @import "resource://devtools/client/application/src/components/routing/Sidebar.css";
 @import "resource://devtools/client/application/src/components/routing/SidebarItem.css";
+@import "resource://devtools/client/application/src/components/service-workers/Registration.css";
 @import "resource://devtools/client/application/src/components/service-workers/RegistrationList.css";
 @import "resource://devtools/client/application/src/components/service-workers/RegistrationListEmpty.css";
 @import "resource://devtools/client/application/src/components/service-workers/Worker.css";
 @import "resource://devtools/client/application/src/components/service-workers/WorkersPage.css";
 @import "resource://devtools/client/application/src/components/ui/UIButton.css";
 
 html,
 body,
copy from devtools/client/application/src/components/service-workers/Worker.css
copy to devtools/client/application/src/components/service-workers/Registration.css
--- a/devtools/client/application/src/components/service-workers/Worker.css
+++ b/devtools/client/application/src/components/service-workers/Registration.css
@@ -1,83 +1,45 @@
 /* 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       |
-    |              | "Updated" update_time        |
- *  |--------------+-------------+----------------|
- *  |     "Status" | status start_button          |
- *  +---+----------+-------------+----------------|
- */
-
-.worker {
-  display: grid;
-  grid-template-rows: auto auto auto;
-  grid-template-columns: auto 1fr;
-  width: 100%;
-  grid-column-gap: 0;
+.registration {
   padding: 1rem 0;
   line-height: 1.5;
   font-size: var(--body-10-font-size);
 }
 
-.worker:first-child {
-  padding-top: 0;
-}
-
-.worker:not(:last-child) {
-  border-bottom: 1px solid var(--separator-color);
-}
-
-.worker__header {
-  grid-column: 1/3;
+/*
+ * The current layout of a registration header is
+ *
+ *  +----------------------------+----------------+
+ *  | Registration scope         | Unregister_btn |
+ *  +---+----------+-------------+----------------|
+ */
+.registration__header {
   display: grid;
   grid-template-columns: 1fr auto;
-  grid-column-gap: 2rem;
+  grid-column-gap: calc(4 * var(--base-unit));
   align-items: center;
 }
 
-.worker__scope {
+.registration__scope {
   font-weight: bold;
   text-overflow: ellipsis;
   overflow: hidden;
   white-space: nowrap;
-}
-
-.worker__scope,
-.worker__source-url {
   user-select: text;
 }
 
-.worker__meta-name {
-  color: var(--grey-50);
-  padding-inline-start: 4.5rem;
-}
-
-.worker__data {
-  display: grid;
-  grid-template-columns: auto 1fr;
-  grid-gap: 1rem;
-}
-
-.worker__data > * {
-  margin: 0;
-}
-
-.worker__data__updated {
+.registration__updated-time {
   color: var(--theme-text-color-alt);
 }
 
-.worker__link-start,
-.worker__link-debug {
-  margin: 0 calc(var(--base-unit) * 2);
+.registration__workers {
+  list-style-type: none;
+  padding: 0;
+  margin: calc(2 * var(--base-unit)) 0 0 0;
 }
 
-.worker__status {
-  text-transform: capitalize;
+.registration__workers-item:not(:first-child) {
+  margin-block-start: calc(var(--base-unit) * 2);
 }
\ No newline at end of file
copy from devtools/client/application/src/components/service-workers/Worker.js
copy to devtools/client/application/src/components/service-workers/Registration.js
--- a/devtools/client/application/src/components/service-workers/Worker.js
+++ b/devtools/client/application/src/components/service-workers/Registration.js
@@ -7,241 +7,136 @@
 const {
   createFactory,
   PureComponent,
 } = require("devtools/client/shared/vendor/react");
 
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 
 const {
-  br,
-  dd,
-  dl,
-  dt,
+  article,
+  aside,
   header,
   li,
-  section,
+  p,
   span,
   time,
+  ul,
 } = require("devtools/client/shared/vendor/react-dom-factories");
 
-const {
-  getUnicodeUrl,
-  getUnicodeUrlPath,
-} = require("devtools/client/shared/unicode-url");
+const { getUnicodeUrl } = require("devtools/client/shared/unicode-url");
 
 const FluentReact = require("devtools/client/shared/vendor/fluent-react");
 const Localized = createFactory(FluentReact.Localized);
-const { l10n } = require("devtools/client/application/src/modules/l10n");
 
-const {
-  services,
-} = require("devtools/client/application/src/modules/application-services");
 const Types = require("devtools/client/application/src/types/index");
 
 const UIButton = createFactory(
   require("devtools/client/application/src/components/ui/UIButton")
 );
 
+const Worker = createFactory(
+  require("devtools/client/application/src/components/service-workers/Worker")
+);
+
 /**
- * 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...).
+ * This component is dedicated to display a service worker registration, along
+ * the list of attached workers to it.
+ * It displays information about the registration as well as an Unregister
+ * button.
  */
-class Worker extends PureComponent {
+class Registration extends PureComponent {
   static get propTypes() {
     return {
+      className: PropTypes.string,
       isDebugEnabled: PropTypes.bool.isRequired,
-      worker: PropTypes.shape(Types.worker).isRequired,
+      registration: PropTypes.shape(Types.registration).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;
-    }
-
-    services.openWorkerInDebugger(this.props.worker.workerTargetFront);
-  }
-
-  start() {
-    if (!this.props.isDebugEnabled) {
-      console.log("Service workers cannot be started in multi-e10s");
-      return;
-    }
-
-    if (!this.isActive() || this.isRunning()) {
-      console.log("Running or inactive service workers cannot be started");
-      return;
-    }
-
-    const { registrationFront } = this.props.worker;
-    registrationFront.start();
-  }
-
   unregister() {
-    const { registrationFront } = this.props.worker;
+    const { registrationFront } = this.props.registration;
     registrationFront.unregister();
   }
 
-  isRunning() {
-    // We know the worker is running if it has a worker actor.
-    return !!this.props.worker.workerTargetFront;
-  }
-
   isActive() {
-    return this.props.worker.isActive;
-  }
-
-  getLocalizedStatus() {
-    if (this.isActive() && this.isRunning()) {
-      return l10n.getString("serviceworker-worker-status-running");
-    } else if (this.isActive()) {
-      return l10n.getString("serviceworker-worker-status-stopped");
-    }
-    // NOTE: this is already localized by the service worker front
-    // (strings are in debugger.properties)
-    return this.props.worker.stateText;
+    const { workers } = this.props.registration;
+    return workers.some(x => x.isActive);
   }
 
   formatScope(scope) {
     const [, remainder] = getUnicodeUrl(scope).split("://");
     return remainder || scope;
   }
 
-  formatSource(source) {
-    const parts = source.split("/");
-    return getUnicodeUrlPath(parts[parts.length - 1]);
-  }
-
-  renderDebugButton() {
-    const { isDebugEnabled } = this.props;
-
-    const isDisabled = !this.isRunning() || !isDebugEnabled;
-
-    const localizationId = isDebugEnabled
-      ? "serviceworker-worker-debug"
-      : "serviceworker-worker-debug-forbidden";
-
-    return Localized(
-      {
-        id: localizationId,
-        // The localized title is only displayed if the debug link is disabled.
-        attrs: {
-          title: isDisabled,
-        },
-      },
-      UIButton({
-        onClick: this.debug,
-        className: `js-debug-button`,
-        disabled: isDisabled,
-        size: "micro",
-      })
-    );
-  }
-
-  renderStartButton() {
-    const { isDebugEnabled } = this.props;
-    const isDisabled = !isDebugEnabled;
-
-    return Localized(
-      {
-        id: "serviceworker-worker-start2",
-        // The localized title is only displayed if the debug link is disabled.
-        attrs: {
-          title: !isDisabled,
-        },
-      },
-      UIButton({
-        onClick: this.start,
-        className: `js-start-button`,
-        disabled: isDisabled,
-        size: "micro",
-      })
-    );
-  }
-
   render() {
-    const { worker } = this.props;
-    const statusText = this.getLocalizedStatus();
+    const { registration, isDebugEnabled, className } = this.props;
 
     const unregisterButton = this.isActive()
       ? Localized(
           { id: "serviceworker-worker-unregister" },
           UIButton({
             onClick: this.unregister,
-            className: "worker__unregister-button js-unregister-button",
+            className: "js-unregister-button",
           })
         )
       : null;
 
-    const lastUpdated = worker.lastUpdateTime
+    const lastUpdated = registration.lastUpdateTime
       ? Localized(
           {
             id: "serviceworker-worker-updated",
             // XXX: $date should normally be a Date object, but we pass the timestamp as a
-            // workaround. See Bug 1465718. worker.lastUpdateTime is in microseconds,
+            // workaround. See Bug 1465718. registration.lastUpdateTime is in microseconds,
             // convert to a valid timestamp in milliseconds by dividing by 1000.
-            $date: worker.lastUpdateTime / 1000,
+            $date: registration.lastUpdateTime / 1000,
             time: time({ className: "js-sw-updated" }),
           },
-          span({ className: "worker__data__updated" })
+          span({ className: "registration__updated-time" })
         )
       : null;
 
     const scope = span(
-      { title: worker.scope, className: "worker__scope js-sw-scope" },
-      this.formatScope(worker.scope)
+      {
+        title: registration.scope,
+        className: "registration__scope js-sw-scope",
+      },
+      this.formatScope(registration.scope)
     );
 
     return li(
-      { className: "worker js-sw-container" },
-      header(
-        { className: "worker__header" },
-        scope,
-        section({ className: "worker__controls" }, unregisterButton)
-      ),
-      dl(
-        { className: "worker__data" },
-        Localized(
-          { id: "serviceworker-worker-source" },
-          dt({ className: "worker__meta-name" })
+      { className: className ? className : "" },
+      article(
+        { className: "registration js-sw-container" },
+        header(
+          { className: "registration__header" },
+          scope,
+          aside({}, unregisterButton)
         ),
-        dd(
-          {},
-          span(
-            {
-              title: worker.scope,
-              className: "worker__source-url js-source-url",
-            },
-            this.formatSource(worker.url)
-          ),
-          " ",
-          this.renderDebugButton(),
-          lastUpdated ? br({}) : null,
-          lastUpdated ? lastUpdated : null
-        ),
-        Localized(
-          { id: "serviceworker-worker-status" },
-          dt({ className: "worker__meta-name" })
-        ),
-        dd(
-          {},
-          span({ className: "js-worker-status worker__status" }, statusText),
-          " ",
-          !this.isRunning() ? this.renderStartButton() : null
+        lastUpdated ? p({}, lastUpdated) : null,
+        // render list of workers
+        ul(
+          { className: "registration__workers" },
+          registration.workers.map(worker => {
+            return li(
+              {
+                key: worker.id,
+                className: "registration__workers-item",
+              },
+              Worker({
+                worker,
+                isDebugEnabled,
+              })
+            );
+          })
         )
       )
     );
   }
 }
 
-module.exports = Worker;
+module.exports = Registration;
--- a/devtools/client/application/src/components/service-workers/RegistrationList.css
+++ b/devtools/client/application/src/components/service-workers/RegistrationList.css
@@ -9,8 +9,22 @@
 
 .aboutdebugging-plug__link {
   margin: 0;
 }
 
 .registrations-container {
   flex-grow: 1;
 }
+
+.registrations-container__item {
+  list-style-type: none;
+  margin: 0;
+  padding: 0;
+}
+
+.registrations-container__item:first-child {
+  padding-top: 0;
+}
+
+.registrations-container__item:not(:last-child) {
+  border-bottom: 1px solid var(--separator-color);
+}
--- a/devtools/client/application/src/components/service-workers/RegistrationList.js
+++ b/devtools/client/application/src/components/service-workers/RegistrationList.js
@@ -17,55 +17,56 @@ const {
   h1,
   ul,
 } = require("devtools/client/shared/vendor/react-dom-factories");
 
 const FluentReact = require("devtools/client/shared/vendor/fluent-react");
 const Localized = createFactory(FluentReact.Localized);
 
 const Types = require("devtools/client/application/src/types/index");
-const Worker = createFactory(
-  require("devtools/client/application/src/components/service-workers/Worker")
+const Registration = createFactory(
+  require("devtools/client/application/src/components/service-workers/Registration")
 );
 
 /**
  * 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 RegistrationList extends PureComponent {
   static get propTypes() {
     return {
       canDebugWorkers: PropTypes.bool.isRequired,
-      workers: Types.workerArray.isRequired,
+      registrations: Types.registrationArray.isRequired,
     };
   }
 
   render() {
-    const { canDebugWorkers, workers } = this.props;
+    const { canDebugWorkers, registrations } = this.props;
 
     return [
       article(
         {
           className: "registrations-container",
           key: "registrations-container",
         },
         Localized(
           { id: "serviceworker-list-header" },
           h1({
             className: "app-page__title",
           })
         ),
         ul(
           {},
-          workers.map(worker =>
-            Worker({
-              key: `${worker.id}-${worker.state}`,
+          registrations.map(registration =>
+            Registration({
+              key: registration.id,
               isDebugEnabled: canDebugWorkers,
-              worker,
+              registration,
+              className: "registrations-container__item",
             })
           )
         )
       ),
       Localized(
         {
           id: "serviceworker-list-aboutdebugging",
           key: "serviceworkerlist-footer",
--- a/devtools/client/application/src/components/service-workers/Worker.css
+++ b/devtools/client/application/src/components/service-workers/Worker.css
@@ -1,83 +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       |
-    |              | "Updated" update_time        |
  *  |--------------+-------------+----------------|
  *  |     "Status" | status start_button          |
  *  +---+----------+-------------+----------------|
  */
 
 .worker {
-  display: grid;
-  grid-template-rows: auto auto auto;
-  grid-template-columns: auto 1fr;
-  width: 100%;
-  grid-column-gap: 0;
-  padding: 1rem 0;
   line-height: 1.5;
   font-size: var(--body-10-font-size);
 }
 
-.worker:first-child {
-  padding-top: 0;
-}
-
-.worker:not(:last-child) {
-  border-bottom: 1px solid var(--separator-color);
-}
-
-.worker__header {
-  grid-column: 1/3;
-  display: grid;
-  grid-template-columns: 1fr auto;
-  grid-column-gap: 2rem;
-  align-items: center;
-}
-
-.worker__scope {
-  font-weight: bold;
-  text-overflow: ellipsis;
-  overflow: hidden;
-  white-space: nowrap;
-}
-
-.worker__scope,
 .worker__source-url {
   user-select: text;
 }
 
 .worker__meta-name {
   color: var(--grey-50);
-  padding-inline-start: 4.5rem;
+  padding-inline-start: calc(var(--base-unit) * 3);
 }
 
 .worker__data {
   display: grid;
   grid-template-columns: auto 1fr;
-  grid-gap: 1rem;
+  grid-column-gap: calc(var(--base-unit) * 2);
+  grid-row-gap: calc(var(--base-unit) * 0.5);
 }
 
 .worker__data > * {
   margin: 0;
 }
 
-.worker__data__updated {
-  color: var(--theme-text-color-alt);
-}
-
 .worker__link-start,
 .worker__link-debug {
   margin: 0 calc(var(--base-unit) * 2);
 }
 
 .worker__status {
   text-transform: capitalize;
 }
\ No newline at end of file
--- a/devtools/client/application/src/components/service-workers/Worker.js
+++ b/devtools/client/application/src/components/service-workers/Worker.js
@@ -7,31 +7,24 @@
 const {
   createFactory,
   PureComponent,
 } = require("devtools/client/shared/vendor/react");
 
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 
 const {
-  br,
   dd,
   dl,
   dt,
-  header,
-  li,
   section,
   span,
-  time,
 } = require("devtools/client/shared/vendor/react-dom-factories");
 
-const {
-  getUnicodeUrl,
-  getUnicodeUrlPath,
-} = require("devtools/client/shared/unicode-url");
+const { getUnicodeUrlPath } = require("devtools/client/shared/unicode-url");
 
 const FluentReact = require("devtools/client/shared/vendor/fluent-react");
 const Localized = createFactory(FluentReact.Localized);
 const { l10n } = require("devtools/client/application/src/modules/l10n");
 
 const {
   services,
 } = require("devtools/client/application/src/modules/application-services");
@@ -55,17 +48,16 @@ class Worker extends PureComponent {
     };
   }
 
   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;
     }
 
@@ -82,21 +74,16 @@ class Worker extends PureComponent {
       console.log("Running or inactive service workers cannot be started");
       return;
     }
 
     const { registrationFront } = this.props.worker;
     registrationFront.start();
   }
 
-  unregister() {
-    const { registrationFront } = this.props.worker;
-    registrationFront.unregister();
-  }
-
   isRunning() {
     // We know the worker is running if it has a worker actor.
     return !!this.props.worker.workerTargetFront;
   }
 
   isActive() {
     return this.props.worker.isActive;
   }
@@ -107,21 +94,16 @@ class Worker extends PureComponent {
     } else if (this.isActive()) {
       return l10n.getString("serviceworker-worker-status-stopped");
     }
     // NOTE: this is already localized by the service worker front
     // (strings are in debugger.properties)
     return this.props.worker.stateText;
   }
 
-  formatScope(scope) {
-    const [, remainder] = getUnicodeUrl(scope).split("://");
-    return remainder || scope;
-  }
-
   formatSource(source) {
     const parts = source.split("/");
     return getUnicodeUrlPath(parts[parts.length - 1]);
   }
 
   renderDebugButton() {
     const { isDebugEnabled } = this.props;
 
@@ -145,18 +127,24 @@ class Worker extends PureComponent {
         disabled: isDisabled,
         size: "micro",
       })
     );
   }
 
   renderStartButton() {
     const { isDebugEnabled } = this.props;
+
+    // avoid rendering the button at all for workers that are either running,
+    // or in a state that prevents them from starting (like waiting)
+    if (this.isRunning() || !this.isActive()) {
+      return null;
+    }
+
     const isDisabled = !isDebugEnabled;
-
     return Localized(
       {
         id: "serviceworker-worker-start2",
         // The localized title is only displayed if the debug link is disabled.
         attrs: {
           title: !isDisabled,
         },
       },
@@ -168,80 +156,44 @@ class Worker extends PureComponent {
       })
     );
   }
 
   render() {
     const { worker } = this.props;
     const statusText = this.getLocalizedStatus();
 
-    const unregisterButton = this.isActive()
-      ? Localized(
-          { id: "serviceworker-worker-unregister" },
-          UIButton({
-            onClick: this.unregister,
-            className: "worker__unregister-button js-unregister-button",
-          })
-        )
-      : null;
-
-    const lastUpdated = worker.lastUpdateTime
-      ? Localized(
-          {
-            id: "serviceworker-worker-updated",
-            // XXX: $date should normally be a Date object, but we pass the timestamp as a
-            // workaround. See Bug 1465718. worker.lastUpdateTime is in microseconds,
-            // convert to a valid timestamp in milliseconds by dividing by 1000.
-            $date: worker.lastUpdateTime / 1000,
-            time: time({ className: "js-sw-updated" }),
-          },
-          span({ className: "worker__data__updated" })
-        )
-      : null;
-
-    const scope = span(
-      { title: worker.scope, className: "worker__scope js-sw-scope" },
-      this.formatScope(worker.scope)
-    );
-
-    return li(
-      { className: "worker js-sw-container" },
-      header(
-        { className: "worker__header" },
-        scope,
-        section({ className: "worker__controls" }, unregisterButton)
-      ),
+    return section(
+      { className: "worker" },
       dl(
         { className: "worker__data" },
         Localized(
           { id: "serviceworker-worker-source" },
           dt({ className: "worker__meta-name" })
         ),
         dd(
           {},
           span(
             {
-              title: worker.scope,
+              title: worker.url,
               className: "worker__source-url js-source-url",
             },
             this.formatSource(worker.url)
           ),
           " ",
-          this.renderDebugButton(),
-          lastUpdated ? br({}) : null,
-          lastUpdated ? lastUpdated : null
+          this.renderDebugButton()
         ),
         Localized(
           { id: "serviceworker-worker-status" },
           dt({ className: "worker__meta-name" })
         ),
         dd(
           {},
           span({ className: "js-worker-status worker__status" }, statusText),
           " ",
-          !this.isRunning() ? this.renderStartButton() : null
+          this.renderStartButton()
         )
       )
     );
   }
 }
 
 module.exports = Worker;
--- a/devtools/client/application/src/components/service-workers/WorkersPage.js
+++ b/devtools/client/application/src/components/service-workers/WorkersPage.js
@@ -23,47 +23,47 @@ const RegistrationListEmpty = createFact
 );
 
 class WorkersPage extends PureComponent {
   static get propTypes() {
     return {
       // mapped from state
       canDebugWorkers: PropTypes.bool.isRequired,
       domain: PropTypes.string.isRequired,
-      workers: Types.workerArray.isRequired,
+      registrations: Types.registrationArray.isRequired,
     };
   }
 
   render() {
-    const { canDebugWorkers, domain, workers } = this.props;
+    const { canDebugWorkers, domain, registrations } = this.props;
 
     // Filter out workers from other domains
-    const domainWorkers = workers.filter(
-      x => new URL(x.url).hostname === domain
+    const domainWorkers = registrations.filter(
+      x => x.workers.length > 0 && new URL(x.workers[0].url).hostname === domain
     );
-    const isWorkerListEmpty = domainWorkers.length === 0;
+    const isListEmpty = domainWorkers.length === 0;
 
     return section(
       {
         className: `app-page js-service-workers-page ${
-          isWorkerListEmpty ? "app-page--empty" : ""
+          isListEmpty ? "app-page--empty" : ""
         }`,
       },
-      isWorkerListEmpty
+      isListEmpty
         ? RegistrationListEmpty({})
         : RegistrationList({
             canDebugWorkers,
-            workers: domainWorkers,
+            registrations: domainWorkers,
           })
     );
   }
 }
 
 function mapStateToProps(state) {
   return {
     canDebugWorkers: state.workers.canDebugWorkers,
     domain: state.page.domain,
-    workers: state.workers.list,
+    registrations: state.workers.list,
   };
 }
 
 // Exports
 module.exports = connect(mapStateToProps)(WorkersPage);
--- a/devtools/client/application/src/components/service-workers/moz.build
+++ b/devtools/client/application/src/components/service-workers/moz.build
@@ -1,13 +1,15 @@
 # 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(
+    'Registration.css',
+    'Registration.js',
     'RegistrationList.css',
     'RegistrationList.js',
     'RegistrationListEmpty.css',
     'RegistrationListEmpty.js',
     'Worker.css',
     'Worker.js',
     'WorkersPage.css',
     'WorkersPage.js',
--- a/devtools/client/application/src/reducers/workers-state.js
+++ b/devtools/client/application/src/reducers/workers-state.js
@@ -8,33 +8,37 @@ const { Ci } = require("chrome");
 
 const {
   UPDATE_CAN_DEBUG_WORKERS,
   UPDATE_WORKERS,
 } = require("devtools/client/application/src/constants");
 
 function WorkersState() {
   return {
-    // Array of all service workers
+    // Array of all service worker registrations
     list: [],
     canDebugWorkers: false,
   };
 }
 
 function buildWorkerDataFromFronts({ registration, workers }) {
-  return workers.map(worker => ({
-    id: worker.id,
-    isActive: worker.state === Ci.nsIServiceWorkerInfo.STATE_ACTIVATED,
+  return {
+    id: registration.id,
+    lastUpdateTime: registration.lastUpdateTime,
+    registrationFront: registration,
     scope: registration.scope,
-    lastUpdateTime: registration.lastUpdateTime, // only available for active worker
-    url: worker.url,
-    registrationFront: registration,
-    workerTargetFront: worker.workerTargetFront,
-    stateText: worker.stateText,
-  }));
+    workers: workers.map(worker => ({
+      id: worker.id,
+      isActive: worker.state === Ci.nsIServiceWorkerInfo.STATE_ACTIVATED,
+      url: worker.url,
+      stateText: worker.stateText,
+      registrationFront: registration,
+      workerTargetFront: worker.workerTargetFront,
+    })),
+  };
 }
 
 function workersReducer(state = WorkersState(), action) {
   switch (action.type) {
     case UPDATE_CAN_DEBUG_WORKERS: {
       return Object.assign({}, state, {
         canDebugWorkers: action.canDebugWorkers,
       });
--- a/devtools/client/application/src/types/service-workers.js
+++ b/devtools/client/application/src/types/service-workers.js
@@ -4,23 +4,32 @@
 
 "use strict";
 
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 
 const worker = {
   id: PropTypes.string.isRequired,
   isActive: PropTypes.bool.isRequired,
-  lastUpdateTime: PropTypes.number,
-  scope: PropTypes.string.isRequired,
   stateText: PropTypes.string.isRequired,
   url: PropTypes.string.isRequired,
-  // registrationFront can be missing in e10s.
+  workerTargetFront: PropTypes.object,
   registrationFront: PropTypes.object,
-  workerTargetFront: PropTypes.object,
 };
 
 const workerArray = PropTypes.arrayOf(PropTypes.shape(worker));
 
+const registration = {
+  id: PropTypes.string.isRequired,
+  lastUpdateTime: PropTypes.number,
+  registrationFront: PropTypes.object.isRequired,
+  scope: PropTypes.string.isRequired,
+  workers: workerArray.isRequired,
+};
+
+const registrationArray = PropTypes.arrayOf(PropTypes.shape(registration));
+
 module.exports = {
+  registration,
+  registrationArray,
   worker,
   workerArray,
 };
--- a/devtools/client/application/test/node/components/service-workers/__snapshots__/components_application_panel-RegistrationList.test.js.snap
+++ b/devtools/client/application/test/node/components/service-workers/__snapshots__/components_application_panel-RegistrationList.test.js.snap
@@ -9,29 +9,35 @@ Array [
     <Localized
       id="serviceworker-list-header"
     >
       <h1
         className="app-page__title"
       />
     </Localized>
     <ul>
-      <Worker
+      <Registration
+        className="registrations-container__item"
         isDebugEnabled={true}
-        key="id-worker-1-example-0"
-        worker={
+        key="id-reg-1-example"
+        registration={
           Object {
-            "active": true,
-            "id": "id-worker-1-example",
-            "name": "worker1",
+            "id": "id-reg-1-example",
             "registrationFront": "",
             "scope": "SCOPE 123",
-            "state": 0,
-            "url": "http://example.com/worker.js",
-            "workerTargetFront": "",
+            "workers": Array [
+              Object {
+                "id": "id-worker-1-example",
+                "isActive": true,
+                "state": 4,
+                "stateText": "activated",
+                "url": "http://example.com/worker.js",
+                "workerTargetFront": "",
+              },
+            ],
           }
         }
       />
     </ul>
   </article>,
   <Localized
     a={
       <a
@@ -58,61 +64,79 @@ Array [
     <Localized
       id="serviceworker-list-header"
     >
       <h1
         className="app-page__title"
       />
     </Localized>
     <ul>
-      <Worker
+      <Registration
+        className="registrations-container__item"
         isDebugEnabled={true}
-        key="id-worker-1-example-0"
-        worker={
+        key="id-reg-1-example"
+        registration={
           Object {
-            "active": true,
-            "id": "id-worker-1-example",
-            "name": "worker1",
+            "id": "id-reg-1-example",
             "registrationFront": "",
-            "scope": "SCOPE 123",
-            "state": 0,
-            "url": "http://example.com/worker.js",
-            "workerTargetFront": "",
+            "scope": "SCOPE1",
+            "workers": Array [
+              Object {
+                "id": "id-worker-1-example",
+                "isActive": true,
+                "state": 4,
+                "stateText": "activated",
+                "url": "http://example.com/worker.js",
+                "workerTargetFront": "",
+              },
+            ],
           }
         }
       />
-      <Worker
+      <Registration
+        className="registrations-container__item"
         isDebugEnabled={true}
-        key="id-worker-2-example-0"
-        worker={
+        key="id-reg-1-example"
+        registration={
           Object {
-            "active": false,
-            "id": "id-worker-2-example",
-            "name": "worker2",
+            "id": "id-reg-1-example",
             "registrationFront": "",
-            "scope": "SCOPE 456",
-            "state": 0,
-            "url": "http://example.com/worker.js",
-            "workerTargetFront": "",
+            "scope": "SCOPE2",
+            "workers": Array [
+              Object {
+                "id": "id-worker-2-example",
+                "isActive": false,
+                "state": 2,
+                "stateText": "installed",
+                "url": "http://example.com/worker.js",
+                "workerTargetFront": "",
+              },
+            ],
           }
         }
       />
-      <Worker
+      <Registration
+        className="registrations-container__item"
         isDebugEnabled={true}
-        key="id-worker-3-example-0"
-        worker={
+        key="id-reg-3-example"
+        registration={
           Object {
-            "active": true,
-            "id": "id-worker-3-example",
-            "name": "worker3",
+            "id": "id-reg-3-example",
             "registrationFront": "",
-            "scope": "SCOPE 789",
-            "state": 0,
-            "url": "http://example.com/worker.js",
-            "workerTargetFront": "",
+            "scope": "SCOPE3",
+            "workers": Array [
+              Object {
+                "id": "id-worker-3-example",
+                "isActive": true,
+                "state": 4,
+                "stateText": "activated",
+                "url": "http://example.com/worker.js",
+                "workerTargetFront": "",
+              },
+            ],
           }
         }
       />
     </ul>
   </article>,
   <Localized
     a={
       <a
--- a/devtools/client/application/test/node/components/service-workers/__snapshots__/components_application_panel-WorkersPage.test.js.snap
+++ b/devtools/client/application/test/node/components/service-workers/__snapshots__/components_application_panel-WorkersPage.test.js.snap
@@ -1,37 +1,47 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`WorkersPage filters out workers from diferent domains 1`] = `
 <section
   className="app-page js-service-workers-page "
 >
   <RegistrationList
     canDebugWorkers={true}
-    workers={
+    registrations={
       Array [
         Object {
-          "active": true,
-          "id": "id-worker-1-example",
-          "name": "worker1",
+          "id": "id-reg-1-example",
           "registrationFront": "",
-          "scope": "SCOPE 123",
-          "state": 0,
-          "url": "http://example.com/worker.js",
-          "workerTargetFront": "",
+          "scope": "SCOPE1",
+          "workers": Array [
+            Object {
+              "id": "id-worker-1-example",
+              "isActive": true,
+              "state": 4,
+              "stateText": "activated",
+              "url": "http://example.com/worker.js",
+              "workerTargetFront": "",
+            },
+          ],
         },
         Object {
-          "active": false,
-          "id": "id-worker-2-example",
-          "name": "worker2",
+          "id": "id-reg-2-example",
           "registrationFront": "",
-          "scope": "SCOPE 456",
-          "state": 0,
-          "url": "http://example.com/worker.js",
-          "workerTargetFront": "",
+          "scope": "SCOPE2",
+          "workers": Array [
+            Object {
+              "id": "id-worker-2-example",
+              "isActive": true,
+              "state": 4,
+              "stateText": "activated",
+              "url": "http://example.com/worker.js",
+              "workerTargetFront": "",
+            },
+          ],
         },
       ]
     }
   />
 </section>
 `;
 
 exports[`WorkersPage filters out workers from different domains and renders an empty list when there is none left 1`] = `
@@ -43,71 +53,91 @@ exports[`WorkersPage filters out workers
 `;
 
 exports[`WorkersPage it renders a list with a single element if there's just 1 worker 1`] = `
 <section
   className="app-page js-service-workers-page "
 >
   <RegistrationList
     canDebugWorkers={true}
-    workers={
+    registrations={
       Array [
         Object {
-          "active": true,
-          "id": "id-worker-1-example",
-          "name": "worker1",
+          "id": "id-reg-1-example",
           "registrationFront": "",
           "scope": "SCOPE 123",
-          "state": 0,
-          "url": "http://example.com/worker.js",
-          "workerTargetFront": "",
+          "workers": Array [
+            Object {
+              "id": "id-worker-1-example",
+              "isActive": true,
+              "state": 4,
+              "stateText": "activated",
+              "url": "http://example.com/worker.js",
+              "workerTargetFront": "",
+            },
+          ],
         },
       ]
     }
   />
 </section>
 `;
 
 exports[`WorkersPage renders a list with multiple elements when there are multiple workers 1`] = `
 <section
   className="app-page js-service-workers-page "
 >
   <RegistrationList
     canDebugWorkers={true}
-    workers={
+    registrations={
       Array [
         Object {
-          "active": true,
-          "id": "id-worker-1-example",
-          "name": "worker1",
+          "id": "id-reg-1-example",
           "registrationFront": "",
-          "scope": "SCOPE 123",
-          "state": 0,
-          "url": "http://example.com/worker.js",
-          "workerTargetFront": "",
+          "scope": "SCOPE1",
+          "workers": Array [
+            Object {
+              "id": "id-worker-1-example",
+              "isActive": true,
+              "state": 4,
+              "stateText": "activated",
+              "url": "http://example.com/worker.js",
+              "workerTargetFront": "",
+            },
+          ],
         },
         Object {
-          "active": false,
-          "id": "id-worker-2-example",
-          "name": "worker2",
+          "id": "id-reg-1-example",
           "registrationFront": "",
-          "scope": "SCOPE 456",
-          "state": 0,
-          "url": "http://example.com/worker.js",
-          "workerTargetFront": "",
+          "scope": "SCOPE2",
+          "workers": Array [
+            Object {
+              "id": "id-worker-2-example",
+              "isActive": false,
+              "state": 2,
+              "stateText": "installed",
+              "url": "http://example.com/worker.js",
+              "workerTargetFront": "",
+            },
+          ],
         },
         Object {
-          "active": true,
-          "id": "id-worker-3-example",
-          "name": "worker3",
+          "id": "id-reg-3-example",
           "registrationFront": "",
-          "scope": "SCOPE 789",
-          "state": 0,
-          "url": "http://example.com/worker.js",
-          "workerTargetFront": "",
+          "scope": "SCOPE3",
+          "workers": Array [
+            Object {
+              "id": "id-worker-3-example",
+              "isActive": true,
+              "state": 4,
+              "stateText": "activated",
+              "url": "http://example.com/worker.js",
+              "workerTargetFront": "",
+            },
+          ],
         },
       ]
     }
   />
 </section>
 `;
 
 exports[`WorkersPage renders an empty list if there are no workers 1`] = `
--- a/devtools/client/application/test/node/components/service-workers/components_application_panel-RegistrationList.test.js
+++ b/devtools/client/application/test/node/components/service-workers/components_application_panel-RegistrationList.test.js
@@ -19,25 +19,25 @@ const RegistrationList = createFactory(
 
 /**
  * Test for RegistrationList.js component
  */
 describe("RegistrationList", () => {
   it("renders the expected snapshot for a list with a single registration", () => {
     const wrapper = shallow(
       RegistrationList({
-        workers: SINGLE_WORKER_DEFAULT_DOMAIN_LIST,
+        registrations: SINGLE_WORKER_DEFAULT_DOMAIN_LIST,
         canDebugWorkers: true,
       })
     );
     expect(wrapper).toMatchSnapshot();
   });
 
   it("renders the expected snapshot for a multiple registration list", () => {
     const wrapper = shallow(
       RegistrationList({
-        workers: MULTIPLE_WORKER_LIST,
+        registrations: MULTIPLE_WORKER_LIST,
         canDebugWorkers: true,
       })
     );
     expect(wrapper).toMatchSnapshot();
   });
 });
--- a/devtools/client/application/test/node/fixtures/data/constants.js
+++ b/devtools/client/application/test/node/fixtures/data/constants.js
@@ -1,105 +1,148 @@
 /* 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";
 
+// NOTE: worker state values are defined in an enum in nsIServiceWorkerManager
+// https://searchfox.org/mozilla-central/source/dom/interfaces/base/nsIServiceWorkerManager.idl
+
 const EMPTY_WORKER_LIST = [];
 
 const SINGLE_WORKER_DEFAULT_DOMAIN_LIST = [
   {
-    active: true,
-    name: "worker1",
+    id: "id-reg-1-example",
     scope: "SCOPE 123",
     registrationFront: "",
-    workerTargetFront: "",
-    url: "http://example.com/worker.js",
-    id: "id-worker-1-example",
-    state: 0,
+    workers: [
+      {
+        id: "id-worker-1-example",
+        isActive: true,
+        workerTargetFront: "",
+        url: "http://example.com/worker.js",
+        state: 4,
+        stateText: "activated",
+      },
+    ],
   },
 ];
 
 const SINGLE_WORKER_DIFFERENT_DOMAIN_LIST = [
   {
-    active: true,
-    name: "worker1",
+    id: "id-reg-1-example",
     scope: "SCOPE 123",
     registrationFront: "",
-    workerTargetFront: "",
-    url: "http://different-example.com/worker.js",
-    id: "id-worker-1-different-example",
-    state: 0,
+    workers: [
+      {
+        id: "id-worker-1-example",
+        isActive: true,
+        workerTargetFront: "",
+        url: "http://different-example.com/worker.js",
+        state: 4,
+        stateText: "activated",
+      },
+    ],
   },
 ];
 
 const MULTIPLE_WORKER_LIST = [
   {
-    active: true,
-    name: "worker1",
-    scope: "SCOPE 123",
+    id: "id-reg-1-example",
+    scope: "SCOPE1",
     registrationFront: "",
-    workerTargetFront: "",
-    url: "http://example.com/worker.js",
-    id: "id-worker-1-example",
-    state: 0,
+    workers: [
+      {
+        id: "id-worker-1-example",
+        isActive: true,
+        workerTargetFront: "",
+        url: "http://example.com/worker.js",
+        state: 4,
+        stateText: "activated",
+      },
+    ],
   },
   {
-    active: false,
-    name: "worker2",
-    scope: "SCOPE 456",
+    id: "id-reg-1-example",
+    scope: "SCOPE2",
     registrationFront: "",
-    workerTargetFront: "",
-    url: "http://example.com/worker.js",
-    id: "id-worker-2-example",
-    state: 0,
+    workers: [
+      {
+        id: "id-worker-2-example",
+        isActive: false,
+        workerTargetFront: "",
+        url: "http://example.com/worker.js",
+        state: 2,
+        stateText: "installed",
+      },
+    ],
   },
   {
-    active: true,
-    name: "worker3",
-    scope: "SCOPE 789",
+    id: "id-reg-3-example",
+    scope: "SCOPE3",
     registrationFront: "",
-    workerTargetFront: "",
-    url: "http://example.com/worker.js",
-    id: "id-worker-3-example",
-    state: 0,
+    workers: [
+      {
+        id: "id-worker-3-example",
+        isActive: true,
+        workerTargetFront: "",
+        url: "http://example.com/worker.js",
+        state: 4,
+        stateText: "activated",
+      },
+    ],
   },
 ];
 
 const MULTIPLE_WORKER_MIXED_DOMAINS_LIST = [
   {
-    active: true,
-    name: "worker1",
-    scope: "SCOPE 123",
+    id: "id-reg-1-example",
+    scope: "SCOPE1",
     registrationFront: "",
-    workerTargetFront: "",
-    url: "http://example.com/worker.js",
-    id: "id-worker-1-example",
-    state: 0,
+    workers: [
+      {
+        id: "id-worker-1-example",
+        isActive: true,
+        workerTargetFront: "",
+        url: "http://example.com/worker.js",
+        state: 4,
+        stateText: "activated",
+      },
+    ],
   },
   {
-    active: false,
-    name: "worker2",
-    scope: "SCOPE 456",
+    id: "id-reg-2-example",
+    scope: "SCOPE2",
     registrationFront: "",
-    workerTargetFront: "",
-    url: "http://example.com/worker.js",
-    id: "id-worker-2-example",
-    state: 0,
+    workers: [
+      {
+        id: "id-worker-2-example",
+        isActive: true,
+        workerTargetFront: "",
+        url: "http://example.com/worker.js",
+        state: 4,
+        stateText: "activated",
+      },
+    ],
   },
   {
-    active: true,
-    name: "worker3",
-    scope: "SCOPE 789",
+    id: "id-reg-3-example",
+    scope: "SCOPE3",
     registrationFront: "",
-    workerTargetFront: "",
-    url: "http://different-example.com/worker.js",
-    id: "id-worker-3-different-example",
-    state: 0,
+    workers: [
+      {
+        id: "id-worker-3-example",
+        isActive: true,
+        workerTargetFront: "",
+        url: "http://different-example.com/worker.js",
+        state: 4,
+        stateText: "activated",
+      },
+    ],
   },
 ];
 
 // props for a simple manifest
 const MANIFEST_SIMPLE = {
   icons: [
     {
       key: { sizes: "1x1", contentType: "image/png" },
--- a/devtools/client/application/test/xpcshell/test_workers_reducer.js
+++ b/devtools/client/application/test/xpcshell/test_workers_reducer.js
@@ -37,38 +37,60 @@ add_task(async function() {
   info("Test workers reducer: UPDATE_WORKERS action");
   const state = WorkersState();
 
   const rawData = [
     {
       registration: {
         scope: "lorem-ipsum",
         lastUpdateTime: 42,
+        id: "r1",
       },
       workers: [
         {
-          id: 1,
+          id: "w1",
           state: Ci.nsIServiceWorkerInfo.STATE_ACTIVATED,
-          url: "https://example.com",
+          url: "https://example.com/w1.js",
           workerTargetFront: { foo: "bar" },
           stateText: "activated",
         },
+        {
+          id: "w2",
+          state: Ci.nsIServiceWorkerInfo.STATE_INSTALLED,
+          url: "https://example.com/w2.js",
+          workerTargetFront: undefined,
+          stateText: "installed",
+        },
       ],
     },
   ];
 
   const expectedData = [
     {
-      id: 1,
-      isActive: true,
-      scope: "lorem-ipsum",
+      id: "r1",
       lastUpdateTime: 42,
-      url: "https://example.com",
       registrationFront: rawData[0].registration,
-      workerTargetFront: rawData[0].workers[0].workerTargetFront,
-      stateText: "activated",
+      scope: "lorem-ipsum",
+      workers: [
+        {
+          id: "w1",
+          isActive: true,
+          url: "https://example.com/w1.js",
+          workerTargetFront: rawData[0].workers[0].workerTargetFront,
+          registrationFront: rawData[0].registration,
+          stateText: "activated",
+        },
+        {
+          id: "w2",
+          isActive: false,
+          url: "https://example.com/w2.js",
+          workerTargetFront: undefined,
+          registrationFront: rawData[0].registration,
+          stateText: "installed",
+        },
+      ],
     },
   ];
 
   const action = updateWorkers(rawData);
   const newState = workersReducer(state, action);
   deepEqual(newState.list, expectedData, "workers contains the expected list");
 });
--- a/devtools/client/fronts/root.js
+++ b/devtools/client/fronts/root.js
@@ -69,17 +69,17 @@ class RootFront extends FrontClassWithSp
         .filter(w => !!w) // filter out non-existing workers
         // build a worker object with its WorkerTargetFront
         .map(workerFront => {
           const workerTargetFront = allWorkers.find(
             targetFront => targetFront.id === workerFront.id
           );
 
           return {
-            id: registrationFront.id,
+            id: workerFront.id,
             name: workerFront.url,
             state: workerFront.state,
             stateText: workerFront.stateText,
             url: workerFront.url,
             workerTargetFront,
           };
         });