summaryrefslogtreecommitdiffstats
path: root/devtools/client/application/src/components
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-21 11:44:51 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-21 11:44:51 +0000
commit9e3c08db40b8916968b9f30096c7be3f00ce9647 (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /devtools/client/application/src/components
parentInitial commit. (diff)
downloadthunderbird-upstream.tar.xz
thunderbird-upstream.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/application/src/components')
-rw-r--r--devtools/client/application/src/components/App.css34
-rw-r--r--devtools/client/application/src/components/App.js46
-rw-r--r--devtools/client/application/src/components/manifest/Manifest.js136
-rw-r--r--devtools/client/application/src/components/manifest/ManifestColorItem.css29
-rw-r--r--devtools/client/application/src/components/manifest/ManifestColorItem.js57
-rw-r--r--devtools/client/application/src/components/manifest/ManifestEmpty.js81
-rw-r--r--devtools/client/application/src/components/manifest/ManifestIconItem.css7
-rw-r--r--devtools/client/application/src/components/manifest/ManifestIconItem.js98
-rw-r--r--devtools/client/application/src/components/manifest/ManifestIssue.css17
-rw-r--r--devtools/client/application/src/components/manifest/ManifestIssue.js72
-rw-r--r--devtools/client/application/src/components/manifest/ManifestIssueList.css15
-rw-r--r--devtools/client/application/src/components/manifest/ManifestIssueList.js68
-rw-r--r--devtools/client/application/src/components/manifest/ManifestItem.css28
-rw-r--r--devtools/client/application/src/components/manifest/ManifestItem.js50
-rw-r--r--devtools/client/application/src/components/manifest/ManifestJsonLink.css9
-rw-r--r--devtools/client/application/src/components/manifest/ManifestJsonLink.js67
-rw-r--r--devtools/client/application/src/components/manifest/ManifestLoader.css14
-rw-r--r--devtools/client/application/src/components/manifest/ManifestLoader.js108
-rw-r--r--devtools/client/application/src/components/manifest/ManifestPage.js76
-rw-r--r--devtools/client/application/src/components/manifest/ManifestSection.css25
-rw-r--r--devtools/client/application/src/components/manifest/ManifestSection.js44
-rw-r--r--devtools/client/application/src/components/manifest/ManifestUrlItem.css8
-rw-r--r--devtools/client/application/src/components/manifest/ManifestUrlItem.js39
-rw-r--r--devtools/client/application/src/components/manifest/moz.build18
-rw-r--r--devtools/client/application/src/components/moz.build14
-rw-r--r--devtools/client/application/src/components/routing/PageSwitcher.css45
-rw-r--r--devtools/client/application/src/components/routing/PageSwitcher.js59
-rw-r--r--devtools/client/application/src/components/routing/Sidebar.css33
-rw-r--r--devtools/client/application/src/components/routing/Sidebar.js70
-rw-r--r--devtools/client/application/src/components/routing/SidebarItem.css33
-rw-r--r--devtools/client/application/src/components/routing/SidebarItem.js95
-rw-r--r--devtools/client/application/src/components/routing/moz.build5
-rw-r--r--devtools/client/application/src/components/service-workers/Registration.css73
-rw-r--r--devtools/client/application/src/components/service-workers/Registration.js153
-rw-r--r--devtools/client/application/src/components/service-workers/RegistrationList.css54
-rw-r--r--devtools/client/application/src/components/service-workers/RegistrationList.js92
-rw-r--r--devtools/client/application/src/components/service-workers/RegistrationListEmpty.js122
-rw-r--r--devtools/client/application/src/components/service-workers/Worker.css75
-rw-r--r--devtools/client/application/src/components/service-workers/Worker.js225
-rw-r--r--devtools/client/application/src/components/service-workers/WorkersPage.js71
-rw-r--r--devtools/client/application/src/components/service-workers/moz.build11
-rw-r--r--devtools/client/application/src/components/ui/UIButton.css75
-rw-r--r--devtools/client/application/src/components/ui/UIButton.js41
-rw-r--r--devtools/client/application/src/components/ui/moz.build7
44 files changed, 2499 insertions, 0 deletions
diff --git a/devtools/client/application/src/components/App.css b/devtools/client/application/src/components/App.css
new file mode 100644
index 0000000000..e0bcef7d2d
--- /dev/null
+++ b/devtools/client/application/src/components/App.css
@@ -0,0 +1,34 @@
+/* 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/. */
+
+/*
+ * General styles
+ */
+
+a.disabled-link,
+a.disabled-link:hover,
+a.disabled-link:visited {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.app {
+ display: grid;
+ min-width: calc(var(--base-unit) * 90);
+}
+
+/* wide layout -> two columns, 1 row */
+@media(min-width: 701px) {
+ .app {
+ grid-template-columns: calc(var(--base-unit) * 50) auto;
+ height: 100vh;
+ }
+}
+
+/* vertical layout -> 1 column, 2 rows */
+@media(max-width: 700px) {
+ .app {
+ grid-template-rows: auto 1fr;
+ }
+}
diff --git a/devtools/client/application/src/components/App.js b/devtools/client/application/src/components/App.js
new file mode 100644
index 0000000000..eac53e0cf6
--- /dev/null
+++ b/devtools/client/application/src/components/App.js
@@ -0,0 +1,46 @@
+/* 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("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ main,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const LocalizationProvider = createFactory(FluentReact.LocalizationProvider);
+
+const PageSwitcher = createFactory(
+ require("resource://devtools/client/application/src/components/routing/PageSwitcher.js")
+);
+const Sidebar = createFactory(
+ require("resource://devtools/client/application/src/components/routing/Sidebar.js")
+);
+
+/**
+ * This is the main component for the application panel.
+ */
+class App extends PureComponent {
+ static get propTypes() {
+ return {
+ fluentBundles: PropTypes.array.isRequired,
+ };
+ }
+
+ render() {
+ const { fluentBundles } = this.props;
+
+ return LocalizationProvider(
+ { bundles: fluentBundles },
+ main({ className: `app` }, Sidebar({}), PageSwitcher({}))
+ );
+ }
+}
+
+module.exports = App;
diff --git a/devtools/client/application/src/components/manifest/Manifest.js b/devtools/client/application/src/components/manifest/Manifest.js
new file mode 100644
index 0000000000..f6b5f19b37
--- /dev/null
+++ b/devtools/client/application/src/components/manifest/Manifest.js
@@ -0,0 +1,136 @@
+/* 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 {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ article,
+ h1,
+ table,
+ tbody,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+const {
+ l10n,
+} = require("resource://devtools/client/application/src/modules/l10n.js");
+
+const ManifestColorItem = createFactory(
+ require("resource://devtools/client/application/src/components/manifest/ManifestColorItem.js")
+);
+const ManifestIconItem = createFactory(
+ require("resource://devtools/client/application/src/components/manifest/ManifestIconItem.js")
+);
+const ManifestUrlItem = createFactory(
+ require("resource://devtools/client/application/src/components/manifest/ManifestUrlItem.js")
+);
+const ManifestItem = createFactory(
+ require("resource://devtools/client/application/src/components/manifest/ManifestItem.js")
+);
+const ManifestIssueList = createFactory(
+ require("resource://devtools/client/application/src/components/manifest/ManifestIssueList.js")
+);
+const ManifestSection = createFactory(
+ require("resource://devtools/client/application/src/components/manifest/ManifestSection.js")
+);
+const ManifestJsonLink = createFactory(
+ require("resource://devtools/client/application/src/components/manifest/ManifestJsonLink.js")
+);
+
+const {
+ MANIFEST_MEMBER_VALUE_TYPES,
+} = require("resource://devtools/client/application/src/constants.js");
+const Types = require("resource://devtools/client/application/src/types/index.js");
+
+/**
+ * A canonical manifest, splitted in different sections
+ */
+class Manifest extends PureComponent {
+ static get propTypes() {
+ return {
+ ...Types.manifest, // { identity, presentation, icons, validation, url }
+ };
+ }
+
+ renderIssueSection() {
+ const { validation } = this.props;
+ const shouldRender = validation && !!validation.length;
+
+ return shouldRender
+ ? ManifestSection(
+ {
+ key: `manifest-section-0`,
+ title: l10n.getString("manifest-item-warnings"),
+ },
+ ManifestIssueList({ issues: validation })
+ )
+ : null;
+ }
+
+ renderMember({ key, value, type }, index) {
+ let domKey = key;
+ switch (type) {
+ case MANIFEST_MEMBER_VALUE_TYPES.COLOR:
+ return ManifestColorItem({ label: key, key: domKey, value });
+ case MANIFEST_MEMBER_VALUE_TYPES.ICON:
+ // since the manifest may have keys with empty size/contentType,
+ // we cannot use them as unique IDs
+ domKey = index;
+ return ManifestIconItem({ label: key, key: domKey, value });
+ case MANIFEST_MEMBER_VALUE_TYPES.URL:
+ return ManifestUrlItem({ label: key, key: domKey, value });
+ case MANIFEST_MEMBER_VALUE_TYPES.STRING:
+ default:
+ return ManifestItem({ label: key, key: domKey }, value);
+ }
+ }
+
+ renderItemSections() {
+ const { identity, icons, presentation } = this.props;
+
+ const manifestMembers = [
+ { localizationId: "manifest-item-identity", items: identity },
+ { localizationId: "manifest-item-presentation", items: presentation },
+ { localizationId: "manifest-item-icons", items: icons },
+ ];
+
+ return manifestMembers.map(({ localizationId, items }, index) => {
+ return ManifestSection(
+ {
+ key: `manifest-section-${index + 1}`,
+ title: l10n.getString(localizationId),
+ },
+ // NOTE: this table should probably be its own component, to keep
+ // the same level of abstraction as with the validation issues
+ // Bug https://bugzilla.mozilla.org/show_bug.cgi?id=1577138
+ table({}, tbody({}, items.map(this.renderMember)))
+ );
+ });
+ }
+
+ render() {
+ const { url } = this.props;
+
+ return article(
+ { className: "js-manifest" },
+ Localized(
+ {
+ id: "manifest-view-header",
+ },
+ h1({ className: "app-page__title" })
+ ),
+ ManifestJsonLink({ url }),
+ this.renderIssueSection(),
+ this.renderItemSections()
+ );
+ }
+}
+
+// Exports
+module.exports = Manifest;
diff --git a/devtools/client/application/src/components/manifest/ManifestColorItem.css b/devtools/client/application/src/components/manifest/ManifestColorItem.css
new file mode 100644
index 0000000000..92dadec271
--- /dev/null
+++ b/devtools/client/application/src/components/manifest/ManifestColorItem.css
@@ -0,0 +1,29 @@
+/* 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/. */
+
+.manifest-item__color {
+ /* NOTE: platform converts any color format that is in the manifest to
+ hexadecimal, so we can uppercase */
+ text-transform: uppercase;
+ direction: ltr; /* force LTR so the # stays at the beginning of the hex number */
+ display: inline-block;
+}
+
+.manifest-item__color::before {
+ display: inline-block;
+ content: '';
+ background-color: #fff;
+ background-image: linear-gradient(var(--color-value), var(--color-value)), /* injected via React */
+ linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc),
+ linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc);
+ background-size: 6px 6px;
+ background-position: 0 0, 3px 3px;
+ border-radius: 50%;
+ width: calc(var(--base-unit) * 3);
+ height: calc(var(--base-unit) * 3);
+ margin-block-start: calc(var(--base-unit) * -0.5);
+ margin-inline-end: var(--base-unit);
+ box-shadow: 0 0 0 1px var(--theme-splitter-color);
+ vertical-align: middle;
+}
diff --git a/devtools/client/application/src/components/manifest/ManifestColorItem.js b/devtools/client/application/src/components/manifest/ManifestColorItem.js
new file mode 100644
index 0000000000..ac4b54e82f
--- /dev/null
+++ b/devtools/client/application/src/components/manifest/ManifestColorItem.js
@@ -0,0 +1,57 @@
+/* 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 {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ div,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const Types = require("resource://devtools/client/application/src/types/index.js");
+const ManifestItem = createFactory(
+ require("resource://devtools/client/application/src/components/manifest/ManifestItem.js")
+);
+
+/**
+ * This component displays a Manifest member which holds a color value
+ */
+class ManifestColorItem extends PureComponent {
+ static get propTypes() {
+ return {
+ ...Types.manifestItemColor, // { label, value }
+ };
+ }
+
+ renderColor() {
+ let { value } = this.props;
+ if (!value) {
+ return null;
+ }
+
+ // Truncate colors in #rrggbbaa format to #rrggbb
+ if (value.length === 9 && value.toLowerCase().endsWith("ff")) {
+ value = value.slice(0, 7);
+ }
+
+ /* div instead of span because CSS `direction` works with block elements */
+ return div(
+ {
+ className: "manifest-item__color",
+ style: { "--color-value": value },
+ },
+ value
+ );
+ }
+
+ render() {
+ const { label } = this.props;
+ return ManifestItem({ label }, this.renderColor());
+ }
+}
+
+module.exports = ManifestColorItem;
diff --git a/devtools/client/application/src/components/manifest/ManifestEmpty.js b/devtools/client/application/src/components/manifest/ManifestEmpty.js
new file mode 100644
index 0000000000..3e0eb2de48
--- /dev/null
+++ b/devtools/client/application/src/components/manifest/ManifestEmpty.js
@@ -0,0 +1,81 @@
+/* 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 { openDocLink } = require("resource://devtools/client/shared/link.js");
+
+const {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ a,
+ article,
+ aside,
+ div,
+ h1,
+ img,
+ p,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const DOC_URL =
+ "https://developer.mozilla.org/en-US/docs/Web/Manifest" +
+ "?utm_source=devtools&utm_medium=sw-panel-blank";
+
+/**
+ * This component displays help information when no manifest is found for the
+ * current target.
+ */
+class ManifestEmpty extends PureComponent {
+ openDocumentation() {
+ openDocLink(DOC_URL);
+ }
+
+ render() {
+ return article(
+ { className: "app-page__icon-container js-manifest-empty" },
+ aside(
+ {},
+ Localized(
+ {
+ id: "sidebar-item-manifest",
+ attrs: {
+ alt: true,
+ },
+ },
+ img({
+ className: "app-page__icon",
+ src: "chrome://devtools/skin/images/application-manifest.svg",
+ })
+ )
+ ),
+ div(
+ {},
+ Localized(
+ {
+ id: "manifest-empty-intro2",
+ },
+ h1({ className: "app-page__title" })
+ ),
+ p(
+ {},
+ Localized(
+ { id: "manifest-empty-intro-link" },
+ a({
+ onClick: () => this.openDocumentation(),
+ })
+ )
+ ),
+ Localized({ id: "manifest-non-existing" }, p({}))
+ )
+ );
+ }
+}
+
+// Exports
+module.exports = ManifestEmpty;
diff --git a/devtools/client/application/src/components/manifest/ManifestIconItem.css b/devtools/client/application/src/components/manifest/ManifestIconItem.css
new file mode 100644
index 0000000000..a2bbfd9d34
--- /dev/null
+++ b/devtools/client/application/src/components/manifest/ManifestIconItem.css
@@ -0,0 +1,7 @@
+/* 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/. */
+
+.manifest-item__icon {
+ max-width: 100%;
+}
diff --git a/devtools/client/application/src/components/manifest/ManifestIconItem.js b/devtools/client/application/src/components/manifest/ManifestIconItem.js
new file mode 100644
index 0000000000..a525fbcdb9
--- /dev/null
+++ b/devtools/client/application/src/components/manifest/ManifestIconItem.js
@@ -0,0 +1,98 @@
+/* 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 {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ br,
+ code,
+ img,
+ span,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+const {
+ l10n,
+} = require("resource://devtools/client/application/src/modules/l10n.js");
+
+const Types = require("resource://devtools/client/application/src/types/index.js");
+const ManifestItem = createFactory(
+ require("resource://devtools/client/application/src/components/manifest/ManifestItem.js")
+);
+
+/**
+ * This component displays a Manifest member which holds a color value
+ */
+class ManifestIconItem extends PureComponent {
+ static get propTypes() {
+ return {
+ // {
+ // label: { contentType, sizes },
+ // value: { src, purpose }
+ // }
+ ...Types.manifestItemIcon,
+ };
+ }
+
+ getLocalizedImgTitle() {
+ const { sizes } = this.props.label;
+
+ return sizes && sizes.length
+ ? l10n.getString("manifest-icon-img-title", { sizes })
+ : l10n.getString("manifest-icon-img-title-no-sizes");
+ }
+
+ renderLabel() {
+ const { contentType, sizes } = this.props.label;
+
+ // sinze both `contentType` and `sizes` may be undefined, we don't need to
+ // render the <br> if one –or both– are not to be displayed
+ const shallRenderBr = contentType && sizes;
+
+ return [
+ sizes ? sizes : null,
+ shallRenderBr ? br({ key: "label-br" }) : null,
+ contentType ? contentType : null,
+ ];
+ }
+
+ render() {
+ const { src, purpose } = this.props.value;
+
+ return ManifestItem(
+ {
+ label: this.renderLabel(),
+ },
+ Localized(
+ {
+ id: "manifest-icon-img",
+ attrs: {
+ alt: true,
+ },
+ },
+ img({
+ className: "manifest-item__icon",
+ src,
+ title: this.getLocalizedImgTitle(),
+ })
+ ),
+ br({}),
+ Localized(
+ {
+ id: "manifest-icon-purpose",
+ code: code({}),
+ $purpose: purpose,
+ },
+ span({})
+ )
+ );
+ }
+}
+
+module.exports = ManifestIconItem;
diff --git a/devtools/client/application/src/components/manifest/ManifestIssue.css b/devtools/client/application/src/components/manifest/ManifestIssue.css
new file mode 100644
index 0000000000..96bcdae5dd
--- /dev/null
+++ b/devtools/client/application/src/components/manifest/ManifestIssue.css
@@ -0,0 +1,17 @@
+/* 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/. */
+
+.manifest-issue__icon {
+ -moz-context-properties: fill;
+ width: calc(var(--base-unit) * 3);
+ height: calc(var(--base-unit) * 3);
+}
+
+.manifest-issue__icon--warning {
+ fill: var(--theme-icon-warning-color);
+}
+
+.manifest-issue__icon--error {
+ fill: var(--theme-icon-error-color);
+}
diff --git a/devtools/client/application/src/components/manifest/ManifestIssue.js b/devtools/client/application/src/components/manifest/ManifestIssue.js
new file mode 100644
index 0000000000..1cdf62ff8f
--- /dev/null
+++ b/devtools/client/application/src/components/manifest/ManifestIssue.js
@@ -0,0 +1,72 @@
+/* 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("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ img,
+ li,
+ span,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const {
+ MANIFEST_ISSUE_LEVELS,
+} = require("resource://devtools/client/application/src/constants.js");
+const Types = require("resource://devtools/client/application/src/types/index.js");
+
+/**
+ * A Manifest validation issue (warning, error)
+ */
+class ManifestIssue extends PureComponent {
+ static get propTypes() {
+ return {
+ className: PropTypes.string,
+ ...Types.manifestIssue, // { level, message }
+ };
+ }
+
+ getIconData(level) {
+ switch (level) {
+ case MANIFEST_ISSUE_LEVELS.WARNING:
+ return {
+ src: "chrome://devtools/skin/images/alert-small.svg",
+ localizationId: "icon-warning",
+ };
+ case MANIFEST_ISSUE_LEVELS.ERROR:
+ default:
+ return {
+ src: "chrome://devtools/skin/images/error-small.svg",
+ localizationId: "icon-error",
+ };
+ }
+ }
+
+ render() {
+ const { level, message, className } = this.props;
+ const icon = this.getIconData(level);
+
+ return li(
+ { className: `js-manifest-issue ${className ? className : ""}` },
+ Localized(
+ { id: icon.localizationId, attrs: { alt: true, title: true } },
+ img({
+ className: `manifest-issue__icon manifest-issue__icon--${level}`,
+ src: icon.src,
+ })
+ ),
+ span({}, message)
+ );
+ }
+}
+
+// Exports
+module.exports = ManifestIssue;
diff --git a/devtools/client/application/src/components/manifest/ManifestIssueList.css b/devtools/client/application/src/components/manifest/ManifestIssueList.css
new file mode 100644
index 0000000000..ccb3f08df5
--- /dev/null
+++ b/devtools/client/application/src/components/manifest/ManifestIssueList.css
@@ -0,0 +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/. */
+
+.manifest-issues {
+ list-style-type: none;
+ padding-inline-start: 0;
+ display: grid;
+ grid-template-columns: auto 1fr;
+ grid-gap: var(--base-unit);
+}
+
+.manifest-issues__item {
+ display: contents;
+}
diff --git a/devtools/client/application/src/components/manifest/ManifestIssueList.js b/devtools/client/application/src/components/manifest/ManifestIssueList.js
new file mode 100644
index 0000000000..4b3d1f46c8
--- /dev/null
+++ b/devtools/client/application/src/components/manifest/ManifestIssueList.js
@@ -0,0 +1,68 @@
+/* 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 {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ ul,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const {
+ MANIFEST_ISSUE_LEVELS,
+} = require("resource://devtools/client/application/src/constants.js");
+const Types = require("resource://devtools/client/application/src/types/index.js");
+
+const ManifestIssue = createFactory(
+ require("resource://devtools/client/application/src/components/manifest/ManifestIssue.js")
+);
+
+/**
+ * A collection of manifest issues (errors, warnings)
+ */
+class ManifestIssueList extends PureComponent {
+ static get propTypes() {
+ return {
+ issues: Types.manifestIssueArray.isRequired,
+ };
+ }
+
+ // group the errors by level, and order by severity
+ groupIssuesByLevel() {
+ const { issues } = this.props;
+
+ const errors = issues.filter(x => x.level === MANIFEST_ISSUE_LEVELS.ERROR);
+ const warnings = issues.filter(
+ x => x.level === MANIFEST_ISSUE_LEVELS.WARNING
+ );
+
+ return [errors, warnings];
+ }
+
+ render() {
+ const groups = this.groupIssuesByLevel().filter(list => !!list.length);
+
+ return groups.map((list, listIndex) => {
+ return ul(
+ {
+ className: "manifest-issues js-manifest-issues",
+ key: `issuelist-${listIndex}`,
+ },
+ list.map((issue, issueIndex) =>
+ ManifestIssue({
+ className: "manifest-issues__item",
+ key: `issue-${issueIndex}`,
+ ...issue,
+ })
+ )
+ );
+ });
+ }
+}
+
+// Exports
+module.exports = ManifestIssueList;
diff --git a/devtools/client/application/src/components/manifest/ManifestItem.css b/devtools/client/application/src/components/manifest/ManifestItem.css
new file mode 100644
index 0000000000..94da03e9b9
--- /dev/null
+++ b/devtools/client/application/src/components/manifest/ManifestItem.css
@@ -0,0 +1,28 @@
+/* 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/. */
+
+.manifest-item {
+ vertical-align: baseline;
+}
+
+.manifest-item__label {
+ box-sizing: border-box;
+ min-width: calc(var(--base-unit) * 32);
+ padding-inline-end: calc(var(--base-unit) * 4);
+ padding-inline-start: 0;
+ vertical-align: top;
+ color: var(--theme-text-color-alt);
+ font-weight: inherit;
+ text-align: start;
+}
+
+.manifest-item__value {
+ word-break: break-all;
+ vertical-align: top;
+}
+
+.manifest-item__label,
+.manifest-item__value {
+ padding-block: calc(var(--base-unit) * 1);
+}
diff --git a/devtools/client/application/src/components/manifest/ManifestItem.js b/devtools/client/application/src/components/manifest/ManifestItem.js
new file mode 100644
index 0000000000..96c47a1a7f
--- /dev/null
+++ b/devtools/client/application/src/components/manifest/ManifestItem.js
@@ -0,0 +1,50 @@
+/* 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("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ tr,
+ td,
+ th,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+/**
+ * This component displays a key-value data pair from a manifest
+ */
+class ManifestItem extends PureComponent {
+ static get propTypes() {
+ return {
+ label: PropTypes.node.isRequired,
+ children: PropTypes.node,
+ };
+ }
+
+ render() {
+ const { children, label } = this.props;
+ return tr(
+ {
+ className: "manifest-item js-manifest-item",
+ },
+ th(
+ {
+ className: "manifest-item__label js-manifest-item-label",
+ scope: "row",
+ },
+ label
+ ),
+ td(
+ { className: "manifest-item__value js-manifest-item-content" },
+ children
+ )
+ );
+ }
+}
+
+// Exports
+module.exports = ManifestItem;
diff --git a/devtools/client/application/src/components/manifest/ManifestJsonLink.css b/devtools/client/application/src/components/manifest/ManifestJsonLink.css
new file mode 100644
index 0000000000..52343226f9
--- /dev/null
+++ b/devtools/client/application/src/components/manifest/ManifestJsonLink.css
@@ -0,0 +1,9 @@
+/* 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/. */
+
+.manifest-json-link {
+ /* this is so it has an implicit width and the link inside gets to truncate
+ with an ellipsis */
+ display: grid;
+}
diff --git a/devtools/client/application/src/components/manifest/ManifestJsonLink.js b/devtools/client/application/src/components/manifest/ManifestJsonLink.js
new file mode 100644
index 0000000000..d35924b5c3
--- /dev/null
+++ b/devtools/client/application/src/components/manifest/ManifestJsonLink.js
@@ -0,0 +1,67 @@
+/* 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 { openDocLink } = require("resource://devtools/client/shared/link.js");
+
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+
+const {
+ a,
+ p,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+/**
+ * This component displays an "Open JSON" link for the Manifest
+ */
+class ManifestJsonLink extends PureComponent {
+ static get propTypes() {
+ return {
+ url: PropTypes.string.isRequired,
+ };
+ }
+
+ get shouldRenderLink() {
+ const { url } = this.props;
+ // Firefox blocks the loading of Data URLs with mimetypes manifest+json unless
+ // explicitly typed by the user in the address bar.
+ // So we are not showing the link in this case.
+ // See more details in this post:
+ // https://blog.mozilla.org/security/2017/11/27/blocking-top-level-navigations-data-urls-firefox-59/
+ return !url.startsWith("data:");
+ }
+
+ renderLink() {
+ const { url } = this.props;
+
+ return a(
+ {
+ className: "js-manifest-json-link devtools-ellipsis-text",
+ href: "#",
+ title: url,
+ onClick: () => openDocLink(url),
+ },
+ url
+ );
+ }
+
+ render() {
+ return p(
+ { className: "manifest-json-link" },
+ this.shouldRenderLink
+ ? this.renderLink()
+ : Localized({ id: "manifest-json-link-data-url" })
+ );
+ }
+}
+
+module.exports = ManifestJsonLink;
diff --git a/devtools/client/application/src/components/manifest/ManifestLoader.css b/devtools/client/application/src/components/manifest/ManifestLoader.css
new file mode 100644
index 0000000000..4f728ff6e3
--- /dev/null
+++ b/devtools/client/application/src/components/manifest/ManifestLoader.css
@@ -0,0 +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/. */
+
+.manifest-loader {
+ font-size: var(--body-20-font-size);
+ font-weight: var(--body-20-font-weight);
+}
+
+.manifest-loader__load {
+ /* TODO: implement a spinner when tackling the UX review bug
+ https://bugzilla.mozilla.org/show_bug.cgi?id=1566023 */
+ text-align: center;
+}
diff --git a/devtools/client/application/src/components/manifest/ManifestLoader.js b/devtools/client/application/src/components/manifest/ManifestLoader.js
new file mode 100644
index 0000000000..9183b50ff3
--- /dev/null
+++ b/devtools/client/application/src/components/manifest/ManifestLoader.js
@@ -0,0 +1,108 @@
+/* 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 {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const {
+ aside,
+ h1,
+ p,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+
+const {
+ fetchManifest,
+} = require("resource://devtools/client/application/src/actions/manifest.js");
+
+class ManifestLoader extends PureComponent {
+ static get propTypes() {
+ return {
+ // these props get automatically injected via `connect`
+ dispatch: PropTypes.func.isRequired,
+ error: PropTypes.string,
+ hasFetchedManifest: PropTypes.bool.isRequired,
+ isLoading: PropTypes.bool.isRequired,
+ };
+ }
+
+ componentDidMount() {
+ this.loadManifestIfNeeded();
+ }
+
+ componentDidUpdate() {
+ this.loadManifestIfNeeded();
+ }
+
+ loadManifestIfNeeded() {
+ const { isLoading, hasFetchedManifest } = this.props;
+ const shallLoad = !isLoading && !hasFetchedManifest;
+ if (shallLoad) {
+ this.props.dispatch(fetchManifest());
+ }
+ }
+
+ renderResult() {
+ return Localized(
+ { id: "manifest-loaded-ok" },
+ p({ className: "js-manifest-loaded-ok" })
+ );
+ }
+
+ renderError() {
+ const { error } = this.props;
+
+ return [
+ Localized(
+ {
+ id: "manifest-loaded-error",
+ key: "manifest-error-label",
+ },
+ h1({ className: "js-manifest-loaded-error app-page__title" })
+ ),
+ p({ className: "technical-text", key: "manifest-error-message" }, error),
+ ];
+ }
+
+ render() {
+ const { error, isLoading } = this.props;
+
+ const loadingDOM = isLoading
+ ? Localized(
+ { id: "manifest-loading" },
+ p({ className: "manifest-loader__load js-manifest-loading" })
+ )
+ : null;
+
+ const errorDOM = error ? this.renderError() : null;
+ const resultDOM = !isLoading && !error ? this.renderResult() : null;
+
+ return aside(
+ { className: "manifest-loader" },
+ loadingDOM,
+ errorDOM,
+ resultDOM
+ );
+ }
+}
+
+const mapDispatchToProps = dispatch => ({ dispatch });
+const mapStateToProps = state => ({
+ error: state.manifest.errorMessage,
+ hasFetchedManifest: typeof state.manifest.manifest !== "undefined",
+ isLoading: state.manifest.isLoading,
+});
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(ManifestLoader);
diff --git a/devtools/client/application/src/components/manifest/ManifestPage.js b/devtools/client/application/src/components/manifest/ManifestPage.js
new file mode 100644
index 0000000000..60caea591e
--- /dev/null
+++ b/devtools/client/application/src/components/manifest/ManifestPage.js
@@ -0,0 +1,76 @@
+/* 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("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ section,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+
+const Types = require("resource://devtools/client/application/src/types/index.js");
+
+const ManifestLoader = createFactory(
+ require("resource://devtools/client/application/src/components/manifest/ManifestLoader.js")
+);
+const Manifest = createFactory(
+ require("resource://devtools/client/application/src/components/manifest/Manifest.js")
+);
+const ManifestEmpty = createFactory(
+ require("resource://devtools/client/application/src/components/manifest/ManifestEmpty.js")
+);
+
+class ManifestPage extends PureComponent {
+ static get propTypes() {
+ return {
+ // these props are automatically injected via connect
+ hasLoadingFailed: PropTypes.bool.isRequired,
+ isManifestLoading: PropTypes.bool.isRequired,
+ manifest: PropTypes.shape(Types.manifest),
+ };
+ }
+
+ get shouldShowLoader() {
+ const { isManifestLoading, hasLoadingFailed } = this.props;
+ const mustLoadManifest = typeof this.props.manifest === "undefined";
+ return isManifestLoading || mustLoadManifest || hasLoadingFailed;
+ }
+
+ renderManifest() {
+ const { manifest } = this.props;
+ return manifest ? Manifest({ ...manifest }) : ManifestEmpty({});
+ }
+
+ render() {
+ const { manifest } = this.props;
+
+ return section(
+ {
+ className: `app-page js-manifest-page ${
+ !manifest ? "app-page--empty" : ""
+ }`,
+ },
+ this.shouldShowLoader ? ManifestLoader({}) : this.renderManifest()
+ );
+ }
+}
+
+function mapStateToProps(state) {
+ return {
+ hasLoadingFailed: !!state.manifest.errorMessage,
+ isManifestLoading: state.manifest.isLoading,
+ manifest: state.manifest.manifest,
+ };
+}
+
+// Exports
+module.exports = connect(mapStateToProps)(ManifestPage);
diff --git a/devtools/client/application/src/components/manifest/ManifestSection.css b/devtools/client/application/src/components/manifest/ManifestSection.css
new file mode 100644
index 0000000000..479a6d1f79
--- /dev/null
+++ b/devtools/client/application/src/components/manifest/ManifestSection.css
@@ -0,0 +1,25 @@
+/* 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/. */
+
+.manifest-section {
+ padding-block: calc(var(--base-unit) * 2);
+ width: 100%;
+ border-spacing: calc(var(--base-unit) * 2) 0;
+ font-size: var(--body-10-font-size);
+ font-weight: var(--body-10-font-weight);
+}
+
+.manifest-section--empty {
+ padding-block-end: 0;
+}
+
+.manifest-section:not(:last-child) {
+ border-bottom: 1px solid var(--separator-color);
+}
+
+.manifest-section__title {
+ font-size: var(--title-10-font-size);
+ font-weight: var(--title-10-font-weight);
+ margin: 0;
+}
diff --git a/devtools/client/application/src/components/manifest/ManifestSection.js b/devtools/client/application/src/components/manifest/ManifestSection.js
new file mode 100644
index 0000000000..4aa92c6a15
--- /dev/null
+++ b/devtools/client/application/src/components/manifest/ManifestSection.js
@@ -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/. */
+
+"use strict";
+
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ h2,
+ section,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+/**
+ * A section of a manifest in the form of a captioned table.
+ */
+class ManifestSection extends PureComponent {
+ static get propTypes() {
+ return {
+ children: PropTypes.node,
+ title: PropTypes.string.isRequired,
+ };
+ }
+
+ render() {
+ const { children, title } = this.props;
+ const isEmpty = !children || children.length === 0;
+
+ return section(
+ {
+ className: `manifest-section ${
+ isEmpty ? "manifest-section--empty" : ""
+ }`,
+ },
+ h2({ className: "manifest-section__title" }, title),
+ children
+ );
+ }
+}
+
+// Exports
+module.exports = ManifestSection;
diff --git a/devtools/client/application/src/components/manifest/ManifestUrlItem.css b/devtools/client/application/src/components/manifest/ManifestUrlItem.css
new file mode 100644
index 0000000000..9702e7e261
--- /dev/null
+++ b/devtools/client/application/src/components/manifest/ManifestUrlItem.css
@@ -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/. */
+
+.manifest-item__url {
+ direction: ltr; /* force LTR so the # stays at the beginning of the hex number */
+ display: inline-block;
+}
diff --git a/devtools/client/application/src/components/manifest/ManifestUrlItem.js b/devtools/client/application/src/components/manifest/ManifestUrlItem.js
new file mode 100644
index 0000000000..03705d441a
--- /dev/null
+++ b/devtools/client/application/src/components/manifest/ManifestUrlItem.js
@@ -0,0 +1,39 @@
+/* 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 {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ div,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const Types = require("resource://devtools/client/application/src/types/index.js");
+const ManifestItem = createFactory(
+ require("resource://devtools/client/application/src/components/manifest/ManifestItem.js")
+);
+
+/**
+ * This component displays a Manifest member which holds a URL
+ */
+class ManifestUrlItem extends PureComponent {
+ static get propTypes() {
+ return {
+ ...Types.manifestItemUrl, // { label, value }
+ };
+ }
+
+ render() {
+ const { label, value } = this.props;
+ return ManifestItem(
+ { label },
+ div({ className: "manifest-item__url" }, value)
+ );
+ }
+}
+
+module.exports = ManifestUrlItem;
diff --git a/devtools/client/application/src/components/manifest/moz.build b/devtools/client/application/src/components/manifest/moz.build
new file mode 100644
index 0000000000..bb799cbfc4
--- /dev/null
+++ b/devtools/client/application/src/components/manifest/moz.build
@@ -0,0 +1,18 @@
+# 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(
+ "Manifest.js",
+ "ManifestColorItem.js",
+ "ManifestEmpty.js",
+ "ManifestIconItem.js",
+ "ManifestIssue.js",
+ "ManifestIssueList.js",
+ "ManifestItem.js",
+ "ManifestJsonLink.js",
+ "ManifestLoader.js",
+ "ManifestPage.js",
+ "ManifestSection.js",
+ "ManifestUrlItem.js",
+)
diff --git a/devtools/client/application/src/components/moz.build b/devtools/client/application/src/components/moz.build
new file mode 100644
index 0000000000..361ec01204
--- /dev/null
+++ b/devtools/client/application/src/components/moz.build
@@ -0,0 +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 += [
+ "routing",
+ "manifest",
+ "service-workers",
+ "ui",
+]
+
+DevToolsModules(
+ "App.js",
+)
diff --git a/devtools/client/application/src/components/routing/PageSwitcher.css b/devtools/client/application/src/components/routing/PageSwitcher.css
new file mode 100644
index 0000000000..e713adb1bf
--- /dev/null
+++ b/devtools/client/application/src/components/routing/PageSwitcher.css
@@ -0,0 +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/. */
+
+
+/*
+ * Page container for worker + manifest views
+ */
+
+.app-page {
+ padding: calc(var(--base-unit) * 3) calc(var(--base-unit) * 8);
+ user-select: none;
+ overflow-y: auto;
+}
+
+.app-page--empty {
+ display: grid;
+ align-items: center;
+ justify-content: center;
+ font-size: var(--body-10-font-size);
+ color: var(--theme-toolbar-color);
+}
+
+.app-page__title {
+ font-size: var(--title-20-font-size);
+ font-weight: var(--title-20-font-weight);
+ margin: 0;
+}
+
+.app-page__icon-container {
+ display: grid;
+ grid-template-columns: auto 1fr;
+ grid-column-gap: calc(var(--base-unit) * 4);
+}
+
+.app-page__icon {
+ width: calc(var(--base-unit) * 10);
+ height: calc(var(--base-unit) * 10);
+
+ fill: var(--dimmed-icon-color);
+ -moz-context-properties: fill;
+
+ /* alignment fix for text to compensate for low baseline */
+ margin-block-start: var(--base-unit);
+}
diff --git a/devtools/client/application/src/components/routing/PageSwitcher.js b/devtools/client/application/src/components/routing/PageSwitcher.js
new file mode 100644
index 0000000000..9305744da9
--- /dev/null
+++ b/devtools/client/application/src/components/routing/PageSwitcher.js
@@ -0,0 +1,59 @@
+/* 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 {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+
+const {
+ PAGE_TYPES,
+} = require("resource://devtools/client/application/src/constants.js");
+const Types = require("resource://devtools/client/application/src/types/index.js");
+
+const ManifestPage = createFactory(
+ require("resource://devtools/client/application/src/components/manifest/ManifestPage.js")
+);
+const WorkersPage = createFactory(
+ require("resource://devtools/client/application/src/components/service-workers/WorkersPage.js")
+);
+
+class PageSwitcher extends PureComponent {
+ static get propTypes() {
+ return {
+ page: Types.page.isRequired,
+ };
+ }
+
+ render() {
+ let component = null;
+
+ switch (this.props.page) {
+ case PAGE_TYPES.MANIFEST:
+ component = ManifestPage({});
+ break;
+ case PAGE_TYPES.SERVICE_WORKERS:
+ component = WorkersPage({});
+ break;
+ default:
+ console.error("Unknown path. Can not direct to a page.");
+ return null;
+ }
+
+ return component;
+ }
+}
+
+function mapStateToProps(state) {
+ return {
+ page: state.ui.selectedPage,
+ };
+}
+
+module.exports = connect(mapStateToProps)(PageSwitcher);
diff --git a/devtools/client/application/src/components/routing/Sidebar.css b/devtools/client/application/src/components/routing/Sidebar.css
new file mode 100644
index 0000000000..872f5cca86
--- /dev/null
+++ b/devtools/client/application/src/components/routing/Sidebar.css
@@ -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/. */
+
+
+/*
+ * Sidebar list container
+ */
+.sidebar {
+ background-color: var(--bg-color);
+}
+
+/* vertical layout -> the sidebar is the first row */
+@media(max-width: 700px) {
+ .sidebar {
+ border-block-end: 1px solid var(--separator-color);
+ }
+}
+
+/* wide layout -> the sidebar occupies a whole column on the side */
+@media(min-width: 701px) {
+ .sidebar {
+ min-height: 100vh;
+ border-inline-end: 1px solid var(--separator-color);
+ }
+}
+
+.sidebar__list {
+ list-style: none;
+ padding: 0;
+ font-size: var(--body-10-font-size);
+ font-weight: var(--body-10-font-weight);
+}
diff --git a/devtools/client/application/src/components/routing/Sidebar.js b/devtools/client/application/src/components/routing/Sidebar.js
new file mode 100644
index 0000000000..99d260571e
--- /dev/null
+++ b/devtools/client/application/src/components/routing/Sidebar.js
@@ -0,0 +1,70 @@
+/* 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 {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ aside,
+ ul,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+
+const SidebarItem = createFactory(
+ require("resource://devtools/client/application/src/components/routing/SidebarItem.js")
+);
+
+const Types = require("resource://devtools/client/application/src/types/index.js");
+const {
+ PAGE_TYPES,
+} = require("resource://devtools/client/application/src/constants.js");
+
+class Sidebar extends PureComponent {
+ static get propTypes() {
+ return {
+ // this prop is automatically injected via connect
+ selectedPage: Types.page.isRequired,
+ };
+ }
+
+ render() {
+ const navItems = [PAGE_TYPES.SERVICE_WORKERS, PAGE_TYPES.MANIFEST];
+
+ const isSelected = page => {
+ return page === this.props.selectedPage;
+ };
+
+ return aside(
+ {
+ className: "sidebar js-sidebar",
+ },
+ ul(
+ {
+ className: "sidebar__list",
+ },
+ navItems.map(page => {
+ return SidebarItem({
+ page,
+ key: `sidebar-item-${page}`,
+ isSelected: isSelected(page),
+ });
+ })
+ )
+ );
+ }
+}
+
+function mapStateToProps(state) {
+ return {
+ selectedPage: state.ui.selectedPage,
+ };
+}
+
+module.exports = connect(mapStateToProps)(Sidebar);
diff --git a/devtools/client/application/src/components/routing/SidebarItem.css b/devtools/client/application/src/components/routing/SidebarItem.css
new file mode 100644
index 0000000000..f1852748ab
--- /dev/null
+++ b/devtools/client/application/src/components/routing/SidebarItem.css
@@ -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/. */
+
+
+/*
+ * Sidebar list items
+ */
+
+.sidebar-item {
+ display: grid;
+ grid-template-columns: auto 1fr;
+ grid-gap: var(--base-unit);
+ padding: calc(var(--base-unit)) calc(var(--base-unit) * 6);
+ user-select: none;
+ cursor: pointer;
+}
+
+.sidebar-item--selected {
+ background-color: var(--theme-selection-background);
+ color: var(--theme-selection-color);
+}
+
+.sidebar-item:not(.sidebar-item--selected):hover {
+ background-color: var(--highlight-color);
+}
+
+.sidebar-item__icon {
+ height: calc(var(--base-unit) * 4);
+ width: calc(var(--base-unit) * 4);
+ -moz-context-properties: fill;
+ fill: currentColor;
+}
diff --git a/devtools/client/application/src/components/routing/SidebarItem.js b/devtools/client/application/src/components/routing/SidebarItem.js
new file mode 100644
index 0000000000..5683856aaf
--- /dev/null
+++ b/devtools/client/application/src/components/routing/SidebarItem.js
@@ -0,0 +1,95 @@
+/* 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 {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ img,
+ li,
+ span,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const Actions = require("resource://devtools/client/application/src/actions/index.js");
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const {
+ PAGE_TYPES,
+} = require("resource://devtools/client/application/src/constants.js");
+const Types = require("resource://devtools/client/application/src/types/index.js");
+
+const ICONS = {
+ [PAGE_TYPES.MANIFEST]:
+ "chrome://devtools/skin/images/application-manifest.svg",
+ [PAGE_TYPES.SERVICE_WORKERS]:
+ "chrome://devtools/skin/images/debugging-workers.svg",
+};
+
+const LOCALIZATION_IDS = {
+ [PAGE_TYPES.MANIFEST]: "sidebar-item-manifest",
+ [PAGE_TYPES.SERVICE_WORKERS]: "sidebar-item-service-workers",
+};
+
+class SidebarItem extends PureComponent {
+ static get propTypes() {
+ return {
+ page: Types.page.isRequired,
+ isSelected: PropTypes.bool.isRequired,
+ // this prop is automatically injected via connect
+ dispatch: PropTypes.func.isRequired,
+ };
+ }
+
+ render() {
+ const { isSelected, page } = this.props;
+
+ return li(
+ {
+ className: `sidebar-item js-sidebar-${page} ${
+ isSelected ? "sidebar-item--selected" : ""
+ }`,
+ onClick: () => {
+ const { dispatch } = this.props;
+ dispatch(Actions.updateSelectedPage(page));
+ },
+ role: "link",
+ },
+ Localized(
+ {
+ id: LOCALIZATION_IDS[page],
+ attrs: {
+ alt: true,
+ title: true,
+ },
+ },
+ img({
+ src: ICONS[page],
+ className: "sidebar-item__icon",
+ })
+ ),
+ Localized(
+ {
+ id: LOCALIZATION_IDS[page],
+ attrs: {
+ title: true,
+ },
+ },
+ span({ className: "devtools-ellipsis-text" })
+ )
+ );
+ }
+}
+
+const mapDispatchToProps = dispatch => ({ dispatch });
+module.exports = connect(mapDispatchToProps)(SidebarItem);
diff --git a/devtools/client/application/src/components/routing/moz.build b/devtools/client/application/src/components/routing/moz.build
new file mode 100644
index 0000000000..7e22985614
--- /dev/null
+++ b/devtools/client/application/src/components/routing/moz.build
@@ -0,0 +1,5 @@
+# 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("PageSwitcher.js", "Sidebar.js", "SidebarItem.js")
diff --git a/devtools/client/application/src/components/service-workers/Registration.css b/devtools/client/application/src/components/service-workers/Registration.css
new file mode 100644
index 0000000000..84b6de58e1
--- /dev/null
+++ b/devtools/client/application/src/components/service-workers/Registration.css
@@ -0,0 +1,73 @@
+/* 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 registration is
+ *
+ * +------+----------------------+----------------+
+ * | Header - scope + timestamp | Unregister_btn |
+ * +------+----------------------+----------------|
+ * | worker 1 |
+ | worker 2 |
+ | ... |
+ +----------------------------------------------+
+ | Unregister btn |
+ +----------------------------------------------+
+ */
+
+.registration {
+ line-height: 1.5;
+ font-size: var(--body-10-font-size);
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ grid-template-rows: minmax(calc(var(--base-unit) * 6), auto) 1fr auto;
+ grid-column-gap: calc(4 * var(--base-unit));
+ grid-row-gap: calc(2 * var(--base-unit));
+ grid-template-areas: "header header-controls"
+ "workers workers"
+ "footer-controls footer-controls";
+}
+
+/* vertical layout */
+@media(max-width: 700px) {
+ .registration__controls {
+ grid-area: footer-controls;
+ justify-self: end;
+ }
+}
+
+/* wide layout */
+@media(min-width: 701px) {
+ .registration__controls {
+ grid-area: header-controls;
+ }
+}
+
+.registration__header {
+ grid-area: header;
+}
+
+.registration__scope {
+ font-size: var(--title-10-font-size);
+ font-weight: var(--title-10-font-weight);
+ user-select: text;
+ margin: 0;
+
+ grid-area: scope;
+}
+
+.registration__updated-time {
+ color: var(--theme-text-color-alt);
+ grid-area: timestamp;
+}
+
+.registration__workers {
+ grid-area: workers;
+ list-style-type: none;
+ padding: 0;
+}
+
+.registration__workers-item:not(:first-child) {
+ margin-block-start: calc(var(--base-unit) * 2);
+}
diff --git a/devtools/client/application/src/components/service-workers/Registration.js b/devtools/client/application/src/components/service-workers/Registration.js
new file mode 100644
index 0000000000..97569f57e2
--- /dev/null
+++ b/devtools/client/application/src/components/service-workers/Registration.js
@@ -0,0 +1,153 @@
+/* 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 {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const {
+ article,
+ aside,
+ h2,
+ header,
+ li,
+ p,
+ time,
+ ul,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const {
+ getUnicodeUrl,
+} = require("resource://devtools/client/shared/unicode-url.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const Types = require("resource://devtools/client/application/src/types/index.js");
+
+const {
+ unregisterWorker,
+} = require("resource://devtools/client/application/src/actions/workers.js");
+
+const UIButton = createFactory(
+ require("resource://devtools/client/application/src/components/ui/UIButton.js")
+);
+
+const Worker = createFactory(
+ require("resource://devtools/client/application/src/components/service-workers/Worker.js")
+);
+
+/**
+ * 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 Registration extends PureComponent {
+ static get propTypes() {
+ return {
+ className: PropTypes.string,
+ isDebugEnabled: PropTypes.bool.isRequired,
+ registration: PropTypes.shape(Types.registration).isRequired,
+ // this prop get automatically injected via `connect`
+ dispatch: PropTypes.func.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.unregister = this.unregister.bind(this);
+ }
+
+ unregister() {
+ this.props.dispatch(unregisterWorker(this.props.registration));
+ }
+
+ isActive() {
+ const { workers } = this.props.registration;
+ return workers.some(
+ x => x.state === Ci.nsIServiceWorkerInfo.STATE_ACTIVATED
+ );
+ }
+
+ formatScope(scope) {
+ const [, remainder] = getUnicodeUrl(scope).split("://");
+ // remove the last slash from the url, if present
+ // or return the full scope if there's no remainder
+ return remainder ? remainder.replace(/\/$/, "") : scope;
+ }
+
+ render() {
+ const { registration, isDebugEnabled, className } = this.props;
+
+ const unregisterButton = this.isActive()
+ ? Localized(
+ { id: "serviceworker-worker-unregister" },
+ UIButton({
+ onClick: this.unregister,
+ className: "js-unregister-button",
+ })
+ )
+ : null;
+
+ 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. registration.lastUpdateTime is in microseconds,
+ // convert to a valid timestamp in milliseconds by dividing by 1000.
+ $date: registration.lastUpdateTime / 1000,
+ time: time({ className: "js-sw-updated" }),
+ },
+ p({ className: "registration__updated-time" })
+ )
+ : null;
+
+ const scope = h2(
+ {
+ title: registration.scope,
+ className: "registration__scope js-sw-scope devtools-ellipsis-text",
+ },
+ this.formatScope(registration.scope)
+ );
+
+ return li(
+ { className: className ? className : "" },
+ article(
+ { className: "registration js-sw-container" },
+ header({ className: "registration__header" }, scope, lastUpdated),
+ aside({ className: "registration__controls" }, unregisterButton),
+ // render list of workers
+ ul(
+ { className: "registration__workers" },
+ registration.workers.map(worker => {
+ return li(
+ {
+ key: worker.id,
+ className: "registration__workers-item",
+ },
+ Worker({
+ worker,
+ isDebugEnabled,
+ })
+ );
+ })
+ )
+ )
+ );
+ }
+}
+
+const mapDispatchToProps = dispatch => ({ dispatch });
+module.exports = connect(mapDispatchToProps)(Registration);
diff --git a/devtools/client/application/src/components/service-workers/RegistrationList.css b/devtools/client/application/src/components/service-workers/RegistrationList.css
new file mode 100644
index 0000000000..021ed7e351
--- /dev/null
+++ b/devtools/client/application/src/components/service-workers/RegistrationList.css
@@ -0,0 +1,54 @@
+/* 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/. */
+
+.aboutdebugging-plug {
+ padding-block: calc(var(--base-unit) * 3);
+ border-block-start: 1px solid var(--separator-color);
+
+ /* display flex to handle showing the icon with ::before */
+ display: flex;
+ flex-direction: row;
+ column-gap: calc(var(--base-unit) * 2);
+ align-items: baseline;
+ font-size: var(--body-10-font-size);
+ font-weight: var(--body-10-font-weight);
+}
+
+.aboutdebugging-plug::before {
+ flex: 0 0 auto;
+ width: calc(var(--base-unit) * 4);
+ height: calc(var(--base-unit) * 4);
+ content: "";
+ -moz-context-properties: fill;
+ fill: currentColor;
+ background-image: url(chrome://global/skin/icons/developer.svg);
+ /* the icon size is taller than the line-height of the text. Since the
+ text can occupy multiple lines, and we want to keep the icon aligned
+ with respect to the first line, instead of align-items: center in
+ .aboutdebugging-plug, we use baseline, and fine tune the position here. */
+ position: relative;
+ top: 3px;
+}
+
+.registrations-container {
+ flex-grow: 1;
+}
+
+.registrations-container__list {
+ padding-inline-start: 0;
+}
+
+.registrations-container__item {
+ list-style-type: none;
+ margin: 0;
+ padding: calc(var(--base-unit) * 5) 0;
+}
+
+.registrations-container__item:first-child {
+ padding-top: 0;
+}
+
+.registrations-container__item:not(:last-child) {
+ border-bottom: 1px solid var(--separator-color);
+}
diff --git a/devtools/client/application/src/components/service-workers/RegistrationList.js b/devtools/client/application/src/components/service-workers/RegistrationList.js
new file mode 100644
index 0000000000..ed89c9cff3
--- /dev/null
+++ b/devtools/client/application/src/components/service-workers/RegistrationList.js
@@ -0,0 +1,92 @@
+/* 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 {
+ openTrustedLink,
+} = require("resource://devtools/client/shared/link.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ a,
+ article,
+ footer,
+ h1,
+ p,
+ ul,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const Types = require("resource://devtools/client/application/src/types/index.js");
+const Registration = createFactory(
+ require("resource://devtools/client/application/src/components/service-workers/Registration.js")
+);
+
+/**
+ * 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,
+ registrations: Types.registrationArray.isRequired,
+ };
+ }
+
+ render() {
+ const { canDebugWorkers, registrations } = this.props;
+
+ return [
+ article(
+ {
+ className: "registrations-container",
+ key: "registrations-container",
+ },
+ Localized(
+ { id: "serviceworker-list-header" },
+ h1({
+ className: "app-page__title",
+ })
+ ),
+ ul(
+ { className: "registrations-container__list" },
+ registrations.map(registration =>
+ Registration({
+ key: registration.id,
+ isDebugEnabled: canDebugWorkers,
+ registration,
+ className: "registrations-container__item",
+ })
+ )
+ )
+ ),
+
+ footer(
+ { className: "aboutdebugging-plug" },
+ Localized(
+ {
+ id: "serviceworker-list-aboutdebugging",
+ key: "serviceworkerlist-footer",
+ a: a({
+ className: "aboutdebugging-plug__link",
+ onClick: () => openTrustedLink("about:debugging#workers"),
+ }),
+ },
+ p({})
+ )
+ ),
+ ];
+ }
+}
+
+// Exports
+module.exports = RegistrationList;
diff --git a/devtools/client/application/src/components/service-workers/RegistrationListEmpty.js b/devtools/client/application/src/components/service-workers/RegistrationListEmpty.js
new file mode 100644
index 0000000000..2454f121e8
--- /dev/null
+++ b/devtools/client/application/src/components/service-workers/RegistrationListEmpty.js
@@ -0,0 +1,122 @@
+/* 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 {
+ openDocLink,
+ openTrustedLink,
+} = require("resource://devtools/client/shared/link.js");
+const {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const {
+ a,
+ article,
+ aside,
+ div,
+ h1,
+ img,
+ p,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const {
+ services,
+} = require("resource://devtools/client/application/src/modules/application-services.js");
+
+const DOC_URL =
+ "https://developer.mozilla.org/docs/Web/API/Service_Worker_API/Using_Service_Workers" +
+ "?utm_source=devtools&utm_medium=sw-panel-blank";
+
+/**
+ * This component displays help information when no service workers are found for the
+ * current target.
+ */
+class RegistrationListEmpty extends PureComponent {
+ switchToConsole() {
+ services.selectTool("webconsole");
+ }
+
+ switchToDebugger() {
+ services.selectTool("jsdebugger");
+ }
+
+ openAboutDebugging() {
+ openTrustedLink("about:debugging#workers");
+ }
+
+ openDocumentation() {
+ openDocLink(DOC_URL);
+ }
+
+ render() {
+ return article(
+ { className: "app-page__icon-container js-registration-list-empty" },
+ aside(
+ {},
+ Localized(
+ {
+ id: "sidebar-item-service-workers",
+ attrs: {
+ alt: true,
+ },
+ },
+ img({
+ className: "app-page__icon",
+ src: "chrome://devtools/skin/images/debugging-workers.svg",
+ })
+ )
+ ),
+ div(
+ {},
+ Localized(
+ {
+ id: "serviceworker-empty-intro2",
+ },
+ h1({ className: "app-page__title" })
+ ),
+ Localized(
+ {
+ id: "serviceworker-empty-suggestions2",
+ a: a({
+ onClick: () => this.switchToConsole(),
+ }),
+ // NOTE: for <Localized> to parse the markup in the string, the
+ // markup needs to be actual HTML elements
+ span: a({
+ onClick: () => this.switchToDebugger(),
+ }),
+ },
+ p({})
+ ),
+ p(
+ {},
+ Localized(
+ { id: "serviceworker-empty-intro-link" },
+ a({
+ onClick: () => this.openDocumentation(),
+ })
+ )
+ ),
+ p(
+ {},
+ Localized(
+ { id: "serviceworker-empty-suggestions-aboutdebugging2" },
+ a({
+ className: "js-trusted-link",
+ onClick: () => this.openAboutDebugging(),
+ })
+ )
+ )
+ )
+ );
+ }
+}
+
+// Exports
+module.exports = RegistrationListEmpty;
diff --git a/devtools/client/application/src/components/service-workers/Worker.css b/devtools/client/application/src/components/service-workers/Worker.css
new file mode 100644
index 0000000000..e44b49ef6b
--- /dev/null
+++ b/devtools/client/application/src/components/service-workers/Worker.css
@@ -0,0 +1,75 @@
+/* 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
+ *
+ * +------------+------------------------------+
+ * | Worker | script_name |
+ * | Icon |------------------------------|
+ * | | status start_button |
+ * +------------+------------------------------+
+ */
+
+.worker {
+ display: grid;
+ grid-template-columns: auto 1fr;
+ grid-template-areas: "icon source"
+ "icon misc";
+ column-gap: calc(var(--base-unit) * 2);
+ row-gap: var(--base-unit);
+
+ line-height: calc(var(--base-unit) * 4);
+ font-size: var(--body-10-font-size);
+}
+
+.worker__icon {
+ grid-area: icon;
+}
+
+.worker__icon-image {
+ width: calc(var(--base-unit) * 4);
+ height: calc(var(--base-unit) * 4);
+}
+
+.worker__source {
+ grid-area: source;
+ user-select: text;
+}
+
+.worker__misc {
+ grid-area: misc;
+}
+
+.worker__status {
+ text-transform: capitalize;
+ --status-bg-color: transparent;
+ --status-border-color: transparent;
+}
+
+.worker__status::before {
+ content: "";
+ margin-inline-end: var(--base-unit);
+ width: calc(var(--base-unit) * 2);
+ height: calc(var(--base-unit) * 2);
+ display: inline-block;
+ background: var(--status-bg-color);
+ border: 1px solid var(--status-border-color);
+ border-radius: 100%;
+}
+
+.worker__status--active {
+ --status-bg-color: var(--green-60);
+ --status-border-color: var(--green-60);
+}
+
+.worker__status--waiting {
+ --status-bg-color: var(--theme-text-color-alt);
+ --status-border-color: var(--theme-text-color-alt);
+}
+
+.worker__status--installing, .worker__status--default {
+ --status-bg-color: transparent;
+ --status-border-color: var(--theme-text-color-alt);
+}
diff --git a/devtools/client/application/src/components/service-workers/Worker.js b/devtools/client/application/src/components/service-workers/Worker.js
new file mode 100644
index 0000000000..bc95e084a9
--- /dev/null
+++ b/devtools/client/application/src/components/service-workers/Worker.js
@@ -0,0 +1,225 @@
+/* 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 {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+
+const {
+ a,
+ img,
+ p,
+ section,
+ span,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const {
+ getUnicodeUrlPath,
+} = require("resource://devtools/client/shared/unicode-url.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+const {
+ l10n,
+} = require("resource://devtools/client/application/src/modules/l10n.js");
+
+const {
+ services,
+} = require("resource://devtools/client/application/src/modules/application-services.js");
+const Types = require("resource://devtools/client/application/src/types/index.js");
+
+const {
+ startWorker,
+} = require("resource://devtools/client/application/src/actions/workers.js");
+
+const UIButton = createFactory(
+ require("resource://devtools/client/application/src/components/ui/UIButton.js")
+);
+
+/**
+ * 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 PureComponent {
+ static get propTypes() {
+ return {
+ isDebugEnabled: PropTypes.bool.isRequired,
+ worker: PropTypes.shape(Types.worker).isRequired,
+ // this prop get automatically injected via `connect`
+ dispatch: PropTypes.func.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.debug = this.debug.bind(this);
+ this.viewSource = this.viewSource.bind(this);
+ this.start = this.start.bind(this);
+ }
+
+ debug() {
+ if (!this.isRunning()) {
+ console.log("Service workers cannot be debugged if they are not running");
+ return;
+ }
+
+ services.openWorkerInDebugger(this.props.worker.workerDescriptorFront);
+ }
+
+ viewSource() {
+ if (!this.isRunning()) {
+ console.log(
+ "Service workers cannot be inspected if they are not running"
+ );
+ return;
+ }
+
+ services.viewWorkerSource(this.props.worker.workerDescriptorFront);
+ }
+
+ start() {
+ if (!this.isActive() || this.isRunning()) {
+ console.log("Running or inactive service workers cannot be started");
+ return;
+ }
+
+ this.props.dispatch(startWorker(this.props.worker));
+ }
+
+ isRunning() {
+ // We know the worker is running if it has a worker actor.
+ return !!this.props.worker.workerDescriptorFront;
+ }
+
+ isActive() {
+ return this.props.worker.state === Ci.nsIServiceWorkerInfo.STATE_ACTIVATED;
+ }
+
+ 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;
+ }
+
+ getClassNameForStatus(baseClass) {
+ const { state } = this.props.worker;
+
+ switch (state) {
+ case Ci.nsIServiceWorkerInfo.STATE_PARSED:
+ case Ci.nsIServiceWorkerInfo.STATE_INSTALLING:
+ return "worker__status--installing";
+ case Ci.nsIServiceWorkerInfo.STATE_INSTALLED:
+ case Ci.nsIServiceWorkerInfo.STATE_ACTIVATING:
+ return "worker__status--waiting";
+ case Ci.nsIServiceWorkerInfo.STATE_ACTIVATED:
+ return "worker__status--active";
+ }
+
+ return "worker__status--default";
+ }
+
+ formatSource(source) {
+ const parts = source.split("/");
+ return getUnicodeUrlPath(parts[parts.length - 1]);
+ }
+
+ renderInspectLink(url) {
+ // avoid rendering the inspect link if sw is not running
+ const isDisabled = !this.isRunning();
+ // view source instead of debugging when debugging sw is not available
+ const callbackFn = this.props.isDebugEnabled ? this.debug : this.viewSource;
+
+ const sourceUrl = span(
+ { className: "js-source-url" },
+ this.formatSource(url)
+ );
+
+ return isDisabled
+ ? sourceUrl
+ : a(
+ {
+ onClick: callbackFn,
+ title: url,
+ href: "#",
+ className: "js-inspect-link",
+ },
+ sourceUrl,
+ "\u00A0", // &nbsp;
+ Localized(
+ {
+ id: "serviceworker-worker-inspect-icon",
+ attrs: {
+ alt: true,
+ },
+ },
+ img({
+ src: "chrome://devtools/skin/images/application-debug.svg",
+ })
+ )
+ );
+ }
+
+ renderStartButton() {
+ // 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;
+ }
+
+ return Localized(
+ { id: "serviceworker-worker-start3" },
+ UIButton({
+ onClick: this.start,
+ className: `js-start-button`,
+ size: "micro",
+ })
+ );
+ }
+
+ render() {
+ const { worker } = this.props;
+ const statusText = this.getLocalizedStatus();
+ const statusClassName = this.getClassNameForStatus();
+
+ return section(
+ { className: "worker js-sw-worker" },
+ p(
+ { className: "worker__icon" },
+ img({
+ className: "worker__icon-image",
+ src: "chrome://devtools/skin/images/debugging-workers.svg",
+ })
+ ),
+ p({ className: "worker__source" }, this.renderInspectLink(worker.url)),
+ p(
+ { className: "worker__misc" },
+ span(
+ { className: `js-worker-status worker__status ${statusClassName}` },
+ statusText
+ ),
+ " ",
+ this.renderStartButton()
+ )
+ );
+ }
+}
+
+const mapDispatchToProps = dispatch => ({ dispatch });
+module.exports = connect(mapDispatchToProps)(Worker);
diff --git a/devtools/client/application/src/components/service-workers/WorkersPage.js b/devtools/client/application/src/components/service-workers/WorkersPage.js
new file mode 100644
index 0000000000..a44dd292f8
--- /dev/null
+++ b/devtools/client/application/src/components/service-workers/WorkersPage.js
@@ -0,0 +1,71 @@
+/* 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 {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ section,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+
+const Types = require("resource://devtools/client/application/src/types/index.js");
+const RegistrationList = createFactory(
+ require("resource://devtools/client/application/src/components/service-workers/RegistrationList.js")
+);
+const RegistrationListEmpty = createFactory(
+ require("resource://devtools/client/application/src/components/service-workers/RegistrationListEmpty.js")
+);
+
+class WorkersPage extends PureComponent {
+ static get propTypes() {
+ return {
+ // mapped from state
+ canDebugWorkers: PropTypes.bool.isRequired,
+ domain: PropTypes.string.isRequired,
+ registrations: Types.registrationArray.isRequired,
+ };
+ }
+
+ render() {
+ const { canDebugWorkers, domain, registrations } = this.props;
+
+ // Filter out workers from other domains
+ const domainWorkers = registrations.filter(
+ x => !!x.workers.length && new URL(x.workers[0].url).hostname === domain
+ );
+ const isListEmpty = domainWorkers.length === 0;
+
+ return section(
+ {
+ className: `app-page js-service-workers-page ${
+ isListEmpty ? "app-page--empty" : ""
+ }`,
+ },
+ isListEmpty
+ ? RegistrationListEmpty({})
+ : RegistrationList({
+ canDebugWorkers,
+ registrations: domainWorkers,
+ })
+ );
+ }
+}
+
+function mapStateToProps(state) {
+ return {
+ canDebugWorkers: state.workers.canDebugWorkers,
+ domain: state.page.domain,
+ registrations: state.workers.list,
+ };
+}
+
+// Exports
+module.exports = connect(mapStateToProps)(WorkersPage);
diff --git a/devtools/client/application/src/components/service-workers/moz.build b/devtools/client/application/src/components/service-workers/moz.build
new file mode 100644
index 0000000000..f9704b9df8
--- /dev/null
+++ b/devtools/client/application/src/components/service-workers/moz.build
@@ -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/.
+
+DevToolsModules(
+ "Registration.js",
+ "RegistrationList.js",
+ "RegistrationListEmpty.js",
+ "Worker.js",
+ "WorkersPage.js",
+)
diff --git a/devtools/client/application/src/components/ui/UIButton.css b/devtools/client/application/src/components/ui/UIButton.css
new file mode 100644
index 0000000000..2d614e09b0
--- /dev/null
+++ b/devtools/client/application/src/components/ui/UIButton.css
@@ -0,0 +1,75 @@
+/* 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/. */
+
+/* these styles com from Photon. Keep in mind that the "default" style is not used
+ in panels, and we should use the "micro" instead for default, stand-alone buttons. */
+
+:root.theme-light {
+ --button-text-color: var(--grey-90);
+ --button-text-hover-color: var(--grey-90);
+ --button-text-pressed-color: var(--grey-90);
+ --button-background-color: var(--grey-90-a10);
+ --button-background-hover-color: var(--grey-90-a20);
+ --button-background-pressed-color: var(--grey-90-a30);
+}
+
+:root.theme-dark {
+ --button-text-color: var(--grey-40);
+ --button-text-hover-color: var(--grey-30);
+ --button-text-pressed-color: var(--grey-30);
+ --button-background-color: var(--grey-10-a20);
+ --button-background-hover-color: var(--grey-10-a25);
+ --button-background-pressed-color: var(--grey-10-a30);
+}
+
+.ui-button {
+ appearance: none;
+ transition: background-color 0.05s ease-in-out;
+
+ margin: 0;
+ height: calc(var(--base-unit) * 6);
+ padding-inline-start: calc(2 * var(--base-unit));
+ padding-inline-end: calc(2 * var(--base-unit));
+ border: none;
+ border-radius: calc(var(--base-unit) / 2);
+
+ color: var(--button-text-color);
+ background: var(--button-background-color);
+ font-size: var(--caption-10-font-size);
+}
+
+.ui-button:-moz-focusring {
+ outline: none;
+}
+.ui-button::-moz-focus-inner {
+ border: 0;
+ padding: 0;
+}
+
+.ui-button:enabled:hover {
+ background: var(--button-background-hover-color);
+ color: var(--button-text-hover-color);
+}
+
+.ui-button:enabled:active {
+ background: var(--button-background-pressed-color);
+ color: var(--button-text-pressed-color);
+}
+
+.ui-button:focus {
+ box-shadow: 0 0 0 1px var(--blue-50) inset,
+ 0 0 0 1px var(--blue-50),
+ 0 0 0 4px var(--blue-50-a30);
+}
+
+.ui-button:disabled {
+ opacity: 0.4;
+}
+
+/* Note: this "micro" variant here is not the same as the "micro" variant
+ in Photon docs (since we are using that one for our default size) */
+.ui-button--micro {
+ height: auto;
+ padding: calc(var(--base-unit) * 0.5) var(--base-unit);
+}
diff --git a/devtools/client/application/src/components/ui/UIButton.js b/devtools/client/application/src/components/ui/UIButton.js
new file mode 100644
index 0000000000..bc3f297d85
--- /dev/null
+++ b/devtools/client/application/src/components/ui/UIButton.js
@@ -0,0 +1,41 @@
+/* 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 {
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ button,
+} = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+class UIButton extends PureComponent {
+ static get propTypes() {
+ return {
+ children: PropTypes.node,
+ className: PropTypes.string,
+ disabled: PropTypes.bool,
+ onClick: PropTypes.func,
+ size: PropTypes.oneOf(["micro"]),
+ };
+ }
+
+ render() {
+ const { className, disabled, onClick, size } = this.props;
+ const sizeClass = size ? `ui-button--${size}` : "";
+
+ return button(
+ {
+ className: `ui-button ${className || ""} ${sizeClass}`,
+ onClick,
+ disabled,
+ },
+ this.props.children
+ );
+ }
+}
+
+module.exports = UIButton;
diff --git a/devtools/client/application/src/components/ui/moz.build b/devtools/client/application/src/components/ui/moz.build
new file mode 100644
index 0000000000..f62f66d310
--- /dev/null
+++ b/devtools/client/application/src/components/ui/moz.build
@@ -0,0 +1,7 @@
+# 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(
+ "UIButton.js",
+)