diff options
Diffstat (limited to 'devtools/client/application/src/components/manifest')
22 files changed, 1045 insertions, 0 deletions
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..2c69d9870a --- /dev/null +++ b/devtools/client/application/src/components/manifest/Manifest.js @@ -0,0 +1,134 @@ +/* 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("devtools/client/shared/vendor/react"); +const { + article, + h1, + table, + tbody, +} = require("devtools/client/shared/vendor/react-dom-factories"); + +const FluentReact = require("devtools/client/shared/vendor/fluent-react"); +const Localized = createFactory(FluentReact.Localized); +const { l10n } = require("devtools/client/application/src/modules/l10n"); + +const ManifestColorItem = createFactory( + require("devtools/client/application/src/components/manifest/ManifestColorItem") +); +const ManifestIconItem = createFactory( + require("devtools/client/application/src/components/manifest/ManifestIconItem") +); +const ManifestUrlItem = createFactory( + require("devtools/client/application/src/components/manifest/ManifestUrlItem") +); +const ManifestItem = createFactory( + require("devtools/client/application/src/components/manifest/ManifestItem") +); +const ManifestIssueList = createFactory( + require("devtools/client/application/src/components/manifest/ManifestIssueList") +); +const ManifestSection = createFactory( + require("devtools/client/application/src/components/manifest/ManifestSection") +); +const ManifestJsonLink = createFactory( + require("devtools/client/application/src/components/manifest/ManifestJsonLink") +); + +const { + MANIFEST_MEMBER_VALUE_TYPES, +} = require("devtools/client/application/src/constants"); +const Types = require("devtools/client/application/src/types/index"); + +/** + * 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 > 0; + + 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..b28d5322be --- /dev/null +++ b/devtools/client/application/src/components/manifest/ManifestColorItem.js @@ -0,0 +1,55 @@ +/* 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("devtools/client/shared/vendor/react"); +const { div } = require("devtools/client/shared/vendor/react-dom-factories"); + +const Types = require("devtools/client/application/src/types/index"); +const ManifestItem = createFactory( + require("devtools/client/application/src/components/manifest/ManifestItem") +); + +/** + * 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..dfb2bd8363 --- /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("devtools/client/shared/link"); + +const { + createFactory, + PureComponent, +} = require("devtools/client/shared/vendor/react"); +const { + a, + article, + aside, + div, + h1, + img, + p, +} = require("devtools/client/shared/vendor/react-dom-factories"); + +const FluentReact = require("devtools/client/shared/vendor/fluent-react"); +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..899b5ce9a3 --- /dev/null +++ b/devtools/client/application/src/components/manifest/ManifestIconItem.js @@ -0,0 +1,96 @@ +/* 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("devtools/client/shared/vendor/react"); +const { + br, + code, + img, + span, +} = require("devtools/client/shared/vendor/react-dom-factories"); + +const FluentReact = require("devtools/client/shared/vendor/fluent-react"); +const Localized = createFactory(FluentReact.Localized); +const { l10n } = require("devtools/client/application/src/modules/l10n"); + +const Types = require("devtools/client/application/src/types/index"); +const ManifestItem = createFactory( + require("devtools/client/application/src/components/manifest/ManifestItem") +); + +/** + * 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 > 0 + ? 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..2bbaad68f7 --- /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("devtools/client/shared/vendor/react-prop-types"); +const { + createFactory, + PureComponent, +} = require("devtools/client/shared/vendor/react"); +const { + img, + li, + span, +} = require("devtools/client/shared/vendor/react-dom-factories"); + +const FluentReact = require("devtools/client/shared/vendor/fluent-react"); +const Localized = createFactory(FluentReact.Localized); + +const { + MANIFEST_ISSUE_LEVELS, +} = require("devtools/client/application/src/constants"); +const Types = require("devtools/client/application/src/types/index"); + +/** + * 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..54e8f297c7 --- /dev/null +++ b/devtools/client/application/src/components/manifest/ManifestIssueList.js @@ -0,0 +1,66 @@ +/* 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("devtools/client/shared/vendor/react"); +const { ul } = require("devtools/client/shared/vendor/react-dom-factories"); + +const { + MANIFEST_ISSUE_LEVELS, +} = require("devtools/client/application/src/constants"); +const Types = require("devtools/client/application/src/types/index"); + +const ManifestIssue = createFactory( + require("devtools/client/application/src/components/manifest/ManifestIssue") +); + +/** + * 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 > 0); + + 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..e5c5d534b5 --- /dev/null +++ b/devtools/client/application/src/components/manifest/ManifestItem.js @@ -0,0 +1,48 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const PropTypes = require("devtools/client/shared/vendor/react-prop-types"); +const { PureComponent } = require("devtools/client/shared/vendor/react"); +const { + tr, + td, + th, +} = require("devtools/client/shared/vendor/react-dom-factories"); + +/** + * 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..0481be3703 --- /dev/null +++ b/devtools/client/application/src/components/manifest/ManifestJsonLink.js @@ -0,0 +1,64 @@ +/* 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("devtools/client/shared/link"); + +const PropTypes = require("devtools/client/shared/vendor/react-prop-types"); +const { + createFactory, + PureComponent, +} = require("devtools/client/shared/vendor/react"); + +const { a, p } = require("devtools/client/shared/vendor/react-dom-factories"); + +const FluentReact = require("devtools/client/shared/vendor/fluent-react"); +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..157e20916b --- /dev/null +++ b/devtools/client/application/src/components/manifest/ManifestLoader.js @@ -0,0 +1,106 @@ +/* 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("devtools/client/shared/vendor/react"); +const PropTypes = require("devtools/client/shared/vendor/react-prop-types"); + +const { + aside, + h1, + p, +} = require("devtools/client/shared/vendor/react-dom-factories"); + +const FluentReact = require("devtools/client/shared/vendor/fluent-react"); +const Localized = createFactory(FluentReact.Localized); + +const { connect } = require("devtools/client/shared/vendor/react-redux"); + +const { + fetchManifest, +} = require("devtools/client/application/src/actions/manifest"); + +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..7bce9c3e65 --- /dev/null +++ b/devtools/client/application/src/components/manifest/ManifestPage.js @@ -0,0 +1,74 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const PropTypes = require("devtools/client/shared/vendor/react-prop-types"); +const { + createFactory, + PureComponent, +} = require("devtools/client/shared/vendor/react"); +const { + section, +} = require("devtools/client/shared/vendor/react-dom-factories"); + +const { connect } = require("devtools/client/shared/vendor/react-redux"); + +const Types = require("devtools/client/application/src/types/index"); + +const ManifestLoader = createFactory( + require("devtools/client/application/src/components/manifest/ManifestLoader") +); +const Manifest = createFactory( + require("devtools/client/application/src/components/manifest/Manifest") +); +const ManifestEmpty = createFactory( + require("devtools/client/application/src/components/manifest/ManifestEmpty") +); + +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..92038d4aa8 --- /dev/null +++ b/devtools/client/application/src/components/manifest/ManifestSection.js @@ -0,0 +1,42 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const PropTypes = require("devtools/client/shared/vendor/react-prop-types"); +const { PureComponent } = require("devtools/client/shared/vendor/react"); +const { + h2, + section, +} = require("devtools/client/shared/vendor/react-dom-factories"); + +/** + * 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..f5ada9ac92 --- /dev/null +++ b/devtools/client/application/src/components/manifest/ManifestUrlItem.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 { + createFactory, + PureComponent, +} = require("devtools/client/shared/vendor/react"); +const { div } = require("devtools/client/shared/vendor/react-dom-factories"); + +const Types = require("devtools/client/application/src/types/index"); +const ManifestItem = createFactory( + require("devtools/client/application/src/components/manifest/ManifestItem") +); + +/** + * 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", +) |