summaryrefslogtreecommitdiffstats
path: root/devtools/client/application/src/components/manifest
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /devtools/client/application/src/components/manifest
parentInitial commit. (diff)
downloadthunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz
thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/application/src/components/manifest')
-rw-r--r--devtools/client/application/src/components/manifest/Manifest.js136
-rw-r--r--devtools/client/application/src/components/manifest/ManifestColorItem.css29
-rw-r--r--devtools/client/application/src/components/manifest/ManifestColorItem.js57
-rw-r--r--devtools/client/application/src/components/manifest/ManifestEmpty.js81
-rw-r--r--devtools/client/application/src/components/manifest/ManifestIconItem.css7
-rw-r--r--devtools/client/application/src/components/manifest/ManifestIconItem.js98
-rw-r--r--devtools/client/application/src/components/manifest/ManifestIssue.css17
-rw-r--r--devtools/client/application/src/components/manifest/ManifestIssue.js72
-rw-r--r--devtools/client/application/src/components/manifest/ManifestIssueList.css15
-rw-r--r--devtools/client/application/src/components/manifest/ManifestIssueList.js68
-rw-r--r--devtools/client/application/src/components/manifest/ManifestItem.css28
-rw-r--r--devtools/client/application/src/components/manifest/ManifestItem.js50
-rw-r--r--devtools/client/application/src/components/manifest/ManifestJsonLink.css9
-rw-r--r--devtools/client/application/src/components/manifest/ManifestJsonLink.js67
-rw-r--r--devtools/client/application/src/components/manifest/ManifestLoader.css14
-rw-r--r--devtools/client/application/src/components/manifest/ManifestLoader.js108
-rw-r--r--devtools/client/application/src/components/manifest/ManifestPage.js76
-rw-r--r--devtools/client/application/src/components/manifest/ManifestSection.css25
-rw-r--r--devtools/client/application/src/components/manifest/ManifestSection.js44
-rw-r--r--devtools/client/application/src/components/manifest/ManifestUrlItem.css8
-rw-r--r--devtools/client/application/src/components/manifest/ManifestUrlItem.js39
-rw-r--r--devtools/client/application/src/components/manifest/moz.build18
22 files changed, 1066 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..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",
+)