summaryrefslogtreecommitdiffstats
path: root/devtools/client/inspector/compatibility/components
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/inspector/compatibility/components
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/inspector/compatibility/components')
-rw-r--r--devtools/client/inspector/compatibility/components/BrowserIcon.js82
-rw-r--r--devtools/client/inspector/compatibility/components/CompatibilityApp.js126
-rw-r--r--devtools/client/inspector/compatibility/components/Footer.js85
-rw-r--r--devtools/client/inspector/compatibility/components/IssueItem.js245
-rw-r--r--devtools/client/inspector/compatibility/components/IssueList.js45
-rw-r--r--devtools/client/inspector/compatibility/components/IssuePane.js55
-rw-r--r--devtools/client/inspector/compatibility/components/NodeItem.js59
-rw-r--r--devtools/client/inspector/compatibility/components/NodeList.js45
-rw-r--r--devtools/client/inspector/compatibility/components/NodePane.js55
-rw-r--r--devtools/client/inspector/compatibility/components/Settings.js197
-rw-r--r--devtools/client/inspector/compatibility/components/UnsupportedBrowserItem.js60
-rw-r--r--devtools/client/inspector/compatibility/components/UnsupportedBrowserList.js76
-rw-r--r--devtools/client/inspector/compatibility/components/moz.build20
13 files changed, 1150 insertions, 0 deletions
diff --git a/devtools/client/inspector/compatibility/components/BrowserIcon.js b/devtools/client/inspector/compatibility/components/BrowserIcon.js
new file mode 100644
index 0000000000..f452ba608a
--- /dev/null
+++ b/devtools/client/inspector/compatibility/components/BrowserIcon.js
@@ -0,0 +1,82 @@
+/* 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 dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const Types = require("resource://devtools/client/inspector/compatibility/types.js");
+
+const ICONS = {
+ firefox: {
+ src: "chrome://devtools/skin/images/browsers/firefox.svg",
+ isMobileIconNeeded: false,
+ },
+ firefox_android: {
+ src: "chrome://devtools/skin/images/browsers/firefox.svg",
+ isMobileIconNeeded: true,
+ },
+ chrome: {
+ src: "chrome://devtools/skin/images/browsers/chrome.svg",
+ isMobileIconNeeded: false,
+ },
+ chrome_android: {
+ src: "chrome://devtools/skin/images/browsers/chrome.svg",
+ isMobileIconNeeded: true,
+ },
+ safari: {
+ src: "chrome://devtools/skin/images/browsers/safari.svg",
+ isMobileIconNeeded: false,
+ },
+ safari_ios: {
+ src: "chrome://devtools/skin/images/browsers/safari.svg",
+ isMobileIconNeeded: true,
+ },
+ edge: {
+ src: "chrome://devtools/skin/images/browsers/edge.svg",
+ isMobileIconNeeded: false,
+ },
+ ie: {
+ src: "chrome://devtools/skin/images/browsers/ie.svg",
+ isMobileIconNeeded: false,
+ },
+};
+
+class BrowserIcon extends PureComponent {
+ static get propTypes() {
+ return {
+ id: Types.browser.id,
+ title: PropTypes.string,
+ name: PropTypes.string,
+ };
+ }
+
+ render() {
+ const { id, name, title } = this.props;
+
+ const icon = ICONS[id];
+
+ return dom.span(
+ {
+ className:
+ "compatibility-browser-icon" +
+ (icon.isMobileIconNeeded
+ ? " compatibility-browser-icon--mobile"
+ : ""),
+ },
+ dom.img({
+ className: "compatibility-browser-icon__image",
+ alt: name || title,
+ title,
+ src: icon.src,
+ })
+ );
+ }
+}
+
+module.exports = BrowserIcon;
diff --git a/devtools/client/inspector/compatibility/components/CompatibilityApp.js b/devtools/client/inspector/compatibility/components/CompatibilityApp.js
new file mode 100644
index 0000000000..6091263d42
--- /dev/null
+++ b/devtools/client/inspector/compatibility/components/CompatibilityApp.js
@@ -0,0 +1,126 @@
+/* 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 {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+const {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+
+const Types = require("resource://devtools/client/inspector/compatibility/types.js");
+
+const Accordion = createFactory(
+ require("resource://devtools/client/shared/components/Accordion.js")
+);
+const Footer = createFactory(
+ require("resource://devtools/client/inspector/compatibility/components/Footer.js")
+);
+const IssuePane = createFactory(
+ require("resource://devtools/client/inspector/compatibility/components/IssuePane.js")
+);
+const Settings = createFactory(
+ require("resource://devtools/client/inspector/compatibility/components/Settings.js")
+);
+
+class CompatibilityApp extends PureComponent {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ // getString prop is injected by the withLocalization wrapper
+ getString: PropTypes.func.isRequired,
+ isSettingsVisibile: PropTypes.bool.isRequired,
+ isTopLevelTargetProcessing: PropTypes.bool.isRequired,
+ selectedNodeIssues: PropTypes.arrayOf(PropTypes.shape(Types.issue))
+ .isRequired,
+ topLevelTargetIssues: PropTypes.arrayOf(PropTypes.shape(Types.issue))
+ .isRequired,
+ setSelectedNode: PropTypes.func.isRequired,
+ };
+ }
+
+ render() {
+ const {
+ dispatch,
+ getString,
+ isSettingsVisibile,
+ isTopLevelTargetProcessing,
+ selectedNodeIssues,
+ topLevelTargetIssues,
+ setSelectedNode,
+ } = this.props;
+
+ const selectedNodeIssuePane = IssuePane({
+ issues: selectedNodeIssues,
+ });
+
+ const topLevelTargetIssuePane =
+ topLevelTargetIssues.length || !isTopLevelTargetProcessing
+ ? IssuePane({
+ dispatch,
+ issues: topLevelTargetIssues,
+ setSelectedNode,
+ })
+ : null;
+
+ const throbber = isTopLevelTargetProcessing
+ ? dom.div({
+ className: "compatibility-app__throbber devtools-throbber",
+ })
+ : null;
+
+ return dom.section(
+ {
+ className: "compatibility-app theme-sidebar inspector-tabpanel",
+ },
+ dom.div(
+ {
+ className:
+ "compatibility-app__container" +
+ (isSettingsVisibile ? " compatibility-app__container-hidden" : ""),
+ },
+ Accordion({
+ className: "compatibility-app__main",
+ items: [
+ {
+ id: "compatibility-app--selected-element-pane",
+ header: getString("compatibility-selected-element-header"),
+ component: selectedNodeIssuePane,
+ opened: true,
+ },
+ {
+ id: "compatibility-app--all-elements-pane",
+ header: getString("compatibility-all-elements-header"),
+ component: [topLevelTargetIssuePane, throbber],
+ opened: true,
+ },
+ ],
+ }),
+ Footer({
+ className: "compatibility-app__footer",
+ })
+ ),
+ isSettingsVisibile ? Settings() : null
+ );
+ }
+}
+
+const mapStateToProps = state => {
+ return {
+ isSettingsVisibile: state.compatibility.isSettingsVisibile,
+ isTopLevelTargetProcessing: state.compatibility.isTopLevelTargetProcessing,
+ selectedNodeIssues: state.compatibility.selectedNodeIssues,
+ topLevelTargetIssues: state.compatibility.topLevelTargetIssues,
+ };
+};
+module.exports = FluentReact.withLocalization(
+ connect(mapStateToProps)(CompatibilityApp)
+);
diff --git a/devtools/client/inspector/compatibility/components/Footer.js b/devtools/client/inspector/compatibility/components/Footer.js
new file mode 100644
index 0000000000..c48484b1c4
--- /dev/null
+++ b/devtools/client/inspector/compatibility/components/Footer.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";
+
+const {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+const {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const {
+ updateSettingsVisibility,
+} = require("resource://devtools/client/inspector/compatibility/actions/compatibility.js");
+
+const SETTINGS_ICON = "chrome://devtools/skin/images/settings.svg";
+
+class Footer extends PureComponent {
+ static get propTypes() {
+ return {
+ updateSettingsVisibility: PropTypes.func.isRequired,
+ };
+ }
+
+ _renderButton(icon, labelId, titleId, onClick) {
+ return Localized(
+ {
+ id: titleId,
+ attrs: { title: true },
+ },
+ dom.button(
+ {
+ className: "compatibility-footer__button",
+ title: titleId,
+ onClick,
+ },
+ dom.img({
+ className: "compatibility-footer__icon",
+ src: icon,
+ }),
+ Localized(
+ {
+ id: labelId,
+ },
+ dom.label(
+ {
+ className: "compatibility-footer__label",
+ },
+ labelId
+ )
+ )
+ )
+ );
+ }
+
+ render() {
+ return dom.footer(
+ {
+ className: "compatibility-footer",
+ },
+ this._renderButton(
+ SETTINGS_ICON,
+ "compatibility-settings-button-label",
+ "compatibility-settings-button-title",
+ this.props.updateSettingsVisibility
+ )
+ );
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ updateSettingsVisibility: () => dispatch(updateSettingsVisibility(true)),
+ };
+};
+
+module.exports = connect(null, mapDispatchToProps)(Footer);
diff --git a/devtools/client/inspector/compatibility/components/IssueItem.js b/devtools/client/inspector/compatibility/components/IssueItem.js
new file mode 100644
index 0000000000..c576e58223
--- /dev/null
+++ b/devtools/client/inspector/compatibility/components/IssueItem.js
@@ -0,0 +1,245 @@
+/* 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 dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+loader.lazyRequireGetter(
+ this,
+ "openDocLink",
+ "resource://devtools/client/shared/link.js",
+ true
+);
+
+const UnsupportedBrowserList = createFactory(
+ require("resource://devtools/client/inspector/compatibility/components/UnsupportedBrowserList.js")
+);
+
+const Types = require("resource://devtools/client/inspector/compatibility/types.js");
+
+const NodePane = createFactory(
+ require("resource://devtools/client/inspector/compatibility/components/NodePane.js")
+);
+
+// For test
+loader.lazyRequireGetter(
+ this,
+ "toSnakeCase",
+ "resource://devtools/client/inspector/compatibility/utils/cases.js",
+ true
+);
+
+const MDN_LINK_PARAMS = new URLSearchParams({
+ utm_source: "devtools",
+ utm_medium: "inspector-compatibility",
+ utm_campaign: "default",
+});
+
+class IssueItem extends PureComponent {
+ static get propTypes() {
+ return {
+ ...Types.issue,
+ dispatch: PropTypes.func.isRequired,
+ setSelectedNode: PropTypes.func.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this._onLinkClicked = this._onLinkClicked.bind(this);
+ }
+
+ _onLinkClicked(e) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ const isMacOS = Services.appinfo.OS === "Darwin";
+
+ openDocLink(e.target.href, {
+ relatedToCurrent: true,
+ inBackground: isMacOS ? e.metaKey : e.ctrlKey,
+ });
+ }
+
+ _getTestDataAttributes() {
+ const testDataSet = {};
+
+ if (Services.prefs.getBoolPref("devtools.testing", false)) {
+ for (const [key, value] of Object.entries(this.props)) {
+ if (key === "nodes") {
+ continue;
+ }
+ const datasetKey = `data-qa-${toSnakeCase(key)}`;
+ testDataSet[datasetKey] = JSON.stringify(value);
+ }
+ }
+
+ return testDataSet;
+ }
+
+ _renderAliases() {
+ const { property } = this.props;
+ let { aliases } = this.props;
+
+ if (!aliases) {
+ return null;
+ }
+
+ aliases = aliases.filter(alias => alias !== property);
+
+ if (aliases.length === 0) {
+ return null;
+ }
+
+ return dom.ul(
+ {
+ className: "compatibility-issue-item__aliases",
+ },
+ aliases.map(alias =>
+ dom.li(
+ {
+ key: alias,
+ className: "compatibility-issue-item__alias",
+ },
+ alias
+ )
+ )
+ );
+ }
+
+ _renderCauses() {
+ const { deprecated, experimental, prefixNeeded } = this.props;
+
+ if (!deprecated && !experimental && !prefixNeeded) {
+ return null;
+ }
+
+ let localizationId = "";
+
+ if (deprecated && experimental && prefixNeeded) {
+ localizationId =
+ "compatibility-issue-deprecated-experimental-prefixneeded";
+ } else if (deprecated && experimental) {
+ localizationId = "compatibility-issue-deprecated-experimental";
+ } else if (deprecated && prefixNeeded) {
+ localizationId = "compatibility-issue-deprecated-prefixneeded";
+ } else if (experimental && prefixNeeded) {
+ localizationId = "compatibility-issue-experimental-prefixneeded";
+ } else if (deprecated) {
+ localizationId = "compatibility-issue-deprecated";
+ } else if (experimental) {
+ localizationId = "compatibility-issue-experimental";
+ } else if (prefixNeeded) {
+ localizationId = "compatibility-issue-prefixneeded";
+ }
+
+ return Localized(
+ {
+ id: localizationId,
+ },
+ dom.span(
+ { className: "compatibility-issue-item__causes" },
+ localizationId
+ )
+ );
+ }
+
+ _renderPropertyEl() {
+ const { property, url, specUrl } = this.props;
+ const baseCls = "compatibility-issue-item__property devtools-monospace";
+ if (!url && !specUrl) {
+ return dom.span({ className: baseCls }, property);
+ }
+
+ const href = url ? `${url}?${MDN_LINK_PARAMS}` : specUrl;
+
+ return dom.a(
+ {
+ className: `${baseCls} ${
+ url
+ ? "compatibility-issue-item__mdn-link"
+ : "compatibility-issue-item__spec-link"
+ }`,
+ href,
+ title: href,
+ onClick: e => this._onLinkClicked(e),
+ },
+ property
+ );
+ }
+
+ _renderDescription() {
+ return dom.div(
+ {
+ className: "compatibility-issue-item__description",
+ },
+ this._renderPropertyEl(),
+ this._renderCauses(),
+ this._renderUnsupportedBrowserList()
+ );
+ }
+
+ _renderNodeList() {
+ const { dispatch, nodes, setSelectedNode } = this.props;
+
+ if (!nodes) {
+ return null;
+ }
+
+ return NodePane({
+ dispatch,
+ nodes,
+ setSelectedNode,
+ });
+ }
+
+ _renderUnsupportedBrowserList() {
+ const { unsupportedBrowsers } = this.props;
+
+ return unsupportedBrowsers.length
+ ? UnsupportedBrowserList({ browsers: unsupportedBrowsers })
+ : null;
+ }
+
+ render() {
+ const { deprecated, experimental, property, unsupportedBrowsers } =
+ this.props;
+
+ const classes = ["compatibility-issue-item"];
+
+ if (deprecated) {
+ classes.push("compatibility-issue-item--deprecated");
+ }
+
+ if (experimental) {
+ classes.push("compatibility-issue-item--experimental");
+ }
+
+ if (unsupportedBrowsers.length) {
+ classes.push("compatibility-issue-item--unsupported");
+ }
+
+ return dom.li(
+ {
+ className: classes.join(" "),
+ key: property,
+ ...this._getTestDataAttributes(),
+ },
+ this._renderDescription(),
+ this._renderAliases(),
+ this._renderNodeList()
+ );
+ }
+}
+
+module.exports = IssueItem;
diff --git a/devtools/client/inspector/compatibility/components/IssueList.js b/devtools/client/inspector/compatibility/components/IssueList.js
new file mode 100644
index 0000000000..f334276cb3
--- /dev/null
+++ b/devtools/client/inspector/compatibility/components/IssueList.js
@@ -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/. */
+
+"use strict";
+
+const {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const Types = require("resource://devtools/client/inspector/compatibility/types.js");
+
+const IssueItem = createFactory(
+ require("resource://devtools/client/inspector/compatibility/components/IssueItem.js")
+);
+
+class IssueList extends PureComponent {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ issues: PropTypes.arrayOf(PropTypes.shape(Types.issue)).isRequired,
+ setSelectedNode: PropTypes.func.isRequired,
+ };
+ }
+
+ render() {
+ const { dispatch, issues, setSelectedNode } = this.props;
+
+ return dom.ul(
+ { className: "compatibility-issue-list" },
+ issues.map(issue =>
+ IssueItem({
+ ...issue,
+ dispatch,
+ setSelectedNode,
+ })
+ )
+ );
+ }
+}
+
+module.exports = IssueList;
diff --git a/devtools/client/inspector/compatibility/components/IssuePane.js b/devtools/client/inspector/compatibility/components/IssuePane.js
new file mode 100644
index 0000000000..b313274d9a
--- /dev/null
+++ b/devtools/client/inspector/compatibility/components/IssuePane.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("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const Types = require("resource://devtools/client/inspector/compatibility/types.js");
+
+const IssueList = createFactory(
+ require("resource://devtools/client/inspector/compatibility/components/IssueList.js")
+);
+
+class IssuePane extends PureComponent {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ issues: PropTypes.arrayOf(PropTypes.shape(Types.issue)).isRequired,
+ setSelectedNode: PropTypes.func.isRequired,
+ };
+ }
+
+ _renderNoIssues() {
+ return Localized(
+ { id: "compatibility-no-issues-found" },
+ dom.p(
+ { className: "devtools-sidepanel-no-result" },
+ "compatibility-no-issues-found"
+ )
+ );
+ }
+
+ render() {
+ const { dispatch, issues, setSelectedNode } = this.props;
+
+ return issues.length
+ ? IssueList({
+ dispatch,
+ issues,
+ setSelectedNode,
+ })
+ : this._renderNoIssues();
+ }
+}
+
+module.exports = IssuePane;
diff --git a/devtools/client/inspector/compatibility/components/NodeItem.js b/devtools/client/inspector/compatibility/components/NodeItem.js
new file mode 100644
index 0000000000..078b04a3b4
--- /dev/null
+++ b/devtools/client/inspector/compatibility/components/NodeItem.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 {
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const {
+ translateNodeFrontToGrip,
+} = require("resource://devtools/client/inspector/shared/utils.js");
+const {
+ REPS,
+ MODE,
+} = require("resource://devtools/client/shared/components/reps/index.js");
+const { Rep } = REPS;
+const ElementNode = REPS.ElementNode;
+
+const Types = require("resource://devtools/client/inspector/compatibility/types.js");
+
+const {
+ highlightNode,
+ unhighlightNode,
+} = require("resource://devtools/client/inspector/boxmodel/actions/box-model-highlighter.js");
+
+class NodeItem extends PureComponent {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ node: Types.node.isRequired,
+ setSelectedNode: PropTypes.func.isRequired,
+ };
+ }
+
+ render() {
+ const { dispatch, node, setSelectedNode } = this.props;
+
+ return dom.li(
+ { className: "compatibility-node-item" },
+ Rep({
+ defaultRep: ElementNode,
+ mode: MODE.TINY,
+ object: translateNodeFrontToGrip(node),
+ onDOMNodeClick: () => {
+ setSelectedNode(node);
+ dispatch(unhighlightNode());
+ },
+ onDOMNodeMouseOut: () => dispatch(unhighlightNode()),
+ onDOMNodeMouseOver: () => dispatch(highlightNode(node)),
+ })
+ );
+ }
+}
+
+module.exports = NodeItem;
diff --git a/devtools/client/inspector/compatibility/components/NodeList.js b/devtools/client/inspector/compatibility/components/NodeList.js
new file mode 100644
index 0000000000..7dc93c8b06
--- /dev/null
+++ b/devtools/client/inspector/compatibility/components/NodeList.js
@@ -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/. */
+
+"use strict";
+
+const {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const Types = require("resource://devtools/client/inspector/compatibility/types.js");
+
+const NodeItem = createFactory(
+ require("resource://devtools/client/inspector/compatibility/components/NodeItem.js")
+);
+
+class NodeList extends PureComponent {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ nodes: PropTypes.arrayOf(Types.node).isRequired,
+ setSelectedNode: PropTypes.func.isRequired,
+ };
+ }
+
+ render() {
+ const { dispatch, nodes, setSelectedNode } = this.props;
+
+ return dom.ul(
+ { className: "compatibility-node-list" },
+ nodes.map(node =>
+ NodeItem({
+ dispatch,
+ node,
+ setSelectedNode,
+ })
+ )
+ );
+ }
+}
+
+module.exports = NodeList;
diff --git a/devtools/client/inspector/compatibility/components/NodePane.js b/devtools/client/inspector/compatibility/components/NodePane.js
new file mode 100644
index 0000000000..06c844d012
--- /dev/null
+++ b/devtools/client/inspector/compatibility/components/NodePane.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("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const Types = require("resource://devtools/client/inspector/compatibility/types.js");
+
+const NodeList = createFactory(
+ require("resource://devtools/client/inspector/compatibility/components/NodeList.js")
+);
+
+class NodePane extends PureComponent {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ nodes: PropTypes.arrayOf(Types.node).isRequired,
+ setSelectedNode: PropTypes.func.isRequired,
+ };
+ }
+
+ render() {
+ const { nodes } = this.props;
+
+ return dom.details(
+ {
+ className: "compatibility-node-pane",
+ open: nodes.length <= 1,
+ },
+ Localized(
+ {
+ id: "compatibility-issue-occurrences",
+ $number: nodes.length,
+ },
+ dom.summary(
+ { className: "compatibility-node-pane__summary" },
+ "compatibility-issue-occurrences"
+ )
+ ),
+ NodeList(this.props)
+ );
+ }
+}
+
+module.exports = NodePane;
diff --git a/devtools/client/inspector/compatibility/components/Settings.js b/devtools/client/inspector/compatibility/components/Settings.js
new file mode 100644
index 0000000000..6f55353aa6
--- /dev/null
+++ b/devtools/client/inspector/compatibility/components/Settings.js
@@ -0,0 +1,197 @@
+/* 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 {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+const {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const Types = require("resource://devtools/client/inspector/compatibility/types.js");
+
+const BrowserIcon = createFactory(
+ require("resource://devtools/client/inspector/compatibility/components/BrowserIcon.js")
+);
+
+const {
+ updateSettingsVisibility,
+ updateTargetBrowsers,
+} = require("resource://devtools/client/inspector/compatibility/actions/compatibility.js");
+
+const CLOSE_ICON = "chrome://devtools/skin/images/close.svg";
+
+class Settings extends PureComponent {
+ static get propTypes() {
+ return {
+ defaultTargetBrowsers: PropTypes.arrayOf(PropTypes.shape(Types.browser))
+ .isRequired,
+ targetBrowsers: PropTypes.arrayOf(PropTypes.shape(Types.browser))
+ .isRequired,
+ updateTargetBrowsers: PropTypes.func.isRequired,
+ updateSettingsVisibility: PropTypes.func.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this._onTargetBrowserChanged = this._onTargetBrowserChanged.bind(this);
+
+ this.state = {
+ targetBrowsers: props.targetBrowsers,
+ };
+ }
+
+ _onTargetBrowserChanged({ target }) {
+ const { id, status } = target.dataset;
+ let { targetBrowsers } = this.state;
+
+ if (target.checked) {
+ targetBrowsers = [...targetBrowsers, { id, status }];
+ } else {
+ targetBrowsers = targetBrowsers.filter(
+ b => !(b.id === id && b.status === status)
+ );
+ }
+
+ this.setState({ targetBrowsers });
+ }
+
+ _renderTargetBrowsers() {
+ const { defaultTargetBrowsers } = this.props;
+ const { targetBrowsers } = this.state;
+
+ return dom.section(
+ {
+ className: "compatibility-settings__target-browsers",
+ },
+ Localized(
+ { id: "compatibility-target-browsers-header" },
+ dom.header(
+ {
+ className: "compatibility-settings__target-browsers-header",
+ },
+ "compatibility-target-browsers-header"
+ )
+ ),
+ dom.ul(
+ {
+ className: "compatibility-settings__target-browsers-list",
+ },
+ defaultTargetBrowsers.map(({ id, name, status, version }) => {
+ const inputId = `${id}-${status}`;
+ const isTargetBrowser = !!targetBrowsers.find(
+ b => b.id === id && b.status === status
+ );
+ return dom.li(
+ {
+ className: "compatibility-settings__target-browsers-item",
+ },
+ dom.input({
+ id: inputId,
+ type: "checkbox",
+ checked: isTargetBrowser,
+ onChange: this._onTargetBrowserChanged,
+ "data-id": id,
+ "data-status": status,
+ }),
+ dom.label(
+ {
+ className: "compatibility-settings__target-browsers-item-label",
+ htmlFor: inputId,
+ },
+ BrowserIcon({ id, title: `${name} ${status}` }),
+ `${name} ${status} (${version})`
+ )
+ );
+ })
+ )
+ );
+ }
+
+ _renderHeader() {
+ return dom.header(
+ {
+ className: "compatibility-settings__header",
+ },
+ Localized(
+ { id: "compatibility-settings-header" },
+ dom.label(
+ {
+ className: "compatibility-settings__header-label",
+ },
+ "compatibility-settings-header"
+ )
+ ),
+ Localized(
+ {
+ id: "compatibility-close-settings-button",
+ attrs: { title: true },
+ },
+ dom.button(
+ {
+ className: "compatibility-settings__header-button",
+ title: "compatibility-close-settings-button",
+ onClick: () => {
+ const { defaultTargetBrowsers } = this.props;
+ const { targetBrowsers } = this.state;
+
+ // Sort by ordering of default browsers.
+ const browsers = defaultTargetBrowsers.filter(b =>
+ targetBrowsers.find(t => t.id === b.id && t.status === b.status)
+ );
+
+ if (
+ this.props.targetBrowsers.toString() !== browsers.toString()
+ ) {
+ this.props.updateTargetBrowsers(browsers);
+ }
+
+ this.props.updateSettingsVisibility();
+ },
+ },
+ dom.img({
+ className: "compatibility-settings__header-icon",
+ src: CLOSE_ICON,
+ })
+ )
+ )
+ );
+ }
+
+ render() {
+ return dom.section(
+ {
+ className: "compatibility-settings",
+ },
+ this._renderHeader(),
+ this._renderTargetBrowsers()
+ );
+ }
+}
+
+const mapStateToProps = state => {
+ return {
+ defaultTargetBrowsers: state.compatibility.defaultTargetBrowsers,
+ targetBrowsers: state.compatibility.targetBrowsers,
+ };
+};
+
+const mapDispatchToProps = dispatch => {
+ return {
+ updateTargetBrowsers: browsers => dispatch(updateTargetBrowsers(browsers)),
+ updateSettingsVisibility: () => dispatch(updateSettingsVisibility(false)),
+ };
+};
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(Settings);
diff --git a/devtools/client/inspector/compatibility/components/UnsupportedBrowserItem.js b/devtools/client/inspector/compatibility/components/UnsupportedBrowserItem.js
new file mode 100644
index 0000000000..ae9806d206
--- /dev/null
+++ b/devtools/client/inspector/compatibility/components/UnsupportedBrowserItem.js
@@ -0,0 +1,60 @@
+/* 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 dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const BrowserIcon = createFactory(
+ require("resource://devtools/client/inspector/compatibility/components/BrowserIcon.js")
+);
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const Types = require("resource://devtools/client/inspector/compatibility/types.js");
+
+class UnsupportedBrowserItem extends PureComponent {
+ static get propTypes() {
+ return {
+ id: Types.browser.id,
+ name: Types.browser.name,
+ unsupportedVersions: PropTypes.array.isRequired,
+ version: Types.browser.version,
+ };
+ }
+
+ render() {
+ const { unsupportedVersions, id, name, version } = this.props;
+
+ return Localized(
+ {
+ id: "compatibility-issue-browsers-list",
+ $browsers: unsupportedVersions
+ .map(
+ ({ version: v, status }) =>
+ `${name} ${v}${status ? ` (${status})` : ""}`
+ )
+ .join("\n"),
+ attrs: { title: true },
+ },
+ dom.li(
+ { className: "compatibility-browser", "data-browser-id": id },
+ BrowserIcon({ id, name }),
+ dom.span(
+ {
+ className: "compatibility-browser-version",
+ },
+ version
+ )
+ )
+ );
+ }
+}
+
+module.exports = UnsupportedBrowserItem;
diff --git a/devtools/client/inspector/compatibility/components/UnsupportedBrowserList.js b/devtools/client/inspector/compatibility/components/UnsupportedBrowserList.js
new file mode 100644
index 0000000000..51b513253f
--- /dev/null
+++ b/devtools/client/inspector/compatibility/components/UnsupportedBrowserList.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 {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+
+const UnsupportedBrowserItem = createFactory(
+ require("resource://devtools/client/inspector/compatibility/components/UnsupportedBrowserItem.js")
+);
+
+const Types = require("resource://devtools/client/inspector/compatibility/types.js");
+
+class UnsupportedBrowserList extends PureComponent {
+ static get propTypes() {
+ return {
+ browsers: PropTypes.arrayOf(PropTypes.shape(Types.browser)).isRequired,
+ };
+ }
+
+ render() {
+ const { browsers } = this.props;
+
+ const unsupportedBrowserItems = {};
+
+ const unsupportedVersionsListByBrowser = new Map();
+
+ for (const { name, version, status } of browsers) {
+ if (!unsupportedVersionsListByBrowser.has(name)) {
+ unsupportedVersionsListByBrowser.set(name, []);
+ }
+ unsupportedVersionsListByBrowser.get(name).push({ version, status });
+ }
+
+ for (const { id, name, version, status } of browsers) {
+ // Only display one icon per browser
+ if (!unsupportedBrowserItems[id]) {
+ if (status === "esr") {
+ // The data is ordered by version number, so we'll show the first unsupported
+ // browser version. This might be confusing for Firefox as we'll show ESR
+ // version first, and so the user wouldn't be able to tell if there's an issue
+ // only on ESR, or also on release.
+ // So only show ESR if there's no newer unsupported version
+ const newerVersionIsUnsupported = browsers.find(
+ browser => browser.id == id && browser.status !== status
+ );
+ if (newerVersionIsUnsupported) {
+ continue;
+ }
+ }
+
+ unsupportedBrowserItems[id] = UnsupportedBrowserItem({
+ key: id,
+ id,
+ name,
+ version,
+ unsupportedVersions: unsupportedVersionsListByBrowser.get(name),
+ });
+ }
+ }
+ return dom.ul(
+ {
+ className: "compatibility-unsupported-browser-list",
+ },
+ Object.values(unsupportedBrowserItems)
+ );
+ }
+}
+
+module.exports = UnsupportedBrowserList;
diff --git a/devtools/client/inspector/compatibility/components/moz.build b/devtools/client/inspector/compatibility/components/moz.build
new file mode 100644
index 0000000000..b4a1c7cf7c
--- /dev/null
+++ b/devtools/client/inspector/compatibility/components/moz.build
@@ -0,0 +1,20 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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(
+ "BrowserIcon.js",
+ "CompatibilityApp.js",
+ "Footer.js",
+ "IssueItem.js",
+ "IssueList.js",
+ "IssuePane.js",
+ "NodeItem.js",
+ "NodeList.js",
+ "NodePane.js",
+ "Settings.js",
+ "UnsupportedBrowserItem.js",
+ "UnsupportedBrowserList.js",
+)