summaryrefslogtreecommitdiffstats
path: root/devtools/client/application/src
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--devtools/client/application/src/actions/index.js12
-rw-r--r--devtools/client/application/src/actions/manifest.js50
-rw-r--r--devtools/client/application/src/actions/moz.build11
-rw-r--r--devtools/client/application/src/actions/page.js20
-rw-r--r--devtools/client/application/src/actions/ui.js20
-rw-r--r--devtools/client/application/src/actions/workers.js51
-rw-r--r--devtools/client/application/src/base.css83
-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
-rw-r--r--devtools/client/application/src/constants.js61
-rw-r--r--devtools/client/application/src/create-store.js50
-rw-r--r--devtools/client/application/src/middleware/event-telemetry.js37
-rw-r--r--devtools/client/application/src/middleware/moz.build7
-rw-r--r--devtools/client/application/src/modules/application-services.js85
-rw-r--r--devtools/client/application/src/modules/l10n.js12
-rw-r--r--devtools/client/application/src/modules/moz.build8
-rw-r--r--devtools/client/application/src/moz.build17
-rw-r--r--devtools/client/application/src/reducers/index.js28
-rw-r--r--devtools/client/application/src/reducers/manifest-state.js158
-rw-r--r--devtools/client/application/src/reducers/moz.build11
-rw-r--r--devtools/client/application/src/reducers/page-state.js39
-rw-r--r--devtools/client/application/src/reducers/ui-state.js30
-rw-r--r--devtools/client/application/src/reducers/workers-state.js65
-rw-r--r--devtools/client/application/src/types/index.js18
-rw-r--r--devtools/client/application/src/types/manifest.js89
-rw-r--r--devtools/client/application/src/types/moz.build10
-rw-r--r--devtools/client/application/src/types/routing.js16
-rw-r--r--devtools/client/application/src/types/service-workers.js35
70 files changed, 3522 insertions, 0 deletions
diff --git a/devtools/client/application/src/actions/index.js b/devtools/client/application/src/actions/index.js
new file mode 100644
index 0000000000..67e9cbfd88
--- /dev/null
+++ b/devtools/client/application/src/actions/index.js
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const workers = require("resource://devtools/client/application/src/actions/workers.js");
+const page = require("resource://devtools/client/application/src/actions/page.js");
+const ui = require("resource://devtools/client/application/src/actions/ui.js");
+const manifest = require("resource://devtools/client/application/src/actions/manifest.js");
+
+Object.assign(exports, workers, page, ui, manifest);
diff --git a/devtools/client/application/src/actions/manifest.js b/devtools/client/application/src/actions/manifest.js
new file mode 100644
index 0000000000..050fab2b89
--- /dev/null
+++ b/devtools/client/application/src/actions/manifest.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 {
+ l10n,
+} = require("resource://devtools/client/application/src/modules/l10n.js");
+
+const {
+ services,
+ ManifestDevToolsError,
+} = require("resource://devtools/client/application/src/modules/application-services.js");
+const {
+ FETCH_MANIFEST_FAILURE,
+ FETCH_MANIFEST_START,
+ FETCH_MANIFEST_SUCCESS,
+ RESET_MANIFEST,
+} = require("resource://devtools/client/application/src/constants.js");
+
+function fetchManifest() {
+ return async ({ dispatch, getState }) => {
+ dispatch({ type: FETCH_MANIFEST_START });
+ try {
+ const manifest = await services.fetchManifest();
+ dispatch({ type: FETCH_MANIFEST_SUCCESS, manifest });
+ } catch (error) {
+ let errorMessage = error.message;
+
+ // since Firefox DevTools errors may not make sense for the user, swap
+ // their message for a generic one.
+ if (error instanceof ManifestDevToolsError) {
+ console.error(error);
+ errorMessage = l10n.getString("manifest-loaded-devtools-error");
+ }
+
+ dispatch({ type: FETCH_MANIFEST_FAILURE, error: errorMessage });
+ }
+ };
+}
+
+function resetManifest() {
+ return { type: RESET_MANIFEST };
+}
+
+module.exports = {
+ fetchManifest,
+ resetManifest,
+};
diff --git a/devtools/client/application/src/actions/moz.build b/devtools/client/application/src/actions/moz.build
new file mode 100644
index 0000000000..f2a41f8674
--- /dev/null
+++ b/devtools/client/application/src/actions/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(
+ "index.js",
+ "manifest.js",
+ "page.js",
+ "ui.js",
+ "workers.js",
+)
diff --git a/devtools/client/application/src/actions/page.js b/devtools/client/application/src/actions/page.js
new file mode 100644
index 0000000000..1348a4aaa2
--- /dev/null
+++ b/devtools/client/application/src/actions/page.js
@@ -0,0 +1,20 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ UPDATE_DOMAIN,
+} = require("resource://devtools/client/application/src/constants.js");
+
+function updateDomain(url) {
+ return {
+ type: UPDATE_DOMAIN,
+ url,
+ };
+}
+
+module.exports = {
+ updateDomain,
+};
diff --git a/devtools/client/application/src/actions/ui.js b/devtools/client/application/src/actions/ui.js
new file mode 100644
index 0000000000..92de169ab0
--- /dev/null
+++ b/devtools/client/application/src/actions/ui.js
@@ -0,0 +1,20 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ UPDATE_SELECTED_PAGE,
+} = require("resource://devtools/client/application/src/constants.js");
+
+function updateSelectedPage(selectedPage) {
+ return {
+ type: UPDATE_SELECTED_PAGE,
+ selectedPage,
+ };
+}
+
+module.exports = {
+ updateSelectedPage,
+};
diff --git a/devtools/client/application/src/actions/workers.js b/devtools/client/application/src/actions/workers.js
new file mode 100644
index 0000000000..375dfe9ba7
--- /dev/null
+++ b/devtools/client/application/src/actions/workers.js
@@ -0,0 +1,51 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ START_WORKER,
+ UNREGISTER_WORKER,
+ UPDATE_CAN_DEBUG_WORKERS,
+ UPDATE_WORKERS,
+} = require("resource://devtools/client/application/src/constants.js");
+
+function startWorker(worker) {
+ const { registrationFront } = worker;
+ registrationFront.start();
+
+ return {
+ type: START_WORKER,
+ };
+}
+
+function unregisterWorker(registration) {
+ const { registrationFront } = registration;
+ registrationFront.unregister();
+
+ return {
+ type: UNREGISTER_WORKER,
+ };
+}
+
+function updateWorkers(workers) {
+ return {
+ type: UPDATE_WORKERS,
+ workers,
+ };
+}
+
+function updateCanDebugWorkers(canDebugWorkers) {
+ return {
+ type: UPDATE_CAN_DEBUG_WORKERS,
+ canDebugWorkers,
+ };
+}
+
+module.exports = {
+ startWorker,
+ unregisterWorker,
+ updateCanDebugWorkers,
+ updateWorkers,
+};
diff --git a/devtools/client/application/src/base.css b/devtools/client/application/src/base.css
new file mode 100644
index 0000000000..4e7dcc8c2e
--- /dev/null
+++ b/devtools/client/application/src/base.css
@@ -0,0 +1,83 @@
+/* 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/. */
+
+:root {
+ /* Typography from Photon */
+ /* See https://firefox-dev.tools/photon/visuals/typography.html */
+ --caption-10-font-size: 11px;
+ --caption-10-font-weight: 400;
+ --body-10-font-size: 13px;
+ --body-10-font-weight: 400;
+ --body-20-font-size: 15px;
+ --body-20-font-weight: 400;
+ --body-20-font-weight-bold: 700;
+ --title-10-font-size: 13px;
+ --title-10-font-weight: 600;
+ --title-20-font-size: 17px;
+ --title-20-font-weight: 400;
+ --title-30-font-size: 22px;
+
+ /* Global styles */
+ --base-line-height: 1.8;
+ --list-line-height: 1.25;
+
+ /* Global colours */
+ --separator-color: var(--theme-splitter-color);
+ --bg-color: var(--theme-toolbar-background);
+ --highlight-color: var(--theme-toolbar-background-hover);
+
+ /* extra, raw colors */
+ --blue-50-a30: rgba(10, 132, 255, 0.3);
+
+ /* Global layout vars */
+ --base-unit: 4px;
+
+ /* these are the color for icons in empty pages - note that these are not
+ available in devtools' variables.css */
+ --dimmed-icon-color: #d3d3d3;
+}
+
+:root.theme-dark {
+ --dimmed-icon-color: #484848;
+}
+
+/*
+* Reset some tags
+*/
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ padding: 0;
+ line-height: var(--base-line-height);
+}
+
+ul {
+ line-height: var(--list-line-height);
+}
+
+a {
+ color: var(--theme-highlight-blue);
+ text-decoration: underline;
+ cursor: pointer;
+}
+
+p {
+ margin: 0;
+}
+
+table {
+ border-spacing: 0;
+}
+
+/*
+ * utility classes
+ */
+
+.technical-text {
+ font-family: monospace;
+}
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",
+)
diff --git a/devtools/client/application/src/constants.js b/devtools/client/application/src/constants.js
new file mode 100644
index 0000000000..f7e34082a3
--- /dev/null
+++ b/devtools/client/application/src/constants.js
@@ -0,0 +1,61 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const actionTypes = {
+ // manifest substate
+ FETCH_MANIFEST_FAILURE: "FETCH_MANIFEST_FAILURE",
+ FETCH_MANIFEST_START: "FETCH_MANIFEST_START",
+ FETCH_MANIFEST_SUCCESS: "FETCH_MANIFEST_SUCCESS",
+ RESET_MANIFEST: "RESET_MANIFEST",
+ // page substate
+ UPDATE_DOMAIN: "UPDATE_DOMAIN",
+ // ui substate
+ UPDATE_SELECTED_PAGE: "UPDATE_SELECTED_PAGE",
+ // workers substate
+ START_WORKER: "START_WORKER",
+ UNREGISTER_WORKER: "UNREGISTER_WORKER",
+ UPDATE_CAN_DEBUG_WORKERS: "UPDATE_CAN_DEBUG_WORKERS",
+ UPDATE_WORKERS: "UPDATE_WORKERS",
+};
+
+// NOTE: these const values are used as part of CSS selectors - be mindful of the characters used
+const PAGE_TYPES = {
+ MANIFEST: "manifest",
+ SERVICE_WORKERS: "service-workers",
+};
+
+const DEFAULT_PAGE = PAGE_TYPES.SERVICE_WORKERS;
+
+const MANIFEST_CATEGORIES = {
+ IDENTITY: "identity",
+ PRESENTATION: "presentation",
+ ICONS: "icons",
+};
+
+const MANIFEST_MEMBER_VALUE_TYPES = {
+ COLOR: "color",
+ ICON: "icon",
+ STRING: "string",
+ URL: "url",
+};
+
+const MANIFEST_ISSUE_LEVELS = {
+ ERROR: "error",
+ WARNING: "warning",
+};
+
+// flatten constants
+module.exports = Object.assign(
+ {},
+ {
+ DEFAULT_PAGE,
+ PAGE_TYPES,
+ MANIFEST_CATEGORIES,
+ MANIFEST_ISSUE_LEVELS,
+ MANIFEST_MEMBER_VALUE_TYPES,
+ },
+ actionTypes
+);
diff --git a/devtools/client/application/src/create-store.js b/devtools/client/application/src/create-store.js
new file mode 100644
index 0000000000..7c89adece3
--- /dev/null
+++ b/devtools/client/application/src/create-store.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 {
+ thunk,
+} = require("resource://devtools/client/shared/redux/middleware/thunk.js");
+const eventTelemetryMiddleware = require("resource://devtools/client/application/src/middleware/event-telemetry.js");
+
+const {
+ applyMiddleware,
+ createStore,
+} = require("resource://devtools/client/shared/vendor/redux.js");
+
+// Reducers
+
+const rootReducer = require("resource://devtools/client/application/src/reducers/index.js");
+const {
+ ManifestState,
+} = require("resource://devtools/client/application/src/reducers/manifest-state.js");
+const {
+ WorkersState,
+} = require("resource://devtools/client/application/src/reducers/workers-state.js");
+const {
+ PageState,
+} = require("resource://devtools/client/application/src/reducers/page-state.js");
+const {
+ UiState,
+} = require("resource://devtools/client/application/src/reducers/ui-state.js");
+
+function configureStore(telemetry) {
+ // Prepare initial state.
+ const initialState = {
+ manifest: new ManifestState(),
+ page: new PageState(),
+ ui: new UiState(),
+ workers: new WorkersState(),
+ };
+
+ const middleware = applyMiddleware(
+ thunk(),
+ eventTelemetryMiddleware(telemetry)
+ );
+
+ return createStore(rootReducer, initialState, middleware);
+}
+
+exports.configureStore = configureStore;
diff --git a/devtools/client/application/src/middleware/event-telemetry.js b/devtools/client/application/src/middleware/event-telemetry.js
new file mode 100644
index 0000000000..60129d2bde
--- /dev/null
+++ b/devtools/client/application/src/middleware/event-telemetry.js
@@ -0,0 +1,37 @@
+/* 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 {
+ START_WORKER,
+ UNREGISTER_WORKER,
+ UPDATE_SELECTED_PAGE,
+} = require("resource://devtools/client/application/src/constants.js");
+
+function eventTelemetryMiddleware(telemetry) {
+ function recordEvent(method, details = {}) {
+ telemetry.recordEvent(method, "application", null, details);
+ }
+
+ return store => next => action => {
+ switch (action.type) {
+ // ui telemetry
+ case UPDATE_SELECTED_PAGE:
+ recordEvent("select_page", { page_type: action.selectedPage });
+ break;
+ // service-worker related telemetry
+ case UNREGISTER_WORKER:
+ recordEvent("unregister_worker");
+ break;
+ case START_WORKER:
+ recordEvent("start_worker");
+ break;
+ }
+
+ return next(action);
+ };
+}
+
+module.exports = eventTelemetryMiddleware;
diff --git a/devtools/client/application/src/middleware/moz.build b/devtools/client/application/src/middleware/moz.build
new file mode 100644
index 0000000000..5041f3ca13
--- /dev/null
+++ b/devtools/client/application/src/middleware/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(
+ "event-telemetry.js",
+)
diff --git a/devtools/client/application/src/modules/application-services.js b/devtools/client/application/src/modules/application-services.js
new file mode 100644
index 0000000000..e51caa585b
--- /dev/null
+++ b/devtools/client/application/src/modules/application-services.js
@@ -0,0 +1,85 @@
+/* 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";
+
+// keyword to use in telemetry, as `reason` parameter
+const REASON = "application";
+
+class ManifestDevToolsError extends Error {
+ constructor(...params) {
+ super(...params);
+
+ this.name = "ManifestDevToolsError";
+ }
+}
+
+class ApplicationServices {
+ init(toolbox) {
+ this._toolbox = toolbox;
+
+ this.features = {
+ doesDebuggerSupportWorkers: Services.prefs.getBoolPref(
+ "devtools.debugger.features.windowless-service-workers",
+ false
+ ),
+ };
+ }
+
+ selectTool(toolId) {
+ this._assertInit();
+ return this._toolbox.selectTool(toolId, REASON);
+ }
+
+ async openWorkerInDebugger(workerDescriptorFront) {
+ const debuggerPanel = await this.selectTool("jsdebugger");
+ debuggerPanel.selectWorker(workerDescriptorFront);
+ }
+
+ async viewWorkerSource(workerDescriptorFront) {
+ // NOTE: this falls back to view-source: if the source can't be inspected
+ // within the debugger.
+ this._toolbox.viewSourceInDebugger(
+ workerDescriptorFront.url,
+ 1,
+ 1,
+ null,
+ REASON
+ );
+ }
+
+ async fetchManifest() {
+ let response;
+
+ try {
+ this._assertInit();
+ const manifestFront = await this._toolbox.target.getFront("manifest");
+ response = await manifestFront.fetchCanonicalManifest();
+ } catch (error) {
+ throw new ManifestDevToolsError(
+ error.message,
+ error.fileName,
+ error.lineNumber
+ );
+ }
+
+ if (response.errorMessage) {
+ throw new Error(response.errorMessage);
+ }
+
+ return response.manifest;
+ }
+
+ _assertInit() {
+ if (!this._toolbox) {
+ throw new Error("Services singleton has not been initialized");
+ }
+ }
+}
+
+module.exports = {
+ ManifestDevToolsError,
+ // exports a singleton, which will be used across all application panel modules
+ services: new ApplicationServices(),
+};
diff --git a/devtools/client/application/src/modules/l10n.js b/devtools/client/application/src/modules/l10n.js
new file mode 100644
index 0000000000..21e300cb38
--- /dev/null
+++ b/devtools/client/application/src/modules/l10n.js
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ FluentL10n,
+} = require("resource://devtools/client/shared/fluent-l10n/fluent-l10n.js");
+
+// exports a singleton, which will be used across all application panel modules
+exports.l10n = new FluentL10n();
diff --git a/devtools/client/application/src/modules/moz.build b/devtools/client/application/src/modules/moz.build
new file mode 100644
index 0000000000..778345fb1f
--- /dev/null
+++ b/devtools/client/application/src/modules/moz.build
@@ -0,0 +1,8 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ "application-services.js",
+ "l10n.js",
+)
diff --git a/devtools/client/application/src/moz.build b/devtools/client/application/src/moz.build
new file mode 100644
index 0000000000..58e6f92857
--- /dev/null
+++ b/devtools/client/application/src/moz.build
@@ -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/.
+
+DIRS += [
+ "actions",
+ "components",
+ "middleware",
+ "modules",
+ "reducers",
+ "types",
+]
+
+DevToolsModules(
+ "constants.js",
+ "create-store.js",
+)
diff --git a/devtools/client/application/src/reducers/index.js b/devtools/client/application/src/reducers/index.js
new file mode 100644
index 0000000000..8b93290d9d
--- /dev/null
+++ b/devtools/client/application/src/reducers/index.js
@@ -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/. */
+
+"use strict";
+
+const {
+ combineReducers,
+} = require("resource://devtools/client/shared/vendor/redux.js");
+const {
+ workersReducer,
+} = require("resource://devtools/client/application/src/reducers/workers-state.js");
+const {
+ pageReducer,
+} = require("resource://devtools/client/application/src/reducers/page-state.js");
+const {
+ uiReducer,
+} = require("resource://devtools/client/application/src/reducers/ui-state.js");
+const {
+ manifestReducer,
+} = require("resource://devtools/client/application/src/reducers/manifest-state.js");
+
+module.exports = combineReducers({
+ manifest: manifestReducer,
+ page: pageReducer,
+ workers: workersReducer,
+ ui: uiReducer,
+});
diff --git a/devtools/client/application/src/reducers/manifest-state.js b/devtools/client/application/src/reducers/manifest-state.js
new file mode 100644
index 0000000000..61a2fa6759
--- /dev/null
+++ b/devtools/client/application/src/reducers/manifest-state.js
@@ -0,0 +1,158 @@
+/* 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 {
+ MANIFEST_CATEGORIES,
+ MANIFEST_ISSUE_LEVELS,
+ MANIFEST_MEMBER_VALUE_TYPES,
+ FETCH_MANIFEST_FAILURE,
+ FETCH_MANIFEST_START,
+ FETCH_MANIFEST_SUCCESS,
+ RESET_MANIFEST,
+} = require("resource://devtools/client/application/src/constants.js");
+
+function _processRawManifestIcons(rawIcons) {
+ // NOTE: about `rawIcons` array we are getting from platform:
+ // - Icons that do not comform to the spec are filtered out
+ // - We will always get a `src`
+ // - We will always get `purpose` with a value (default is `["any"]`)
+ // - `sizes` may be undefined
+ // - `type` may be undefined
+ return rawIcons.map(icon => {
+ return {
+ key: {
+ sizes: Array.isArray(icon.sizes) ? icon.sizes.join(" ") : icon.sizes,
+ contentType: icon.type,
+ },
+ value: {
+ src: icon.src,
+ purpose: icon.purpose.join(" "),
+ },
+ type: MANIFEST_MEMBER_VALUE_TYPES.ICON,
+ };
+ });
+}
+
+function _processRawManifestMembers(rawManifest) {
+ function getCategoryForMember(key) {
+ switch (key) {
+ case "name":
+ case "short_name":
+ return MANIFEST_CATEGORIES.IDENTITY;
+ default:
+ return MANIFEST_CATEGORIES.PRESENTATION;
+ }
+ }
+
+ function getValueTypeForMember(key) {
+ switch (key) {
+ case "start_url":
+ case "scope":
+ return MANIFEST_MEMBER_VALUE_TYPES.URL;
+ case "theme_color":
+ case "background_color":
+ return MANIFEST_MEMBER_VALUE_TYPES.COLOR;
+ default:
+ return MANIFEST_MEMBER_VALUE_TYPES.STRING;
+ }
+ }
+
+ const res = {
+ [MANIFEST_CATEGORIES.IDENTITY]: [],
+ [MANIFEST_CATEGORIES.PRESENTATION]: [],
+ };
+
+ // filter out extra metadata members (those with moz_ prefix) and icons
+ const rawMembers = Object.entries(rawManifest).filter(
+ ([key, value]) => !key.startsWith("moz_") && !(key === "icons")
+ );
+
+ for (const [key, value] of rawMembers) {
+ const category = getCategoryForMember(key);
+ const type = getValueTypeForMember(key);
+ res[category].push({ key, value, type });
+ }
+
+ return res;
+}
+
+function _processRawManifestIssues(issues) {
+ return issues.map(x => {
+ return {
+ level: x.warn
+ ? MANIFEST_ISSUE_LEVELS.WARNING
+ : MANIFEST_ISSUE_LEVELS.ERROR,
+ message: x.warn || x.error,
+ type: x.type || null,
+ };
+ });
+}
+
+function _processRawManifest(rawManifest) {
+ const res = {
+ url: rawManifest.moz_manifest_url,
+ };
+
+ // group manifest members by category
+ Object.assign(res, _processRawManifestMembers(rawManifest));
+ // process icons
+ res.icons = _processRawManifestIcons(rawManifest.icons || []);
+ // process error messages
+ res.validation = _processRawManifestIssues(rawManifest.moz_validation || []);
+
+ return res;
+}
+
+function ManifestState() {
+ return {
+ errorMessage: "",
+ isLoading: false,
+ manifest: undefined,
+ };
+}
+
+function manifestReducer(state = ManifestState(), action) {
+ switch (action.type) {
+ case FETCH_MANIFEST_START:
+ return Object.assign({}, state, {
+ isLoading: true,
+ mustLoadManifest: false,
+ });
+
+ case FETCH_MANIFEST_FAILURE:
+ const { error } = action;
+ // If we add a redux middleware to log errors, we should move the
+ // console.error below there.
+ console.error(error);
+ return Object.assign({}, state, {
+ errorMessage: error,
+ isLoading: false,
+ manifest: null,
+ });
+
+ case FETCH_MANIFEST_SUCCESS:
+ // NOTE: we don't get an error when the page does not have a manifest,
+ // but a `null` value there.
+ const { manifest } = action;
+ return Object.assign({}, state, {
+ errorMessage: "",
+ isLoading: false,
+ manifest: manifest ? _processRawManifest(manifest) : null,
+ });
+
+ case RESET_MANIFEST:
+ const defaultState = ManifestState();
+ return defaultState;
+
+ default:
+ return state;
+ }
+}
+
+module.exports = {
+ ManifestState,
+ manifestReducer,
+};
diff --git a/devtools/client/application/src/reducers/moz.build b/devtools/client/application/src/reducers/moz.build
new file mode 100644
index 0000000000..752b27a685
--- /dev/null
+++ b/devtools/client/application/src/reducers/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(
+ "index.js",
+ "manifest-state.js",
+ "page-state.js",
+ "ui-state.js",
+ "workers-state.js",
+)
diff --git a/devtools/client/application/src/reducers/page-state.js b/devtools/client/application/src/reducers/page-state.js
new file mode 100644
index 0000000000..ef3b6c970d
--- /dev/null
+++ b/devtools/client/application/src/reducers/page-state.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 {
+ UPDATE_DOMAIN,
+} = require("resource://devtools/client/application/src/constants.js");
+
+function PageState() {
+ return {
+ // Domain
+ domain: null,
+ };
+}
+
+function getDomainFromUrl(url) {
+ return new URL(url).hostname;
+}
+
+function pageReducer(state = PageState(), action) {
+ switch (action.type) {
+ case UPDATE_DOMAIN: {
+ const { url } = action;
+ return {
+ domain: getDomainFromUrl(url),
+ };
+ }
+
+ default:
+ return state;
+ }
+}
+
+module.exports = {
+ PageState,
+ pageReducer,
+};
diff --git a/devtools/client/application/src/reducers/ui-state.js b/devtools/client/application/src/reducers/ui-state.js
new file mode 100644
index 0000000000..81a57c8086
--- /dev/null
+++ b/devtools/client/application/src/reducers/ui-state.js
@@ -0,0 +1,30 @@
+/* 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 {
+ DEFAULT_PAGE,
+ UPDATE_SELECTED_PAGE,
+} = require("resource://devtools/client/application/src/constants.js");
+
+function UiState() {
+ return {
+ selectedPage: DEFAULT_PAGE,
+ };
+}
+
+function uiReducer(state = UiState(), action) {
+ switch (action.type) {
+ case UPDATE_SELECTED_PAGE:
+ return Object.assign({}, state, { selectedPage: action.selectedPage });
+ default:
+ return state;
+ }
+}
+
+module.exports = {
+ UiState,
+ uiReducer,
+};
diff --git a/devtools/client/application/src/reducers/workers-state.js b/devtools/client/application/src/reducers/workers-state.js
new file mode 100644
index 0000000000..004c25ddfa
--- /dev/null
+++ b/devtools/client/application/src/reducers/workers-state.js
@@ -0,0 +1,65 @@
+/* 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 {
+ START_WORKER,
+ UNREGISTER_WORKER,
+ UPDATE_CAN_DEBUG_WORKERS,
+ UPDATE_WORKERS,
+} = require("resource://devtools/client/application/src/constants.js");
+
+function WorkersState() {
+ return {
+ // Array of all service worker registrations
+ list: [],
+ canDebugWorkers: false,
+ };
+}
+
+function buildWorkerDataFromFronts({ registration, workers }) {
+ return {
+ id: registration.id,
+ lastUpdateTime: registration.lastUpdateTime,
+ registrationFront: registration,
+ scope: registration.scope,
+ workers: workers.map(worker => ({
+ id: worker.id,
+ url: worker.url,
+ state: worker.state,
+ stateText: worker.stateText,
+ registrationFront: registration,
+ workerDescriptorFront: worker.workerDescriptorFront,
+ })),
+ };
+}
+
+function workersReducer(state = WorkersState(), action) {
+ switch (action.type) {
+ case UPDATE_CAN_DEBUG_WORKERS: {
+ return Object.assign({}, state, {
+ canDebugWorkers: action.canDebugWorkers,
+ });
+ }
+ case UPDATE_WORKERS: {
+ const { workers } = action;
+ return Object.assign({}, state, {
+ list: workers.map(buildWorkerDataFromFronts).flat(),
+ });
+ }
+ // these actions don't change the state, but get picked up by the
+ // telemetry middleware
+ case START_WORKER:
+ case UNREGISTER_WORKER:
+ return state;
+ default:
+ return state;
+ }
+}
+
+module.exports = {
+ WorkersState,
+ workersReducer,
+};
diff --git a/devtools/client/application/src/types/index.js b/devtools/client/application/src/types/index.js
new file mode 100644
index 0000000000..bf15f187a6
--- /dev/null
+++ b/devtools/client/application/src/types/index.js
@@ -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/. */
+
+"use strict";
+
+const manifestTypes = require("resource://devtools/client/application/src/types/manifest.js");
+const routingTypes = require("resource://devtools/client/application/src/types/routing.js");
+const workersTypes = require("resource://devtools/client/application/src/types/service-workers.js");
+
+module.exports = Object.assign(
+ {},
+ {
+ ...manifestTypes,
+ ...routingTypes,
+ ...workersTypes,
+ }
+);
diff --git a/devtools/client/application/src/types/manifest.js b/devtools/client/application/src/types/manifest.js
new file mode 100644
index 0000000000..7f49522a25
--- /dev/null
+++ b/devtools/client/application/src/types/manifest.js
@@ -0,0 +1,89 @@
+/* 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 {
+ MANIFEST_ISSUE_LEVELS,
+} = require("resource://devtools/client/application/src/constants.js");
+const {
+ MANIFEST_MEMBER_VALUE_TYPES,
+} = require("resource://devtools/client/application/src/constants.js");
+
+const manifestIssue = {
+ level: PropTypes.oneOf(Object.values(MANIFEST_ISSUE_LEVELS)).isRequired,
+ message: PropTypes.string.isRequired,
+ // NOTE: we are currently ignoring the 'type' field that platform adds to errors
+};
+
+const manifestIssueArray = PropTypes.arrayOf(PropTypes.shape(manifestIssue));
+
+const manifestItemColor = {
+ label: PropTypes.string.isRequired,
+ value: PropTypes.string,
+};
+
+const manifestItemIcon = {
+ label: PropTypes.shape({
+ contentType: PropTypes.string,
+ sizes: PropTypes.string,
+ }).isRequired,
+ value: PropTypes.shape({
+ src: PropTypes.string.isRequired,
+ purpose: PropTypes.string.isRequired,
+ }).isRequired,
+};
+
+const manifestItemUrl = {
+ label: PropTypes.string.isRequired,
+ value: PropTypes.string,
+};
+
+const manifestMemberColor = {
+ key: manifestItemColor.label,
+ value: manifestItemColor.value,
+ type: PropTypes.oneOf([MANIFEST_MEMBER_VALUE_TYPES.COLOR]),
+};
+
+const manifestMemberIcon = {
+ key: manifestItemIcon.label,
+ value: manifestItemIcon.value,
+ type: PropTypes.oneOf([MANIFEST_MEMBER_VALUE_TYPES.ICON]),
+};
+
+const manifestMemberString = {
+ key: PropTypes.string.isRequired,
+ value: PropTypes.string,
+ type: PropTypes.oneOf([MANIFEST_MEMBER_VALUE_TYPES.STRING]),
+};
+
+const manifest = {
+ // members
+ identity: PropTypes.arrayOf(PropTypes.shape(manifestMemberString)).isRequired,
+ presentation: PropTypes.arrayOf(
+ PropTypes.oneOfType([
+ PropTypes.shape(manifestMemberColor),
+ PropTypes.shape(manifestMemberString),
+ ])
+ ).isRequired,
+ icons: PropTypes.arrayOf(PropTypes.shape(manifestMemberIcon)).isRequired,
+ // validation issues
+ validation: manifestIssueArray.isRequired,
+ // misc
+ url: PropTypes.string.isRequired,
+};
+
+module.exports = {
+ // full manifest
+ manifest,
+ // specific manifest items
+ manifestItemColor,
+ manifestItemIcon,
+ manifestItemUrl,
+ // manifest issues
+ manifestIssue,
+ manifestIssueArray,
+};
diff --git a/devtools/client/application/src/types/moz.build b/devtools/client/application/src/types/moz.build
new file mode 100644
index 0000000000..c8161f448d
--- /dev/null
+++ b/devtools/client/application/src/types/moz.build
@@ -0,0 +1,10 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ "index.js",
+ "manifest.js",
+ "routing.js",
+ "service-workers.js",
+)
diff --git a/devtools/client/application/src/types/routing.js b/devtools/client/application/src/types/routing.js
new file mode 100644
index 0000000000..a1d922ab3d
--- /dev/null
+++ b/devtools/client/application/src/types/routing.js
@@ -0,0 +1,16 @@
+/* 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 {
+ PAGE_TYPES,
+} = require("resource://devtools/client/application/src/constants.js");
+
+const page = PropTypes.oneOf(Object.values(PAGE_TYPES));
+
+module.exports = {
+ page,
+};
diff --git a/devtools/client/application/src/types/service-workers.js b/devtools/client/application/src/types/service-workers.js
new file mode 100644
index 0000000000..bf913fb264
--- /dev/null
+++ b/devtools/client/application/src/types/service-workers.js
@@ -0,0 +1,35 @@
+/* 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 worker = {
+ id: PropTypes.string.isRequired,
+ state: PropTypes.number.isRequired,
+ stateText: PropTypes.string.isRequired,
+ url: PropTypes.string.isRequired,
+ workerDescriptorFront: PropTypes.object,
+ registrationFront: 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,
+};