diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /devtools/client/application/src | |
parent | Initial commit. (diff) | |
download | firefox-esr-upstream.tar.xz firefox-esr-upstream.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/application/src')
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", // + 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, +}; |