summaryrefslogtreecommitdiffstats
path: root/devtools/client/aboutdebugging/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/aboutdebugging/src/components')
-rw-r--r--devtools/client/aboutdebugging/src/components/App.css71
-rw-r--r--devtools/client/aboutdebugging/src/components/App.js213
-rw-r--r--devtools/client/aboutdebugging/src/components/CompatibilityWarning.js110
-rw-r--r--devtools/client/aboutdebugging/src/components/ConnectionPromptSetting.js55
-rw-r--r--devtools/client/aboutdebugging/src/components/ProfilerDialog.css63
-rw-r--r--devtools/client/aboutdebugging/src/components/ProfilerDialog.js168
-rw-r--r--devtools/client/aboutdebugging/src/components/RuntimeActions.css9
-rw-r--r--devtools/client/aboutdebugging/src/components/RuntimeActions.js82
-rw-r--r--devtools/client/aboutdebugging/src/components/RuntimeInfo.css42
-rw-r--r--devtools/client/aboutdebugging/src/components/RuntimeInfo.js89
-rw-r--r--devtools/client/aboutdebugging/src/components/RuntimePage.js306
-rw-r--r--devtools/client/aboutdebugging/src/components/ServiceWorkersWarning.js52
-rw-r--r--devtools/client/aboutdebugging/src/components/connect/ConnectPage.css50
-rw-r--r--devtools/client/aboutdebugging/src/components/connect/ConnectPage.js315
-rw-r--r--devtools/client/aboutdebugging/src/components/connect/ConnectSection.css50
-rw-r--r--devtools/client/aboutdebugging/src/components/connect/ConnectSection.js69
-rw-r--r--devtools/client/aboutdebugging/src/components/connect/ConnectSteps.css13
-rw-r--r--devtools/client/aboutdebugging/src/components/connect/ConnectSteps.js51
-rw-r--r--devtools/client/aboutdebugging/src/components/connect/NetworkLocationsForm.css23
-rw-r--r--devtools/client/aboutdebugging/src/components/connect/NetworkLocationsForm.js148
-rw-r--r--devtools/client/aboutdebugging/src/components/connect/NetworkLocationsList.css20
-rw-r--r--devtools/client/aboutdebugging/src/components/connect/NetworkLocationsList.js67
-rw-r--r--devtools/client/aboutdebugging/src/components/connect/moz.build11
-rw-r--r--devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetItem.css97
-rw-r--r--devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetItem.js91
-rw-r--r--devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetList.css7
-rw-r--r--devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetList.js80
-rw-r--r--devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetPane.css43
-rw-r--r--devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetPane.js147
-rw-r--r--devtools/client/aboutdebugging/src/components/debugtarget/ExtensionDetail.css27
-rw-r--r--devtools/client/aboutdebugging/src/components/debugtarget/ExtensionDetail.js243
-rw-r--r--devtools/client/aboutdebugging/src/components/debugtarget/FieldPair.css29
-rw-r--r--devtools/client/aboutdebugging/src/components/debugtarget/FieldPair.js49
-rw-r--r--devtools/client/aboutdebugging/src/components/debugtarget/InspectAction.js58
-rw-r--r--devtools/client/aboutdebugging/src/components/debugtarget/ProcessDetail.js32
-rw-r--r--devtools/client/aboutdebugging/src/components/debugtarget/ServiceWorkerAction.css26
-rw-r--r--devtools/client/aboutdebugging/src/components/debugtarget/ServiceWorkerAction.js124
-rw-r--r--devtools/client/aboutdebugging/src/components/debugtarget/ServiceWorkerAdditionalActions.js176
-rw-r--r--devtools/client/aboutdebugging/src/components/debugtarget/TabAction.js52
-rw-r--r--devtools/client/aboutdebugging/src/components/debugtarget/TabDetail.js34
-rw-r--r--devtools/client/aboutdebugging/src/components/debugtarget/TemporaryExtensionAdditionalActions.js182
-rw-r--r--devtools/client/aboutdebugging/src/components/debugtarget/TemporaryExtensionDetail.js67
-rw-r--r--devtools/client/aboutdebugging/src/components/debugtarget/TemporaryExtensionInstallSection.css8
-rw-r--r--devtools/client/aboutdebugging/src/components/debugtarget/TemporaryExtensionInstallSection.js101
-rw-r--r--devtools/client/aboutdebugging/src/components/debugtarget/TemporaryExtensionInstaller.js52
-rw-r--r--devtools/client/aboutdebugging/src/components/debugtarget/WorkerDetail.js120
-rw-r--r--devtools/client/aboutdebugging/src/components/debugtarget/moz.build22
-rw-r--r--devtools/client/aboutdebugging/src/components/moz.build21
-rw-r--r--devtools/client/aboutdebugging/src/components/shared/DetailsLog.js64
-rw-r--r--devtools/client/aboutdebugging/src/components/shared/IconLabel.css27
-rw-r--r--devtools/client/aboutdebugging/src/components/shared/IconLabel.js48
-rw-r--r--devtools/client/aboutdebugging/src/components/shared/Message.css79
-rw-r--r--devtools/client/aboutdebugging/src/components/shared/Message.js109
-rw-r--r--devtools/client/aboutdebugging/src/components/shared/moz.build9
-rw-r--r--devtools/client/aboutdebugging/src/components/sidebar/RefreshDevicesButton.js46
-rw-r--r--devtools/client/aboutdebugging/src/components/sidebar/Sidebar.css43
-rw-r--r--devtools/client/aboutdebugging/src/components/sidebar/Sidebar.js259
-rw-r--r--devtools/client/aboutdebugging/src/components/sidebar/SidebarFixedItem.css29
-rw-r--r--devtools/client/aboutdebugging/src/components/sidebar/SidebarFixedItem.js60
-rw-r--r--devtools/client/aboutdebugging/src/components/sidebar/SidebarItem.css71
-rw-r--r--devtools/client/aboutdebugging/src/components/sidebar/SidebarItem.js81
-rw-r--r--devtools/client/aboutdebugging/src/components/sidebar/SidebarRuntimeItem.css41
-rw-r--r--devtools/client/aboutdebugging/src/components/sidebar/SidebarRuntimeItem.js216
-rw-r--r--devtools/client/aboutdebugging/src/components/sidebar/moz.build11
64 files changed, 5158 insertions, 0 deletions
diff --git a/devtools/client/aboutdebugging/src/components/App.css b/devtools/client/aboutdebugging/src/components/App.css
new file mode 100644
index 0000000000..5196ce8e2e
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/App.css
@@ -0,0 +1,71 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * The current layout of about:debugging is
+ *
+ * +-------------+-------------------------------+
+ * | Sidebar | Page (Runtime or Connect) |
+ * | (240px) | |
+ * | | |
+ * +-------------+-------------------------------+
+ *
+ * Some of the values (font sizes, widths, etc.) are the same as
+ * about:preferences, which uses the shared common.css
+ */
+
+.app {
+ /* from common */
+ --sidebar-width: 280px;
+ --app-top-padding: 70px;
+ --app-bottom-padding: 40px;
+ --app-left-padding: 32px;
+ --app-right-padding: 32px;
+
+ box-sizing: border-box;
+ width: 100vw;
+ height: 100vh;
+ overflow: hidden; /* we don't want the sidebar to scroll, only the main content */
+
+ display: grid;
+ grid-column-gap: 40px;
+ grid-template-columns: var(--sidebar-width) auto;
+
+ font-size: var(--base-font-size);
+ font-weight: var(--base-font-weight);
+ line-height: var(--base-line-height);
+}
+
+.app__sidebar {
+ padding-block-start: var(--app-top-padding);
+ padding-block-end: var(--app-bottom-padding);
+ padding-inline-start: var(--app-left-padding);
+}
+
+.app__content {
+ /* we want to scroll only the main content, not the sidebar */
+ overflow-y: auto;
+
+ /* padding will give space for card shadow to appear and
+ margin will correct the alignment */
+ margin-inline-start: calc(var(--card-shadow-blur-radius) * -1);
+ padding-inline: var(--card-shadow-blur-radius);
+ padding-block-start: var(--app-top-padding);
+}
+
+/* Workaround for Gecko clipping the padding-bottom of a scrollable container;
+ we create a block to act as the bottom padding instead. */
+.app__content::after {
+ content: "";
+ display: block;
+ height: var(--app-bottom-padding);
+}
+
+.page {
+ max-width: var(--page-width);
+ min-width: min-content;
+ font-size: var(--body-20-font-size);
+ font-weight: var(--body-20-font-weight);
+ padding-inline-end: var(--app-right-padding);
+}
diff --git a/devtools/client/aboutdebugging/src/components/App.js b/devtools/client/aboutdebugging/src/components/App.js
new file mode 100644
index 0000000000..7bdf3eb0c5
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/App.js
@@ -0,0 +1,213 @@
+/* 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 Route = createFactory(
+ require("resource://devtools/client/shared/vendor/react-router-dom.js").Route
+);
+const Switch = createFactory(
+ require("resource://devtools/client/shared/vendor/react-router-dom.js").Switch
+);
+const Redirect = createFactory(
+ require("resource://devtools/client/shared/vendor/react-router-dom.js")
+ .Redirect
+);
+
+const Types = require("resource://devtools/client/aboutdebugging/src/types/index.js");
+const {
+ PAGE_TYPES,
+ RUNTIMES,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+const ConnectPage = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/connect/ConnectPage.js")
+);
+const RuntimePage = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/RuntimePage.js")
+);
+const Sidebar = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/sidebar/Sidebar.js")
+);
+
+class App extends PureComponent {
+ static get propTypes() {
+ return {
+ adbAddonStatus: Types.adbAddonStatus,
+ // The "dispatch" helper is forwarded to the App component via connect.
+ // From that point, components are responsible for forwarding the dispatch
+ // property to all components who need to dispatch actions.
+ dispatch: PropTypes.func.isRequired,
+ // getString prop is injected by the withLocalization wrapper
+ getString: PropTypes.func.isRequired,
+ isAdbReady: PropTypes.bool.isRequired,
+ isScanningUsb: PropTypes.bool.isRequired,
+ networkLocations: PropTypes.arrayOf(Types.location).isRequired,
+ networkRuntimes: PropTypes.arrayOf(Types.runtime).isRequired,
+ selectedPage: Types.page,
+ selectedRuntimeId: PropTypes.string,
+ usbRuntimes: PropTypes.arrayOf(Types.runtime).isRequired,
+ };
+ }
+
+ componentDidUpdate() {
+ this.updateTitle();
+ }
+
+ updateTitle() {
+ const { getString, selectedPage, selectedRuntimeId } = this.props;
+
+ const pageTitle =
+ selectedPage === PAGE_TYPES.RUNTIME
+ ? getString("about-debugging-page-title-runtime-page", {
+ selectedRuntimeId,
+ })
+ : getString("about-debugging-page-title-setup-page");
+
+ document.title = pageTitle;
+ }
+
+ renderConnect() {
+ const { adbAddonStatus, dispatch, networkLocations } = this.props;
+
+ return ConnectPage({
+ adbAddonStatus,
+ dispatch,
+ networkLocations,
+ });
+ }
+
+ // The `match` object here is passed automatically by the Route object.
+ // We are using it to read the route path.
+ // See react-router docs:
+ // https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/match.md
+ renderRuntime({ match }) {
+ const isRuntimeAvailable = id => {
+ const runtimes = [
+ ...this.props.networkRuntimes,
+ ...this.props.usbRuntimes,
+ ];
+ const runtime = runtimes.find(x => x.id === id);
+ return runtime?.runtimeDetails;
+ };
+
+ const { dispatch } = this.props;
+
+ let runtimeId = match.params.runtimeId || RUNTIMES.THIS_FIREFOX;
+ if (match.params.runtimeId !== RUNTIMES.THIS_FIREFOX) {
+ const rawId = decodeURIComponent(match.params.runtimeId);
+ if (isRuntimeAvailable(rawId)) {
+ runtimeId = rawId;
+ } else {
+ // Also redirect to "This Firefox" if runtime is not found
+ return Redirect({ to: `/runtime/${RUNTIMES.THIS_FIREFOX}` });
+ }
+ }
+
+ // we need to pass a key so the component updates when we want to showcase
+ // a different runtime
+ return RuntimePage({ dispatch, key: runtimeId, runtimeId });
+ }
+
+ renderRoutes() {
+ return Switch(
+ {},
+ Route({
+ path: "/setup",
+ render: () => this.renderConnect(),
+ }),
+ Route({
+ path: "/runtime/:runtimeId",
+ render: routeProps => this.renderRuntime(routeProps),
+ }),
+ // default route when there's no match which includes "/"
+ // TODO: the url does not match "/" means invalid URL,
+ // in this case maybe we'd like to do something else than a redirect.
+ // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1509897
+ Route({
+ render: routeProps => {
+ const { pathname } = routeProps.location;
+ // The old about:debugging supported the following routes:
+ // about:debugging#workers, about:debugging#addons and about:debugging#tabs.
+ // Such links can still be found in external documentation pages.
+ // We redirect to This Firefox rather than the Setup Page here.
+ if (
+ pathname === "/workers" ||
+ pathname === "/addons" ||
+ pathname === "/tabs"
+ ) {
+ return Redirect({ to: `/runtime/${RUNTIMES.THIS_FIREFOX}` });
+ }
+ return Redirect({ to: "/setup" });
+ },
+ })
+ );
+ }
+
+ render() {
+ const {
+ adbAddonStatus,
+ dispatch,
+ isAdbReady,
+ isScanningUsb,
+ networkRuntimes,
+ selectedPage,
+ selectedRuntimeId,
+ usbRuntimes,
+ } = this.props;
+
+ return Localized(
+ {},
+ dom.div(
+ { className: "app" },
+ Sidebar({
+ adbAddonStatus,
+ className: "app__sidebar",
+ dispatch,
+ isAdbReady,
+ isScanningUsb,
+ networkRuntimes,
+ selectedPage,
+ selectedRuntimeId,
+ usbRuntimes,
+ }),
+ dom.main({ className: "app__content" }, this.renderRoutes())
+ )
+ );
+ }
+}
+
+const mapStateToProps = state => {
+ return {
+ adbAddonStatus: state.ui.adbAddonStatus,
+ isAdbReady: state.ui.isAdbReady,
+ isScanningUsb: state.ui.isScanningUsb,
+ networkLocations: state.ui.networkLocations,
+ networkRuntimes: state.runtimes.networkRuntimes,
+ selectedPage: state.ui.selectedPage,
+ selectedRuntimeId: state.runtimes.selectedRuntimeId,
+ usbRuntimes: state.runtimes.usbRuntimes,
+ };
+};
+
+const mapDispatchToProps = dispatch => ({
+ dispatch,
+});
+
+module.exports = FluentReact.withLocalization(
+ connect(mapStateToProps, mapDispatchToProps)(App)
+);
diff --git a/devtools/client/aboutdebugging/src/components/CompatibilityWarning.js b/devtools/client/aboutdebugging/src/components/CompatibilityWarning.js
new file mode 100644
index 0000000000..42284fa672
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/CompatibilityWarning.js
@@ -0,0 +1,110 @@
+/* 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 FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const Message = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/shared/Message.js")
+);
+
+const {
+ MESSAGE_LEVEL,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+const {
+ COMPATIBILITY_STATUS,
+} = require("resource://devtools/client/shared/remote-debugging/version-checker.js");
+
+const TROUBLESHOOTING_URL =
+ "https://firefox-source-docs.mozilla.org/devtools-user/about_colon_debugging/";
+const FENNEC_TROUBLESHOOTING_URL =
+ "https://firefox-source-docs.mozilla.org/devtools-user/about_colon_debugging/index.html#connection-to-firefox-for-android-68";
+
+const Types = require("resource://devtools/client/aboutdebugging/src/types/index.js");
+
+class CompatibilityWarning extends PureComponent {
+ static get propTypes() {
+ return {
+ compatibilityReport: Types.compatibilityReport.isRequired,
+ };
+ }
+
+ render() {
+ const {
+ localID,
+ localVersion,
+ minVersion,
+ runtimeID,
+ runtimeVersion,
+ status,
+ } = this.props.compatibilityReport;
+
+ if (status === COMPATIBILITY_STATUS.COMPATIBLE) {
+ return null;
+ }
+
+ let localizationId, statusClassName;
+ switch (status) {
+ case COMPATIBILITY_STATUS.TOO_OLD:
+ statusClassName = "qa-compatibility-warning-too-old";
+ localizationId = "about-debugging-browser-version-too-old";
+ break;
+ case COMPATIBILITY_STATUS.TOO_RECENT:
+ statusClassName = "qa-compatibility-warning-too-recent";
+ localizationId = "about-debugging-browser-version-too-recent";
+ break;
+ case COMPATIBILITY_STATUS.TOO_OLD_FENNEC:
+ statusClassName = "qa-compatibility-warning-too-old-fennec";
+ localizationId = "about-debugging-browser-version-too-old-fennec";
+ break;
+ }
+
+ const troubleshootingUrl =
+ status === COMPATIBILITY_STATUS.TOO_OLD_FENNEC
+ ? FENNEC_TROUBLESHOOTING_URL
+ : TROUBLESHOOTING_URL;
+
+ const messageLevel =
+ status === COMPATIBILITY_STATUS.TOO_OLD_FENNEC
+ ? MESSAGE_LEVEL.ERROR
+ : MESSAGE_LEVEL.WARNING;
+
+ return Message(
+ {
+ level: messageLevel,
+ isCloseable: true,
+ },
+ Localized(
+ {
+ id: localizationId,
+ a: dom.a({
+ href: troubleshootingUrl,
+ target: "_blank",
+ }),
+ $localID: localID,
+ $localVersion: localVersion,
+ $minVersion: minVersion,
+ $runtimeID: runtimeID,
+ $runtimeVersion: runtimeVersion,
+ },
+ dom.p(
+ {
+ className: `qa-compatibility-warning ${statusClassName}`,
+ },
+ localizationId
+ )
+ )
+ );
+ }
+}
+
+module.exports = CompatibilityWarning;
diff --git a/devtools/client/aboutdebugging/src/components/ConnectionPromptSetting.js b/devtools/client/aboutdebugging/src/components/ConnectionPromptSetting.js
new file mode 100644
index 0000000000..d4773bb298
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/ConnectionPromptSetting.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 Actions = require("resource://devtools/client/aboutdebugging/src/actions/index.js");
+
+class ConnectionPromptSetting extends PureComponent {
+ static get propTypes() {
+ return {
+ className: PropTypes.string,
+ connectionPromptEnabled: PropTypes.bool.isRequired,
+ dispatch: PropTypes.func.isRequired,
+ };
+ }
+
+ onToggleClick() {
+ const { connectionPromptEnabled, dispatch } = this.props;
+ dispatch(Actions.updateConnectionPromptSetting(!connectionPromptEnabled));
+ }
+
+ render() {
+ const { className, connectionPromptEnabled } = this.props;
+
+ const localizedState = connectionPromptEnabled
+ ? "about-debugging-connection-prompt-disable-button"
+ : "about-debugging-connection-prompt-enable-button";
+
+ return Localized(
+ {
+ id: localizedState,
+ },
+ dom.button(
+ {
+ className: `${className} default-button qa-connection-prompt-toggle-button`,
+ onClick: () => this.onToggleClick(),
+ },
+ localizedState
+ )
+ );
+ }
+}
+
+module.exports = ConnectionPromptSetting;
diff --git a/devtools/client/aboutdebugging/src/components/ProfilerDialog.css b/devtools/client/aboutdebugging/src/components/ProfilerDialog.css
new file mode 100644
index 0000000000..d5352bbea2
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/ProfilerDialog.css
@@ -0,0 +1,63 @@
+/* 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/. */
+
+.profiler-dialog__frame {
+ border: none;
+ height: 100%;
+ width: 100%;
+}
+
+/*
+ * The current layout of the dialog header is
+ *
+ * +-----------------------------+---+
+ * | dialog title (auto) | X |
+ * +-----------------------------+---+
+ */
+.profiler-dialog__header {
+ align-items: center;
+ background-color: var(--popup-header-background-color);
+ color: var(--popup-header-color);
+ display: grid;
+ grid-template-columns: 1fr max-content;
+ padding: var(--base-unit);
+}
+
+.profiler-dialog__header__title {
+ margin-inline-start: calc(var(--base-unit) * 2);
+
+ /* Reset <h1> styles */
+ font-size: 15px;
+ font-weight: normal;
+}
+
+.profiler-dialog__inner {
+ background-color: var(--box-background);
+ display: grid;
+ grid-template-rows: max-content auto;
+ max-height: calc(100% - calc(var(--base-unit) * 25)); /* 100% - 100px */
+ position: fixed;
+}
+
+.profiler-dialog__inner--medium {
+ width: calc(var(--base-unit) * 150); /* 600px */
+ height: calc(var(--base-unit) * 150); /* 600px */
+}
+
+.profiler-dialog__inner--large {
+ width: calc(var(--base-unit) * 200); /* 800px */
+ height: calc(var(--base-unit) * 175); /* 700px */
+}
+
+.profiler-dialog__mask {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: var(--grey-90-a60);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
diff --git a/devtools/client/aboutdebugging/src/components/ProfilerDialog.js b/devtools/client/aboutdebugging/src/components/ProfilerDialog.js
new file mode 100644
index 0000000000..f4bb583464
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/ProfilerDialog.js
@@ -0,0 +1,168 @@
+/* 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 Actions = require("resource://devtools/client/aboutdebugging/src/actions/index.js");
+const Types = require("resource://devtools/client/aboutdebugging/src/types/index.js");
+const {
+ PROFILER_PAGE_CONTEXT,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+/**
+ * This component is a modal dialog containing the performance profiler UI. It uses
+ * the simplified DevTools panel located in devtools/client/performance-new. When
+ * using a custom preset, and editing the settings, the page context switches
+ * to about:profiling, which receives the PerfFront of the remote debuggee.
+ */
+class ProfilerDialog extends PureComponent {
+ static get propTypes() {
+ return {
+ runtimeDetails: Types.runtimeDetails.isRequired,
+ profilerContext: PropTypes.string.isRequired,
+ hideProfilerDialog: PropTypes.func.isRequired,
+ switchProfilerContext: PropTypes.func.isRequired,
+ };
+ }
+
+ hide() {
+ this.props.hideProfilerDialog();
+ }
+
+ setProfilerIframeDirection(frameWindow) {
+ // Set iframe direction according to the parent document direction.
+ const { documentElement } = document;
+ const dir = window.getComputedStyle(documentElement).direction;
+ frameWindow.document.documentElement.setAttribute("dir", dir);
+ }
+
+ /**
+ * The profiler iframe can either be the simplified devtools recording panel,
+ * or the more detailed about:profiling settings page.
+ */
+ renderProfilerIframe() {
+ const {
+ runtimeDetails: { clientWrapper },
+ switchProfilerContext,
+ profilerContext,
+ } = this.props;
+
+ let src, onLoad;
+
+ switch (profilerContext) {
+ case PROFILER_PAGE_CONTEXT.DEVTOOLS_REMOTE:
+ src = clientWrapper.getPerformancePanelUrl();
+ onLoad = e => {
+ const frameWindow = e.target.contentWindow;
+ this.setProfilerIframeDirection(frameWindow);
+
+ clientWrapper.loadPerformanceProfiler(frameWindow, () => {
+ switchProfilerContext(PROFILER_PAGE_CONTEXT.ABOUTPROFILING_REMOTE);
+ });
+ };
+ break;
+
+ case PROFILER_PAGE_CONTEXT.ABOUTPROFILING_REMOTE:
+ src = "about:profiling#remote";
+ onLoad = e => {
+ const frameWindow = e.target.contentWindow;
+ this.setProfilerIframeDirection(frameWindow);
+
+ clientWrapper.loadAboutProfiling(frameWindow, () => {
+ switchProfilerContext(PROFILER_PAGE_CONTEXT.DEVTOOLS_REMOTE);
+ });
+ };
+ break;
+
+ default:
+ throw new Error(`Unhandled profiler context: "${profilerContext}"`);
+ }
+
+ return dom.iframe({
+ key: profilerContext,
+ className: "profiler-dialog__frame",
+ src,
+ onLoad,
+ });
+ }
+
+ render() {
+ const { profilerContext, switchProfilerContext } = this.props;
+ const dialogSizeClassName =
+ profilerContext === PROFILER_PAGE_CONTEXT.DEVTOOLS_REMOTE
+ ? "profiler-dialog__inner--medium"
+ : "profiler-dialog__inner--large";
+
+ return dom.div(
+ {
+ className: "profiler-dialog__mask qa-profiler-dialog-mask",
+ onClick: () => this.hide(),
+ },
+ dom.article(
+ {
+ className: `profiler-dialog__inner ${dialogSizeClassName} qa-profiler-dialog`,
+ onClick: e => e.stopPropagation(),
+ },
+ dom.header(
+ {
+ className: "profiler-dialog__header",
+ },
+ Localized(
+ {
+ id: "about-debugging-profiler-dialog-title2",
+ },
+ dom.h1(
+ {
+ className: "profiler-dialog__header__title",
+ },
+ "about-debugging-profiler-dialog-title2"
+ )
+ ),
+ dom.button(
+ {
+ className: "ghost-button qa-profiler-dialog-close",
+ onClick: () => {
+ if (profilerContext === PROFILER_PAGE_CONTEXT.DEVTOOLS_REMOTE) {
+ this.hide();
+ } else {
+ switchProfilerContext(PROFILER_PAGE_CONTEXT.DEVTOOLS_REMOTE);
+ }
+ },
+ },
+ dom.img({
+ src: "chrome://devtools/skin/images/close.svg",
+ })
+ )
+ ),
+ this.renderProfilerIframe()
+ )
+ );
+ }
+}
+
+const mapStateToProps = state => {
+ return {
+ profilerContext: state.ui.profilerContext,
+ };
+};
+
+const mapDispatchToProps = {
+ hideProfilerDialog: Actions.hideProfilerDialog,
+ switchProfilerContext: Actions.switchProfilerContext,
+};
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(ProfilerDialog);
diff --git a/devtools/client/aboutdebugging/src/components/RuntimeActions.css b/devtools/client/aboutdebugging/src/components/RuntimeActions.css
new file mode 100644
index 0000000000..6333560d4b
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/RuntimeActions.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/. */
+
+.runtime-actions__toolbar {
+ column-gap: var(--base-unit);
+ display: flex;
+ justify-content: end;
+}
diff --git a/devtools/client/aboutdebugging/src/components/RuntimeActions.js b/devtools/client/aboutdebugging/src/components/RuntimeActions.js
new file mode 100644
index 0000000000..eefa8b500b
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/RuntimeActions.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 {
+ 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 ConnectionPromptSetting = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/ConnectionPromptSetting.js")
+);
+
+const Actions = require("resource://devtools/client/aboutdebugging/src/actions/index.js");
+const {
+ RUNTIMES,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+const Types = require("resource://devtools/client/aboutdebugging/src/types/index.js");
+
+class RuntimeActions extends PureComponent {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ runtimeDetails: Types.runtimeDetails,
+ runtimeId: PropTypes.string.isRequired,
+ };
+ }
+
+ onProfilerButtonClick() {
+ this.props.dispatch(Actions.showProfilerDialog());
+ }
+
+ renderConnectionPromptSetting() {
+ const { dispatch, runtimeDetails, runtimeId } = this.props;
+ const { connectionPromptEnabled } = runtimeDetails;
+ // do not show the connection prompt setting in 'This Firefox'
+ return runtimeId !== RUNTIMES.THIS_FIREFOX
+ ? ConnectionPromptSetting({
+ connectionPromptEnabled,
+ dispatch,
+ })
+ : null;
+ }
+
+ renderProfileButton() {
+ const { runtimeId } = this.props;
+
+ return runtimeId !== RUNTIMES.THIS_FIREFOX
+ ? Localized(
+ {
+ id: "about-debugging-runtime-profile-button2",
+ },
+ dom.button(
+ {
+ className: "default-button qa-profile-runtime-button",
+ onClick: () => this.onProfilerButtonClick(),
+ },
+ "about-debugging-runtime-profile-button2"
+ )
+ )
+ : null;
+ }
+
+ render() {
+ return dom.div(
+ {
+ className: "runtime-actions__toolbar",
+ },
+ this.renderProfileButton(),
+ this.renderConnectionPromptSetting()
+ );
+ }
+}
+
+module.exports = RuntimeActions;
diff --git a/devtools/client/aboutdebugging/src/components/RuntimeInfo.css b/devtools/client/aboutdebugging/src/components/RuntimeInfo.css
new file mode 100644
index 0000000000..e6fcd9dd7e
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/RuntimeInfo.css
@@ -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/. */
+
+
+/**
+ * Layout for the runtime info container is:
+ *
+ * <- 68px --x--------- 1fr ----------><---- max ---->
+ * ∧ +---------+------------------------+--------------+
+ * 1fr | | Runtime Info | [Action] |
+ * | | Icon | eg "Firefox (70.0a1)" | |
+ * x | +------------------------+ |
+ * max | | Device Name (optional) | |
+ * ∨ +---------+------------------------+--------------+
+ */
+.runtime-info {
+ align-items: center;
+ display: grid;
+
+ grid-column-gap: var(--main-heading-icon-gap);
+ grid-template-areas:
+ "icon title action"
+ "icon subtitle .";
+ grid-template-columns: var(--main-heading-icon-size) 1fr max-content;
+ grid-template-rows: 1fr max-content;
+
+ margin-block-end: calc(var(--base-unit) * 5);
+}
+
+.runtime-info__icon {
+ grid-area: icon;
+}
+.runtime-info__title {
+ grid-area: title;
+}
+.runtime-info__subtitle {
+ grid-area: subtitle;
+}
+.runtime-info__action {
+ grid-area: action;
+}
diff --git a/devtools/client/aboutdebugging/src/components/RuntimeInfo.js b/devtools/client/aboutdebugging/src/components/RuntimeInfo.js
new file mode 100644
index 0000000000..6a8c67dd33
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/RuntimeInfo.js
@@ -0,0 +1,89 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ 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 Actions = require("resource://devtools/client/aboutdebugging/src/actions/index.js");
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const {
+ RUNTIMES,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+/**
+ * This component displays runtime information.
+ */
+class RuntimeInfo extends PureComponent {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ icon: PropTypes.string.isRequired,
+ deviceName: PropTypes.string,
+ name: PropTypes.string.isRequired,
+ version: PropTypes.string.isRequired,
+ runtimeId: PropTypes.string.isRequired,
+ };
+ }
+ render() {
+ const { icon, deviceName, name, version, runtimeId, dispatch } = this.props;
+
+ return dom.h1(
+ {
+ className: "main-heading runtime-info",
+ },
+ dom.img({
+ className: "main-heading__icon runtime-info__icon qa-runtime-icon",
+ src: icon,
+ }),
+ Localized(
+ {
+ id: "about-debugging-runtime-name",
+ $name: name,
+ $version: version,
+ },
+ dom.label(
+ {
+ className: "qa-runtime-name runtime-info__title",
+ },
+ `${name} (${version})`
+ )
+ ),
+ deviceName
+ ? dom.label(
+ {
+ className: "main-heading-subtitle runtime-info__subtitle",
+ },
+ deviceName
+ )
+ : null,
+ runtimeId !== RUNTIMES.THIS_FIREFOX
+ ? Localized(
+ {
+ id: "about-debugging-runtime-disconnect-button",
+ },
+ dom.button(
+ {
+ className:
+ "default-button runtime-info__action qa-runtime-info__action",
+ onClick() {
+ dispatch(Actions.disconnectRuntime(runtimeId, true));
+ },
+ },
+ "Disconnect"
+ )
+ )
+ : null
+ );
+ }
+}
+
+module.exports = RuntimeInfo;
diff --git a/devtools/client/aboutdebugging/src/components/RuntimePage.js b/devtools/client/aboutdebugging/src/components/RuntimePage.js
new file mode 100644
index 0000000000..e2dae9b0cd
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/RuntimePage.js
@@ -0,0 +1,306 @@
+/* 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 CompatibilityWarning = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/CompatibilityWarning.js")
+);
+const DebugTargetPane = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetPane.js")
+);
+const ExtensionDetail = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/debugtarget/ExtensionDetail.js")
+);
+const InspectAction = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/debugtarget/InspectAction.js")
+);
+const ProfilerDialog = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/ProfilerDialog.js")
+);
+const RuntimeActions = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/RuntimeActions.js")
+);
+const RuntimeInfo = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/RuntimeInfo.js")
+);
+const ServiceWorkerAction = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/debugtarget/ServiceWorkerAction.js")
+);
+const ServiceWorkerAdditionalActions = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/debugtarget/ServiceWorkerAdditionalActions.js")
+);
+const ServiceWorkersWarning = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/ServiceWorkersWarning.js")
+);
+const ProcessDetail = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/debugtarget/ProcessDetail.js")
+);
+const TabAction = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/debugtarget/TabAction.js")
+);
+const TabDetail = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/debugtarget/TabDetail.js")
+);
+const TemporaryExtensionAdditionalActions = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/debugtarget/TemporaryExtensionAdditionalActions.js")
+);
+const TemporaryExtensionDetail = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/debugtarget/TemporaryExtensionDetail.js")
+);
+const TemporaryExtensionInstallSection = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/debugtarget/TemporaryExtensionInstallSection.js")
+);
+const WorkerDetail = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/debugtarget/WorkerDetail.js")
+);
+
+const Actions = require("resource://devtools/client/aboutdebugging/src/actions/index.js");
+const {
+ DEBUG_TARGETS,
+ DEBUG_TARGET_PANE,
+ PAGE_TYPES,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+const Types = require("resource://devtools/client/aboutdebugging/src/types/index.js");
+
+const {
+ getCurrentRuntimeDetails,
+} = require("resource://devtools/client/aboutdebugging/src/modules/runtimes-state-helper.js");
+const {
+ isSupportedDebugTargetPane,
+ supportsTemporaryExtensionInstaller,
+} = require("resource://devtools/client/aboutdebugging/src/modules/debug-target-support.js");
+
+class RuntimePage extends PureComponent {
+ static get propTypes() {
+ return {
+ collapsibilities: Types.collapsibilities.isRequired,
+ dispatch: PropTypes.func.isRequired,
+ installedExtensions: PropTypes.arrayOf(PropTypes.object).isRequired,
+ otherWorkers: PropTypes.arrayOf(PropTypes.object).isRequired,
+ runtimeDetails: Types.runtimeDetails,
+ runtimeId: PropTypes.string.isRequired,
+ processes: PropTypes.arrayOf(PropTypes.object).isRequired,
+ serviceWorkers: PropTypes.arrayOf(PropTypes.object).isRequired,
+ sharedWorkers: PropTypes.arrayOf(PropTypes.object).isRequired,
+ showProfilerDialog: PropTypes.bool.isRequired,
+ tabs: PropTypes.arrayOf(PropTypes.object).isRequired,
+ temporaryExtensions: PropTypes.arrayOf(PropTypes.object).isRequired,
+ temporaryInstallError: PropTypes.object,
+ };
+ }
+
+ // TODO: avoid the use of this method
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillMount() {
+ const { dispatch, runtimeId } = this.props;
+ dispatch(Actions.selectPage(PAGE_TYPES.RUNTIME, runtimeId));
+ }
+
+ getIconByType(type) {
+ switch (type) {
+ case DEBUG_TARGETS.EXTENSION:
+ return "chrome://devtools/skin/images/debugging-addons.svg";
+ case DEBUG_TARGETS.PROCESS:
+ return "chrome://devtools/skin/images/aboutdebugging-process-icon.svg";
+ case DEBUG_TARGETS.TAB:
+ return "chrome://devtools/skin/images/debugging-tabs.svg";
+ case DEBUG_TARGETS.WORKER:
+ return "chrome://devtools/skin/images/debugging-workers.svg";
+ }
+
+ throw new Error(`Unsupported type [${type}]`);
+ }
+
+ renderDebugTargetPane({
+ actionComponent,
+ additionalActionsComponent,
+ children,
+ detailComponent,
+ icon,
+ localizationId,
+ name,
+ paneKey,
+ targets,
+ }) {
+ const { collapsibilities, dispatch, runtimeDetails } = this.props;
+
+ if (!isSupportedDebugTargetPane(runtimeDetails.info.type, paneKey)) {
+ return null;
+ }
+
+ return Localized(
+ {
+ id: localizationId,
+ attrs: { name: true },
+ },
+ DebugTargetPane(
+ {
+ actionComponent,
+ additionalActionsComponent,
+ collapsibilityKey: paneKey,
+ detailComponent,
+ dispatch,
+ icon,
+ isCollapsed: collapsibilities.get(paneKey),
+ name,
+ targets,
+ },
+ children
+ )
+ );
+ }
+
+ renderTemporaryExtensionInstallSection() {
+ const runtimeType = this.props.runtimeDetails.info.type;
+ if (
+ !isSupportedDebugTargetPane(
+ runtimeType,
+ DEBUG_TARGET_PANE.TEMPORARY_EXTENSION
+ ) ||
+ !supportsTemporaryExtensionInstaller(runtimeType)
+ ) {
+ return null;
+ }
+
+ const { dispatch, temporaryInstallError } = this.props;
+ return TemporaryExtensionInstallSection({
+ dispatch,
+ temporaryInstallError,
+ });
+ }
+
+ render() {
+ const {
+ dispatch,
+ installedExtensions,
+ otherWorkers,
+ processes,
+ runtimeDetails,
+ runtimeId,
+ serviceWorkers,
+ sharedWorkers,
+ showProfilerDialog,
+ tabs,
+ temporaryExtensions,
+ } = this.props;
+
+ if (!runtimeDetails) {
+ // runtimeInfo can be null when the selectPage action navigates from a runtime A
+ // to a runtime B (between unwatchRuntime and watchRuntime).
+ return null;
+ }
+
+ const { compatibilityReport } = runtimeDetails;
+
+ return dom.article(
+ {
+ className: "page qa-runtime-page",
+ },
+ RuntimeInfo({ ...runtimeDetails.info, runtimeId, dispatch }),
+ RuntimeActions({ dispatch, runtimeId, runtimeDetails }),
+ runtimeDetails.serviceWorkersAvailable ? null : ServiceWorkersWarning(),
+ CompatibilityWarning({ compatibilityReport }),
+ this.renderDebugTargetPane({
+ actionComponent: TabAction,
+ detailComponent: TabDetail,
+ icon: this.getIconByType(DEBUG_TARGETS.TAB),
+ localizationId: "about-debugging-runtime-tabs",
+ name: "Tabs",
+ paneKey: DEBUG_TARGET_PANE.TAB,
+ targets: tabs,
+ }),
+ this.renderDebugTargetPane({
+ actionComponent: InspectAction,
+ additionalActionsComponent: TemporaryExtensionAdditionalActions,
+ children: this.renderTemporaryExtensionInstallSection(),
+ detailComponent: TemporaryExtensionDetail,
+ icon: this.getIconByType(DEBUG_TARGETS.EXTENSION),
+ localizationId: "about-debugging-runtime-temporary-extensions",
+ name: "Temporary Extensions",
+ paneKey: DEBUG_TARGET_PANE.TEMPORARY_EXTENSION,
+ targets: temporaryExtensions,
+ }),
+ this.renderDebugTargetPane({
+ actionComponent: InspectAction,
+ detailComponent: ExtensionDetail,
+ icon: this.getIconByType(DEBUG_TARGETS.EXTENSION),
+ localizationId: "about-debugging-runtime-extensions",
+ name: "Extensions",
+ paneKey: DEBUG_TARGET_PANE.INSTALLED_EXTENSION,
+ targets: installedExtensions,
+ }),
+ this.renderDebugTargetPane({
+ actionComponent: ServiceWorkerAction,
+ additionalActionsComponent: ServiceWorkerAdditionalActions,
+ detailComponent: WorkerDetail,
+ icon: this.getIconByType(DEBUG_TARGETS.WORKER),
+ localizationId: "about-debugging-runtime-service-workers",
+ name: "Service Workers",
+ paneKey: DEBUG_TARGET_PANE.SERVICE_WORKER,
+ targets: serviceWorkers,
+ }),
+ this.renderDebugTargetPane({
+ actionComponent: InspectAction,
+ detailComponent: WorkerDetail,
+ icon: this.getIconByType(DEBUG_TARGETS.WORKER),
+ localizationId: "about-debugging-runtime-shared-workers",
+ name: "Shared Workers",
+ paneKey: DEBUG_TARGET_PANE.SHARED_WORKER,
+ targets: sharedWorkers,
+ }),
+ this.renderDebugTargetPane({
+ actionComponent: InspectAction,
+ detailComponent: WorkerDetail,
+ icon: this.getIconByType(DEBUG_TARGETS.WORKER),
+ localizationId: "about-debugging-runtime-other-workers",
+ name: "Other Workers",
+ paneKey: DEBUG_TARGET_PANE.OTHER_WORKER,
+ targets: otherWorkers,
+ }),
+ this.renderDebugTargetPane({
+ actionComponent: InspectAction,
+ detailComponent: ProcessDetail,
+ icon: this.getIconByType(DEBUG_TARGETS.PROCESS),
+ localizationId: "about-debugging-runtime-processes",
+ name: "Processes",
+ paneKey: DEBUG_TARGET_PANE.PROCESSES,
+ targets: processes,
+ }),
+
+ showProfilerDialog ? ProfilerDialog({ dispatch, runtimeDetails }) : null
+ );
+ }
+}
+
+const mapStateToProps = state => {
+ return {
+ collapsibilities: state.ui.debugTargetCollapsibilities,
+ installedExtensions: state.debugTargets.installedExtensions,
+ processes: state.debugTargets.processes,
+ otherWorkers: state.debugTargets.otherWorkers,
+ runtimeDetails: getCurrentRuntimeDetails(state.runtimes),
+ serviceWorkers: state.debugTargets.serviceWorkers,
+ sharedWorkers: state.debugTargets.sharedWorkers,
+ showProfilerDialog: state.ui.showProfilerDialog,
+ tabs: state.debugTargets.tabs,
+ temporaryExtensions: state.debugTargets.temporaryExtensions,
+ temporaryInstallError: state.ui.temporaryInstallError,
+ };
+};
+
+module.exports = connect(mapStateToProps)(RuntimePage);
diff --git a/devtools/client/aboutdebugging/src/components/ServiceWorkersWarning.js b/devtools/client/aboutdebugging/src/components/ServiceWorkersWarning.js
new file mode 100644
index 0000000000..4f9dc93d7f
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/ServiceWorkersWarning.js
@@ -0,0 +1,52 @@
+/* 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 FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const Message = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/shared/Message.js")
+);
+
+const {
+ MESSAGE_LEVEL,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+const DOC_URL =
+ "https://firefox-source-docs.mozilla.org/devtools-user/about_colon_debugging/index.html#service-workers-not-compatible";
+
+class ServiceWorkersWarning extends PureComponent {
+ render() {
+ return Message(
+ {
+ level: MESSAGE_LEVEL.WARNING,
+ isCloseable: true,
+ },
+ Localized(
+ {
+ id: "about-debugging-runtime-service-workers-not-compatible",
+ a: dom.a({
+ href: DOC_URL,
+ target: "_blank",
+ }),
+ },
+ dom.p(
+ {
+ className: "qa-service-workers-warning",
+ },
+ "about-debugging-runtime-service-workers-not-compatible"
+ )
+ )
+ );
+ }
+}
+
+module.exports = ServiceWorkersWarning;
diff --git a/devtools/client/aboutdebugging/src/components/connect/ConnectPage.css b/devtools/client/aboutdebugging/src/components/connect/ConnectPage.css
new file mode 100644
index 0000000000..a693bf4113
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/connect/ConnectPage.css
@@ -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/. */
+
+.connect-page__breather {
+ margin-block-start: calc(var(--base-unit) * 6);
+}
+
+/*
+ * +--------+----------------------+
+ * | USB | |<button> |
+ * +--------+ | |
+ * | status | | |
+ * +--------+----------------------+
+ */
+.connect-page__usb-section__heading {
+ display: grid;
+ align-items: center;
+ grid-template-areas: "title . toggle"
+ "status . toggle";
+ grid-template-columns: auto 1fr auto;
+ grid-column-gap: calc(var(--base-unit) * 2);
+ grid-row-gap: var(--base-unit);
+}
+
+.connect-page__usb-section__heading__toggle {
+ grid-area: toggle;
+}
+
+.connect-page__usb-section__heading__title {
+ grid-area: title;
+ line-height: 1;
+}
+.connect-page__usb-section__heading__status {
+ grid-area: status;
+ line-height: 1;
+ font-size: var(--caption-20-font-size);
+ font-weight: var(--caption-20-font-weight);
+ color: var(--secondary-text-color);
+}
+
+.connect-page__troubleshoot {
+ font-size: var(--body-10-font-size);
+ font-weight: var(--body-10-font-weight);
+ margin-block-start: calc(var(--base-unit) * 2);
+}
+
+.connect-page__troubleshoot--network {
+ padding-inline: calc(var(--base-unit) * 6);
+}
diff --git a/devtools/client/aboutdebugging/src/components/connect/ConnectPage.js b/devtools/client/aboutdebugging/src/components/connect/ConnectPage.js
new file mode 100644
index 0000000000..97b1b01df7
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/connect/ConnectPage.js
@@ -0,0 +1,315 @@
+/* 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 {
+ USB_STATES,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+const Actions = require("resource://devtools/client/aboutdebugging/src/actions/index.js");
+
+loader.lazyRequireGetter(
+ this,
+ "ADB_ADDON_STATES",
+ "resource://devtools/client/shared/remote-debugging/adb/adb-addon.js",
+ true
+);
+
+const Link = createFactory(
+ require("resource://devtools/client/shared/vendor/react-router-dom.js").Link
+);
+const ConnectSection = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/connect/ConnectSection.js")
+);
+const ConnectSteps = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/connect/ConnectSteps.js")
+);
+const NetworkLocationsForm = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/connect/NetworkLocationsForm.js")
+);
+const NetworkLocationsList = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/connect/NetworkLocationsList.js")
+);
+
+const {
+ PAGE_TYPES,
+ RUNTIMES,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+const Types = require("resource://devtools/client/aboutdebugging/src/types/index.js");
+
+const USB_ICON_SRC =
+ "chrome://devtools/skin/images/aboutdebugging-usb-icon.svg";
+const GLOBE_ICON_SRC =
+ "chrome://devtools/skin/images/aboutdebugging-globe-icon.svg";
+
+const TROUBLESHOOT_USB_URL =
+ "https://firefox-source-docs.mozilla.org/devtools-user/about_colon_debugging/index.html#connecting-to-a-remote-device";
+const TROUBLESHOOT_NETWORK_URL =
+ "https://firefox-source-docs.mozilla.org/devtools-user/about_colon_debugging/index.html#connecting-over-the-network";
+
+class ConnectPage extends PureComponent {
+ static get propTypes() {
+ return {
+ adbAddonStatus: Types.adbAddonStatus,
+ dispatch: PropTypes.func.isRequired,
+ networkLocations: PropTypes.arrayOf(Types.location).isRequired,
+ };
+ }
+
+ // TODO: avoid the use of this method
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillMount() {
+ this.props.dispatch(Actions.selectPage(PAGE_TYPES.CONNECT));
+ }
+
+ onToggleUSBClick() {
+ const { adbAddonStatus } = this.props;
+ const isAddonInstalled = adbAddonStatus === ADB_ADDON_STATES.INSTALLED;
+ if (isAddonInstalled) {
+ this.props.dispatch(Actions.uninstallAdbAddon());
+ } else {
+ this.props.dispatch(Actions.installAdbAddon());
+ }
+ }
+
+ getUsbStatus() {
+ switch (this.props.adbAddonStatus) {
+ case ADB_ADDON_STATES.INSTALLED:
+ return USB_STATES.ENABLED_USB;
+ case ADB_ADDON_STATES.UNINSTALLED:
+ return USB_STATES.DISABLED_USB;
+ default:
+ return USB_STATES.UPDATING_USB;
+ }
+ }
+
+ renderUsbStatus() {
+ const statusTextId = {
+ [USB_STATES.ENABLED_USB]: "about-debugging-setup-usb-status-enabled",
+ [USB_STATES.DISABLED_USB]: "about-debugging-setup-usb-status-disabled",
+ [USB_STATES.UPDATING_USB]: "about-debugging-setup-usb-status-updating",
+ }[this.getUsbStatus()];
+
+ return Localized(
+ {
+ id: statusTextId,
+ },
+ dom.span(
+ {
+ className: "connect-page__usb-section__heading__status",
+ },
+ statusTextId
+ )
+ );
+ }
+
+ renderUsbToggleButton() {
+ const usbStatus = this.getUsbStatus();
+
+ const localizedStates = {
+ [USB_STATES.ENABLED_USB]: "about-debugging-setup-usb-disable-button",
+ [USB_STATES.DISABLED_USB]: "about-debugging-setup-usb-enable-button",
+ [USB_STATES.UPDATING_USB]: "about-debugging-setup-usb-updating-button",
+ };
+ const localizedState = localizedStates[usbStatus];
+
+ // Disable the button while the USB status is updating.
+ const disabled = usbStatus === USB_STATES.UPDATING_USB;
+
+ return Localized(
+ {
+ id: localizedState,
+ },
+ dom.button(
+ {
+ className:
+ "default-button connect-page__usb-section__heading__toggle " +
+ "qa-connect-usb-toggle-button",
+ disabled,
+ onClick: () => this.onToggleUSBClick(),
+ },
+ localizedState
+ )
+ );
+ }
+
+ renderUsb() {
+ const { adbAddonStatus } = this.props;
+ const isAddonInstalled = adbAddonStatus === ADB_ADDON_STATES.INSTALLED;
+ return ConnectSection(
+ {
+ icon: USB_ICON_SRC,
+ title: dom.div(
+ {
+ className: "connect-page__usb-section__heading",
+ },
+ Localized(
+ { id: "about-debugging-setup-usb-title" },
+ dom.span(
+ {
+ className: "connect-page__usb-section__heading__title",
+ },
+ "USB"
+ )
+ ),
+ this.renderUsbStatus(),
+ this.renderUsbToggleButton()
+ ),
+ },
+ isAddonInstalled
+ ? ConnectSteps({
+ steps: [
+ {
+ localizationId:
+ "about-debugging-setup-usb-step-enable-dev-menu2",
+ },
+ {
+ localizationId: "about-debugging-setup-usb-step-enable-debug2",
+ },
+ {
+ localizationId:
+ "about-debugging-setup-usb-step-enable-debug-firefox2",
+ },
+ {
+ localizationId: "about-debugging-setup-usb-step-plug-device",
+ },
+ ],
+ })
+ : Localized(
+ {
+ id: "about-debugging-setup-usb-disabled",
+ },
+ dom.aside(
+ {
+ className: "qa-connect-usb-disabled-message",
+ },
+ "Enabling this will download and add the required Android USB debugging " +
+ "components to Firefox."
+ )
+ ),
+ this.renderTroubleshootText(RUNTIMES.USB)
+ );
+ }
+
+ renderNetwork() {
+ const { dispatch, networkLocations } = this.props;
+
+ return Localized(
+ {
+ id: "about-debugging-setup-network",
+ attrs: { title: true },
+ },
+ ConnectSection({
+ icon: GLOBE_ICON_SRC,
+ title: "Network Location",
+ extraContent: dom.div(
+ {},
+ NetworkLocationsList({ dispatch, networkLocations }),
+ NetworkLocationsForm({ dispatch, networkLocations }),
+ this.renderTroubleshootText(RUNTIMES.NETWORK)
+ ),
+ })
+ );
+ }
+
+ renderTroubleshootText(connectionType) {
+ const localizationId =
+ connectionType === RUNTIMES.USB
+ ? "about-debugging-setup-usb-troubleshoot"
+ : "about-debugging-setup-network-troubleshoot";
+
+ const className =
+ "connect-page__troubleshoot connect-page__troubleshoot--" +
+ `${connectionType === RUNTIMES.USB ? "usb" : "network"}`;
+
+ const url =
+ connectionType === RUNTIMES.USB
+ ? TROUBLESHOOT_USB_URL
+ : TROUBLESHOOT_NETWORK_URL;
+
+ return dom.aside(
+ {
+ className,
+ },
+ Localized(
+ {
+ id: localizationId,
+ a: dom.a({
+ href: url,
+ target: "_blank",
+ }),
+ },
+ dom.p({}, localizationId)
+ )
+ );
+ }
+
+ render() {
+ return dom.article(
+ {
+ className: "page connect-page qa-connect-page",
+ },
+ Localized(
+ {
+ id: "about-debugging-setup-title",
+ },
+ dom.h1(
+ {
+ className: "alt-heading alt-heading--larger",
+ },
+ "Setup"
+ )
+ ),
+ Localized(
+ {
+ id: "about-debugging-setup-intro",
+ },
+ dom.p(
+ {},
+ "Configure the connection method you wish to remotely debug your device with."
+ )
+ ),
+ Localized(
+ {
+ id: "about-debugging-setup-this-firefox2",
+ a: Link({
+ to: `/runtime/${RUNTIMES.THIS_FIREFOX}`,
+ }),
+ },
+ dom.p({}, "about-debugging-setup-this-firefox")
+ ),
+ dom.section(
+ {
+ className: "connect-page__breather",
+ },
+ Localized(
+ {
+ id: "about-debugging-setup-connect-heading",
+ },
+ dom.h2(
+ {
+ className: "alt-heading",
+ },
+ "Connect a device"
+ )
+ ),
+ this.renderUsb(),
+ this.renderNetwork()
+ )
+ );
+ }
+}
+
+module.exports = FluentReact.withLocalization(ConnectPage);
diff --git a/devtools/client/aboutdebugging/src/components/connect/ConnectSection.css b/devtools/client/aboutdebugging/src/components/connect/ConnectSection.css
new file mode 100644
index 0000000000..4349b147b0
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/connect/ConnectSection.css
@@ -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/. */
+
+.connect-section {
+ --icon-size: calc(var(--base-unit) * 9);
+ --header-col-gap: calc(var(--base-unit) * 2);
+ margin-block-end: calc(var(--base-unit) * 4);
+}
+
+/*
+ * +--------+----------------+
+ * | <icon> | <heading> 1fr |
+ * +--------+----------------+
+ */
+.connect-section__header {
+ display: grid;
+ grid-template-areas: "icon heading";
+ grid-template-columns: auto 1fr;
+ grid-template-rows: var(--icon-size);
+ grid-column-gap: var(--header-col-gap);
+ align-items: center;
+
+ padding-block-end: calc(var(--base-unit) * 4);
+ padding-inline: calc(var(--base-unit) * 5);
+}
+
+.connect-section__header__title {
+ grid-area: heading;
+}
+
+.connect-section__header__icon {
+ grid-area: icon;
+ width: var(--icon-size);
+ height: var(--icon-size);
+
+ -moz-context-properties: fill;
+ fill: currentColor;
+}
+
+.connect-section__content {
+ line-height: 1.5;
+ padding-inline-start: calc(var(--base-unit) * 5
+ + var(--header-col-gap) + var(--icon-size));
+ padding-inline-end: calc(var(--base-unit) * 5);
+}
+
+.connect-section__extra {
+ border-block-start: 1px solid var(--card-separator-color);
+}
diff --git a/devtools/client/aboutdebugging/src/components/connect/ConnectSection.js b/devtools/client/aboutdebugging/src/components/connect/ConnectSection.js
new file mode 100644
index 0000000000..55f8eb4e78
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/connect/ConnectSection.js
@@ -0,0 +1,69 @@
+/* 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");
+
+class ConnectSection extends PureComponent {
+ static get propTypes() {
+ return {
+ children: PropTypes.node,
+ className: PropTypes.string,
+ extraContent: PropTypes.node,
+ icon: PropTypes.string.isRequired,
+ title: PropTypes.node.isRequired,
+ };
+ }
+
+ renderExtraContent() {
+ const { extraContent } = this.props;
+ return dom.section(
+ {
+ className: "connect-section__extra",
+ },
+ extraContent
+ );
+ }
+
+ render() {
+ const { extraContent } = this.props;
+
+ return dom.section(
+ {
+ className: `card connect-section ${this.props.className || ""}`,
+ },
+ dom.header(
+ {
+ className: "connect-section__header",
+ },
+ dom.img({
+ className: "connect-section__header__icon",
+ src: this.props.icon,
+ }),
+ dom.h1(
+ {
+ className: "card__heading connect-section__header__title",
+ },
+ this.props.title
+ )
+ ),
+ this.props.children
+ ? dom.div(
+ {
+ className: "connect-section__content",
+ },
+ this.props.children
+ )
+ : null,
+ extraContent ? this.renderExtraContent() : null
+ );
+ }
+}
+
+module.exports = ConnectSection;
diff --git a/devtools/client/aboutdebugging/src/components/connect/ConnectSteps.css b/devtools/client/aboutdebugging/src/components/connect/ConnectSteps.css
new file mode 100644
index 0000000000..bddd513aa7
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/connect/ConnectSteps.css
@@ -0,0 +1,13 @@
+/* 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/. */
+
+.connect-page__step-list {
+ list-style-type: decimal;
+ list-style-position: outside;
+ margin-inline-start: calc(var(--base-unit) * 4);
+}
+
+.connect-page__step {
+ padding-inline-start: var(--base-unit);
+}
diff --git a/devtools/client/aboutdebugging/src/components/connect/ConnectSteps.js b/devtools/client/aboutdebugging/src/components/connect/ConnectSteps.js
new file mode 100644
index 0000000000..0e8d304108
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/connect/ConnectSteps.js
@@ -0,0 +1,51 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ PureComponent,
+ createFactory,
+} = 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);
+
+class ConnectSteps extends PureComponent {
+ static get propTypes() {
+ return {
+ steps: PropTypes.arrayOf(
+ PropTypes.shape({
+ localizationId: PropTypes.string.isRequired,
+ }).isRequired
+ ),
+ };
+ }
+
+ render() {
+ return dom.ul(
+ {
+ className: "connect-page__step-list",
+ },
+ ...this.props.steps.map(step =>
+ Localized(
+ {
+ id: step.localizationId,
+ },
+ dom.li(
+ {
+ className: "connect-page__step",
+ key: step.localizationId,
+ },
+ step.localizationId
+ )
+ )
+ )
+ );
+ }
+}
+
+module.exports = ConnectSteps;
diff --git a/devtools/client/aboutdebugging/src/components/connect/NetworkLocationsForm.css b/devtools/client/aboutdebugging/src/components/connect/NetworkLocationsForm.css
new file mode 100644
index 0000000000..5694bcf216
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/connect/NetworkLocationsForm.css
@@ -0,0 +1,23 @@
+/* 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/. */
+
+/*
+ * Layout of a network location form
+ *
+ * +-------------+--------------------+------------+
+ * | "Host:port" | Input | Add button |
+ * +-------------+--------------------+------------+
+ */
+.connect-page__network-form {
+ display: grid;
+ grid-column-gap: calc(var(--base-unit) * 2);
+ grid-template-columns: auto 1fr auto;
+ align-items: center;
+ padding-block-start: calc(var(--base-unit) * 4);
+ padding-inline: calc(var(--base-unit) * 6);
+}
+
+.connect-page__network-form__error-message {
+ grid-column: 1 / -1;
+}
diff --git a/devtools/client/aboutdebugging/src/components/connect/NetworkLocationsForm.js b/devtools/client/aboutdebugging/src/components/connect/NetworkLocationsForm.js
new file mode 100644
index 0000000000..347167921b
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/connect/NetworkLocationsForm.js
@@ -0,0 +1,148 @@
+/* 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 Message = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/shared/Message.js")
+);
+
+const Actions = require("resource://devtools/client/aboutdebugging/src/actions/index.js");
+const {
+ MESSAGE_LEVEL,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+const Types = require("resource://devtools/client/aboutdebugging/src/types/index.js");
+
+class NetworkLocationsForm extends PureComponent {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ networkLocations: PropTypes.arrayOf(Types.location).isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ errorHostValue: null,
+ errorMessageId: null,
+ value: "",
+ };
+ }
+
+ onSubmit(e) {
+ const { networkLocations } = this.props;
+ const { value } = this.state;
+
+ e.preventDefault();
+
+ if (!value) {
+ return;
+ }
+
+ if (!value.match(/[^:]+:\d+/)) {
+ this.setState({
+ errorHostValue: value,
+ errorMessageId: "about-debugging-network-location-form-invalid",
+ });
+ return;
+ }
+
+ if (networkLocations.includes(value)) {
+ this.setState({
+ errorHostValue: value,
+ errorMessageId: "about-debugging-network-location-form-duplicate",
+ });
+ return;
+ }
+
+ this.props.dispatch(Actions.addNetworkLocation(value));
+ this.setState({ errorHostValue: null, errorMessageId: null, value: "" });
+ }
+
+ renderError() {
+ const { errorHostValue, errorMessageId } = this.state;
+
+ if (!errorMessageId) {
+ return null;
+ }
+
+ return Message(
+ {
+ className:
+ "connect-page__network-form__error-message " +
+ "qa-connect-page__network-form__error-message",
+ level: MESSAGE_LEVEL.ERROR,
+ isCloseable: true,
+ },
+ Localized(
+ {
+ id: errorMessageId,
+ "$host-value": errorHostValue,
+ },
+ dom.p(
+ {
+ className: "technical-text",
+ },
+ errorMessageId
+ )
+ )
+ );
+ }
+
+ render() {
+ return dom.form(
+ {
+ className: "connect-page__network-form",
+ onSubmit: e => this.onSubmit(e),
+ },
+ this.renderError(),
+ Localized(
+ {
+ id: "about-debugging-network-locations-host-input-label",
+ },
+ dom.label(
+ {
+ htmlFor: "about-debugging-network-locations-host-input",
+ },
+ "Host"
+ )
+ ),
+ dom.input({
+ id: "about-debugging-network-locations-host-input",
+ className: "default-input qa-network-form-input",
+ placeholder: "localhost:6080",
+ type: "text",
+ value: this.state.value,
+ onChange: e => {
+ const value = e.target.value;
+ this.setState({ value });
+ },
+ }),
+ Localized(
+ {
+ id: "about-debugging-network-locations-add-button",
+ },
+ dom.button(
+ {
+ className: "primary-button qa-network-form-submit-button",
+ },
+ "Add"
+ )
+ )
+ );
+ }
+}
+
+module.exports = NetworkLocationsForm;
diff --git a/devtools/client/aboutdebugging/src/components/connect/NetworkLocationsList.css b/devtools/client/aboutdebugging/src/components/connect/NetworkLocationsList.css
new file mode 100644
index 0000000000..e5676b784a
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/connect/NetworkLocationsList.css
@@ -0,0 +1,20 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Layout of a network location item
+ *
+ * +-------------------------------------+---------------+
+ * | Location (eg localhost:8080) | Remove button |
+ * +-------------------------------------+---------------+
+ */
+.network-location {
+ display: grid;
+ grid-template-columns: auto max-content;
+ align-items: center;
+
+ padding-block: calc(var(--base-unit) * 2);
+ padding-inline: calc(var(--base-unit) * 6);
+ border-bottom: 1px solid var(--card-separator-color);
+}
diff --git a/devtools/client/aboutdebugging/src/components/connect/NetworkLocationsList.js b/devtools/client/aboutdebugging/src/components/connect/NetworkLocationsList.js
new file mode 100644
index 0000000000..e680fe525f
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/connect/NetworkLocationsList.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 {
+ 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 Actions = require("resource://devtools/client/aboutdebugging/src/actions/index.js");
+const Types = require("resource://devtools/client/aboutdebugging/src/types/index.js");
+
+class NetworkLocationsList extends PureComponent {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ networkLocations: PropTypes.arrayOf(Types.location).isRequired,
+ };
+ }
+
+ renderList() {
+ return dom.ul(
+ {},
+ this.props.networkLocations.map(location =>
+ dom.li(
+ {
+ className: "network-location qa-network-location",
+ key: location,
+ },
+ dom.span(
+ {
+ className: "ellipsis-text qa-network-location-value",
+ },
+ location
+ ),
+ Localized(
+ {
+ id: "about-debugging-network-locations-remove-button",
+ },
+ dom.button(
+ {
+ className: "default-button qa-network-location-remove-button",
+ onClick: () => {
+ this.props.dispatch(Actions.removeNetworkLocation(location));
+ },
+ },
+ "Remove"
+ )
+ )
+ )
+ )
+ );
+ }
+
+ render() {
+ return this.props.networkLocations.length ? this.renderList() : null;
+ }
+}
+
+module.exports = NetworkLocationsList;
diff --git a/devtools/client/aboutdebugging/src/components/connect/moz.build b/devtools/client/aboutdebugging/src/components/connect/moz.build
new file mode 100644
index 0000000000..9228e80125
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/connect/moz.build
@@ -0,0 +1,11 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ "ConnectPage.js",
+ "ConnectSection.js",
+ "ConnectSteps.js",
+ "NetworkLocationsForm.js",
+ "NetworkLocationsList.js",
+)
diff --git a/devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetItem.css b/devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetItem.css
new file mode 100644
index 0000000000..f049f33b23
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetItem.css
@@ -0,0 +1,97 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * The current layout of debug target item is
+ *
+ * +--------+-----------------------------+----------------+
+ * | | Name | |
+ * | [Icon] |-----------------------------| Action button |
+ * | | Subname | |
+ * +--------+-----------------------------+----------------+
+ * | Detail |
+ * | |
+ * +-------------------------------------------------------+
+ * | Additional actions |
+ * | |
+ * +-------------------------------------------------------+
+ */
+.debug-target-item {
+ display: grid;
+ grid-template-columns: calc(var(--base-unit) * 8) 1fr max-content;
+ grid-template-rows: 1fr minmax(0, auto) auto;
+ grid-column-gap: calc(var(--base-unit) * 2);
+ grid-template-areas: "icon name action"
+ "icon subname action"
+ "detail detail detail"
+ "additional_actions additional_actions additional_actions";
+ margin-block-end: calc(var(--base-unit) * 4);
+
+ padding-block: calc(var(--base-unit) * 3) calc(var(--base-unit) * 2);
+ padding-inline: calc(var(--base-unit) * 3) calc(var(--base-unit) * 2);
+}
+
+.debug-target-item__icon {
+ align-self: center;
+ grid-area: icon;
+ margin-inline-start: calc(var(--base-unit) * 3);
+ width: 100%;
+
+ -moz-context-properties: fill;
+ fill: currentColor;
+}
+
+.debug-target-item__name {
+ align-self: center;
+ grid-area: name;
+ font-size: var(--body-20-font-size);
+ font-weight: var(--body-20-font-weight-bold);
+ line-height: 1.5;
+ margin-inline-start: calc(var(--base-unit) * 3);
+}
+
+.debug-target-item__action {
+ grid-area: action;
+ align-self: center;
+ margin-inline-end: calc(var(--base-unit) * 2);
+}
+
+.debug-target-item__additional_actions {
+ grid-area: additional_actions;
+ border-top: 1px solid var(--card-separator-color);
+ margin-block-start: calc(var(--base-unit) * 2);
+ padding-block-start: calc(var(--base-unit) * 2);
+ padding-inline-end: calc(var(--base-unit) * 2);
+}
+
+.debug-target-item__detail {
+ grid-area: detail;
+ margin-block-start: calc(var(--base-unit) * 3);
+}
+
+.debug-target-item__detail--empty {
+ margin-block-start: var(--base-unit);
+}
+
+.debug-target-item__messages {
+ margin-inline: calc(var(--base-unit) * 3) calc(var(--base-unit) * 2);
+}
+
+.debug-target-item__subname {
+ grid-area: subname;
+ color: var(--secondary-text-color);
+ font-size: var(--caption-20-font-size);
+ font-weight: var(--caption-20-font-weight);
+ line-height: 1.5;
+}
+
+/* The subname is always LTR under the Tabs section,
+ so check its parent's direction to set the correct margin. */
+.debug-target-item:dir(ltr) > .debug-target-item__subname {
+ margin-left: calc(var(--base-unit) * 3);
+}
+
+.debug-target-item:dir(rtl) > .debug-target-item__subname {
+ margin-right: calc(var(--base-unit) * 3);
+}
diff --git a/devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetItem.js b/devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetItem.js
new file mode 100644
index 0000000000..6f19d7be02
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetItem.js
@@ -0,0 +1,91 @@
+/* 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/aboutdebugging/src/types/index.js");
+
+/**
+ * This component displays debug target.
+ */
+class DebugTargetItem extends PureComponent {
+ static get propTypes() {
+ return {
+ actionComponent: PropTypes.any.isRequired,
+ additionalActionsComponent: PropTypes.any,
+ detailComponent: PropTypes.any.isRequired,
+ dispatch: PropTypes.func.isRequired,
+ target: Types.debugTarget.isRequired,
+ };
+ }
+
+ renderAction() {
+ const { actionComponent, dispatch, target } = this.props;
+ return dom.div(
+ {
+ className: "debug-target-item__action",
+ },
+ actionComponent({ dispatch, target })
+ );
+ }
+
+ renderAdditionalActions() {
+ const { additionalActionsComponent, dispatch, target } = this.props;
+
+ if (!additionalActionsComponent) {
+ return null;
+ }
+
+ return dom.section(
+ {
+ className: "debug-target-item__additional_actions",
+ },
+ additionalActionsComponent({ dispatch, target })
+ );
+ }
+
+ renderDetail() {
+ const { detailComponent, target } = this.props;
+ return detailComponent({ target });
+ }
+
+ renderIcon() {
+ return dom.img({
+ className: "debug-target-item__icon qa-debug-target-item-icon",
+ src: this.props.target.icon,
+ });
+ }
+
+ renderName() {
+ return dom.span(
+ {
+ className: "debug-target-item__name ellipsis-text",
+ title: this.props.target.name,
+ },
+ this.props.target.name
+ );
+ }
+
+ render() {
+ return dom.li(
+ {
+ className: "card debug-target-item qa-debug-target-item",
+ "data-qa-target-type": this.props.target.type,
+ },
+ this.renderIcon(),
+ this.renderName(),
+ this.renderAction(),
+ this.renderDetail(),
+ this.renderAdditionalActions()
+ );
+ }
+}
+
+module.exports = DebugTargetItem;
diff --git a/devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetList.css b/devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetList.css
new file mode 100644
index 0000000000..827983e2bf
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetList.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/. */
+
+.debug-target-list {
+ margin-block-start: calc(var(--base-unit) * 4);
+}
diff --git a/devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetList.js b/devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetList.js
new file mode 100644
index 0000000000..ce1e7ff12c
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetList.js
@@ -0,0 +1,80 @@
+/* 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 DebugTargetItem = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetItem.js")
+);
+
+const Types = require("resource://devtools/client/aboutdebugging/src/types/index.js");
+
+/**
+ * This component displays list of debug target.
+ */
+class DebugTargetList extends PureComponent {
+ static get propTypes() {
+ return {
+ actionComponent: PropTypes.any.isRequired,
+ additionalActionsComponent: PropTypes.any,
+ detailComponent: PropTypes.any.isRequired,
+ dispatch: PropTypes.func.isRequired,
+ targets: PropTypes.arrayOf(Types.debugTarget).isRequired,
+ };
+ }
+
+ renderEmptyList() {
+ return Localized(
+ {
+ id: "about-debugging-debug-target-list-empty",
+ },
+ dom.p(
+ {
+ className: "qa-debug-target-list-empty",
+ },
+ "Nothing yet."
+ )
+ );
+ }
+
+ render() {
+ const {
+ actionComponent,
+ additionalActionsComponent,
+ detailComponent,
+ dispatch,
+ targets,
+ } = this.props;
+
+ return targets.length === 0
+ ? this.renderEmptyList()
+ : dom.ul(
+ {
+ className: "debug-target-list qa-debug-target-list",
+ },
+ targets.map((target, key) =>
+ DebugTargetItem({
+ actionComponent,
+ additionalActionsComponent,
+ detailComponent,
+ dispatch,
+ key,
+ target,
+ })
+ )
+ );
+ }
+}
+
+module.exports = DebugTargetList;
diff --git a/devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetPane.css b/devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetPane.css
new file mode 100644
index 0000000000..616b4ac28c
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetPane.css
@@ -0,0 +1,43 @@
+/* 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/. */
+
+/*
+ * Style for the heading of a debug target pane
+ * +-----------------+---------------+-----------------+
+ * | [Category icon] | Category name | [Collapse icon] |
+ * +-----------------+---------------+-----------------+
+ */
+.debug-target-pane__heading {
+ grid-template-columns: var(--main-subheading-icon-size) max-content calc(var(--base-unit) * 3);
+ user-select: none;
+}
+
+.debug-target-pane__icon {
+ transition: transform 150ms cubic-bezier(.07, .95, 0, 1);
+ transform: rotate(90deg);
+}
+
+.debug-target-pane__icon--collapsed {
+ transform: rotate(0deg);
+}
+
+.debug-target-pane__icon--collapsed:dir(rtl) {
+ transform: rotate(180deg);
+}
+
+.debug-target-pane__title {
+ cursor: pointer;
+}
+
+.debug-target-pane__collapsable {
+ overflow: hidden;
+ /* padding will give space for card shadow to appear and
+ margin will correct the alignment */
+ margin-inline: calc(var(--card-shadow-blur-radius) * -1);
+ padding-inline: var(--card-shadow-blur-radius);
+}
+
+.debug-target-pane__collapsable--collapsed {
+ max-height: 0;
+}
diff --git a/devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetPane.js b/devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetPane.js
new file mode 100644
index 0000000000..abfa1042b8
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetPane.js
@@ -0,0 +1,147 @@
+/* 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,
+ createRef,
+ 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 DebugTargetList = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/debugtarget/DebugTargetList.js")
+);
+
+const Actions = require("resource://devtools/client/aboutdebugging/src/actions/index.js");
+const Types = require("resource://devtools/client/aboutdebugging/src/types/index.js");
+
+/**
+ * This component provides list for debug target and name area.
+ */
+class DebugTargetPane extends PureComponent {
+ static get propTypes() {
+ return {
+ actionComponent: PropTypes.any.isRequired,
+ additionalActionsComponent: PropTypes.any,
+ children: PropTypes.node,
+ collapsibilityKey: PropTypes.string.isRequired,
+ detailComponent: PropTypes.any.isRequired,
+ dispatch: PropTypes.func.isRequired,
+ // Provided by wrapping the component with FluentReact.withLocalization.
+ getString: PropTypes.func.isRequired,
+ icon: PropTypes.string.isRequired,
+ isCollapsed: PropTypes.bool.isRequired,
+ name: PropTypes.string.isRequired,
+ targets: PropTypes.arrayOf(Types.debugTarget).isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.collapsableRef = createRef();
+ }
+
+ componentDidUpdate(prevProps, prevState, snapshot) {
+ if (snapshot === null) {
+ return;
+ }
+
+ const el = this.collapsableRef.current;
+
+ // Cancel existing animation which is collapsing/expanding.
+ for (const animation of el.getAnimations()) {
+ animation.cancel();
+ }
+
+ el.animate(
+ { maxHeight: [`${snapshot}px`, `${el.clientHeight}px`] },
+ { duration: 150, easing: "cubic-bezier(.07, .95, 0, 1)" }
+ );
+ }
+
+ getSnapshotBeforeUpdate(prevProps) {
+ if (this.props.isCollapsed !== prevProps.isCollapsed) {
+ return this.collapsableRef.current.clientHeight;
+ }
+
+ return null;
+ }
+
+ toggleCollapsibility() {
+ const { collapsibilityKey, dispatch, isCollapsed } = this.props;
+ dispatch(
+ Actions.updateDebugTargetCollapsibility(collapsibilityKey, !isCollapsed)
+ );
+ }
+
+ render() {
+ const {
+ actionComponent,
+ additionalActionsComponent,
+ children,
+ detailComponent,
+ dispatch,
+ getString,
+ icon,
+ isCollapsed,
+ name,
+ targets,
+ } = this.props;
+
+ const title = getString("about-debugging-collapse-expand-debug-targets");
+
+ return dom.section(
+ {
+ className: "qa-debug-target-pane",
+ },
+ dom.a(
+ {
+ className:
+ "undecorated-link debug-target-pane__title " +
+ "qa-debug-target-pane-title",
+ title,
+ onClick: e => this.toggleCollapsibility(),
+ },
+ dom.h2(
+ { className: "main-subheading debug-target-pane__heading" },
+ dom.img({
+ className: "main-subheading__icon",
+ src: icon,
+ }),
+ `${name} (${targets.length})`,
+ dom.img({
+ className:
+ "main-subheading__icon debug-target-pane__icon" +
+ (isCollapsed ? " debug-target-pane__icon--collapsed" : ""),
+ src: "chrome://devtools/skin/images/arrow-e.svg",
+ })
+ )
+ ),
+ dom.div(
+ {
+ className:
+ "debug-target-pane__collapsable qa-debug-target-pane__collapsable" +
+ (isCollapsed ? " debug-target-pane__collapsable--collapsed" : ""),
+ ref: this.collapsableRef,
+ },
+ children,
+ DebugTargetList({
+ actionComponent,
+ additionalActionsComponent,
+ detailComponent,
+ dispatch,
+ isCollapsed,
+ targets,
+ })
+ )
+ );
+ }
+}
+
+module.exports = FluentReact.withLocalization(DebugTargetPane);
diff --git a/devtools/client/aboutdebugging/src/components/debugtarget/ExtensionDetail.css b/devtools/client/aboutdebugging/src/components/debugtarget/ExtensionDetail.css
new file mode 100644
index 0000000000..6a4befa76a
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/debugtarget/ExtensionDetail.css
@@ -0,0 +1,27 @@
+/* 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/. */
+
+.extension-backgroundscript {
+ display: flex;
+ column-gap: calc(var(--base-unit) * 2);
+}
+
+.extension-backgroundscript__status {
+ display: flex;
+ align-items: center;
+ float: inline-end;
+}
+
+.extension-backgroundscript__status::before {
+ background-color: var(--grey-50);
+ border-radius: 100%;
+ content: "";
+ height: calc(var(--base-unit) * 2);
+ margin-inline-end: var(--base-unit);
+ width: calc(var(--base-unit) * 2);
+}
+
+.extension-backgroundscript__status--running::before {
+ background-color: var(--success-background);
+}
diff --git a/devtools/client/aboutdebugging/src/components/debugtarget/ExtensionDetail.js b/devtools/client/aboutdebugging/src/components/debugtarget/ExtensionDetail.js
new file mode 100644
index 0000000000..ef14483723
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/debugtarget/ExtensionDetail.js
@@ -0,0 +1,243 @@
+/* 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 {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const {
+ getCurrentRuntimeDetails,
+} = require("resource://devtools/client/aboutdebugging/src/modules/runtimes-state-helper.js");
+
+const DetailsLog = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/shared/DetailsLog.js")
+);
+const FieldPair = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/debugtarget/FieldPair.js")
+);
+const Message = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/shared/Message.js")
+);
+
+const {
+ EXTENSION_BGSCRIPT_STATUSES,
+ MESSAGE_LEVEL,
+ RUNTIMES,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+const Types = require("resource://devtools/client/aboutdebugging/src/types/index.js");
+
+/**
+ * This component displays detail information for extension.
+ */
+class ExtensionDetail extends PureComponent {
+ static get propTypes() {
+ return {
+ children: PropTypes.node,
+ // Provided by wrapping the component with FluentReact.withLocalization.
+ getString: PropTypes.func.isRequired,
+ // Provided by redux state
+ runtimeDetails: Types.runtimeDetails.isRequired,
+ target: Types.debugTarget.isRequired,
+ };
+ }
+
+ renderWarnings() {
+ const { warnings } = this.props.target.details;
+
+ if (!warnings.length) {
+ return null;
+ }
+
+ return dom.section(
+ {
+ className: "debug-target-item__messages",
+ },
+ warnings.map((warning, index) => {
+ return Message(
+ {
+ level: MESSAGE_LEVEL.WARNING,
+ isCloseable: true,
+ key: `warning-${index}`,
+ },
+ DetailsLog(
+ {
+ type: MESSAGE_LEVEL.WARNING,
+ },
+ dom.p(
+ {
+ className: "technical-text",
+ },
+ warning
+ )
+ )
+ );
+ })
+ );
+ }
+
+ renderUUID() {
+ const { uuid } = this.props.target.details;
+ if (!uuid) {
+ return null;
+ }
+
+ return Localized(
+ {
+ id: "about-debugging-extension-uuid",
+ attrs: { label: true },
+ },
+ FieldPair({
+ label: "Internal UUID",
+ value: uuid,
+ })
+ );
+ }
+
+ renderExtensionId() {
+ const { id } = this.props.target;
+
+ return Localized(
+ {
+ id: "about-debugging-extension-id",
+ attrs: { label: true },
+ },
+ FieldPair({
+ label: "Extension ID",
+ value: id,
+ })
+ );
+ }
+
+ renderLocation() {
+ const { location } = this.props.target.details;
+ if (!location) {
+ return null;
+ }
+
+ return Localized(
+ {
+ id: "about-debugging-extension-location",
+ attrs: { label: true },
+ },
+ FieldPair({
+ label: "Location",
+ value: location,
+ })
+ );
+ }
+
+ renderManifest() {
+ // Manifest links are only relevant when debugging the current Firefox
+ // instance.
+ if (this.props.runtimeDetails.info.type !== RUNTIMES.THIS_FIREFOX) {
+ return null;
+ }
+
+ const { manifestURL } = this.props.target.details;
+ const link = dom.a(
+ {
+ className: "qa-manifest-url",
+ href: manifestURL,
+ target: "_blank",
+ },
+ manifestURL
+ );
+
+ return Localized(
+ {
+ id: "about-debugging-extension-manifest-url",
+ attrs: { label: true },
+ },
+ FieldPair({
+ label: "Manifest URL",
+ value: link,
+ })
+ );
+ }
+
+ renderBackgroundScriptStatus() {
+ // The status of the background script is only relevant if it is
+ // not persistent.
+ const { persistentBackgroundScript } = this.props.target.details;
+ if (!(persistentBackgroundScript === false)) {
+ return null;
+ }
+
+ const { backgroundScriptStatus } = this.props.target.details;
+
+ let status;
+ let statusLocalizationId;
+ let statusClassName;
+
+ if (backgroundScriptStatus === EXTENSION_BGSCRIPT_STATUSES.RUNNING) {
+ status = `extension-backgroundscript__status--running`;
+ statusLocalizationId = `about-debugging-extension-backgroundscript-status-running`;
+ statusClassName = `extension-backgroundscript__status--running`;
+ } else {
+ status = `extension-backgroundscript__status--stopped`;
+ statusLocalizationId = `about-debugging-extension-backgroundscript-status-stopped`;
+ statusClassName = `extension-backgroundscript__status--stopped`;
+ }
+
+ return Localized(
+ {
+ id: "about-debugging-extension-backgroundscript",
+ attrs: { label: true },
+ },
+ FieldPair({
+ label: "Background Script",
+ value: Localized(
+ {
+ id: statusLocalizationId,
+ },
+ dom.span(
+ {
+ className: `extension-backgroundscript__status qa-extension-backgroundscript-status ${statusClassName}`,
+ },
+ status
+ )
+ ),
+ })
+ );
+ }
+
+ render() {
+ return dom.section(
+ {
+ className: "debug-target-item__detail",
+ },
+ this.renderWarnings(),
+ dom.dl(
+ {},
+ this.renderLocation(),
+ this.renderExtensionId(),
+ this.renderUUID(),
+ this.renderManifest(),
+ this.renderBackgroundScriptStatus(),
+ this.props.children
+ )
+ );
+ }
+}
+
+const mapStateToProps = state => {
+ return {
+ runtimeDetails: getCurrentRuntimeDetails(state.runtimes),
+ };
+};
+
+module.exports = FluentReact.withLocalization(
+ connect(mapStateToProps)(ExtensionDetail)
+);
diff --git a/devtools/client/aboutdebugging/src/components/debugtarget/FieldPair.css b/devtools/client/aboutdebugging/src/components/debugtarget/FieldPair.css
new file mode 100644
index 0000000000..a0b290d2a3
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/debugtarget/FieldPair.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/. */
+
+.fieldpair {
+ display: grid;
+ grid-template-columns: auto auto;
+ border-top: 1px solid var(--card-separator-color);
+ padding-block: calc(var(--base-unit) * 2);
+ padding-inline: calc(var(--base-unit) * 4) calc(var(--base-unit) * 2);
+}
+
+.fieldpair:last-child {
+ padding-block-end: 0;
+}
+
+.fieldpair__title {
+ margin-inline-end: var(--base-unit);
+ font-size: var(--caption-20-font-size);
+ font-weight: var(--caption-20-font-weight);
+}
+
+.fieldpair__description {
+ color: var(--fieldpair-text-color);
+ flex: 1;
+ font-size: var(--caption-20-font-size);
+ font-weight: var(--caption-20-font-weight);
+ text-align: end;
+}
diff --git a/devtools/client/aboutdebugging/src/components/debugtarget/FieldPair.js b/devtools/client/aboutdebugging/src/components/debugtarget/FieldPair.js
new file mode 100644
index 0000000000..5e7ae53c65
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/debugtarget/FieldPair.js
@@ -0,0 +1,49 @@
+/* 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");
+
+/* Renders a pair of `<dt>` (label) + `<dd>` (value) field. */
+class FieldPair extends PureComponent {
+ static get propTypes() {
+ return {
+ className: PropTypes.string,
+ label: PropTypes.node.isRequired,
+ value: PropTypes.node,
+ };
+ }
+
+ render() {
+ const { label, value } = this.props;
+ return dom.div(
+ {
+ className: "fieldpair",
+ },
+ dom.dt(
+ {
+ className:
+ "fieldpair__title " +
+ (this.props.className ? this.props.className : ""),
+ },
+ label
+ ),
+ value
+ ? dom.dd(
+ {
+ className: "fieldpair__description ellipsis-text",
+ },
+ value
+ )
+ : null
+ );
+ }
+}
+
+module.exports = FieldPair;
diff --git a/devtools/client/aboutdebugging/src/components/debugtarget/InspectAction.js b/devtools/client/aboutdebugging/src/components/debugtarget/InspectAction.js
new file mode 100644
index 0000000000..f7aff438a4
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/debugtarget/InspectAction.js
@@ -0,0 +1,58 @@
+/* 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 Actions = require("resource://devtools/client/aboutdebugging/src/actions/index.js");
+const Types = require("resource://devtools/client/aboutdebugging/src/types/index.js");
+
+/**
+ * This component provides inspect button.
+ */
+class InspectAction extends PureComponent {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ disabled: PropTypes.bool,
+ target: Types.debugTarget.isRequired,
+ title: PropTypes.string,
+ };
+ }
+
+ inspect() {
+ const { dispatch, target } = this.props;
+ dispatch(Actions.inspectDebugTarget(target.type, target.id));
+ }
+
+ render() {
+ const { disabled, title } = this.props;
+
+ return Localized(
+ {
+ id: "about-debugging-debug-target-inspect-button",
+ },
+ dom.button(
+ {
+ onClick: e => this.inspect(),
+ className: "default-button qa-debug-target-inspect-button",
+ disabled,
+ title,
+ },
+ "Inspect"
+ )
+ );
+ }
+}
+
+module.exports = InspectAction;
diff --git a/devtools/client/aboutdebugging/src/components/debugtarget/ProcessDetail.js b/devtools/client/aboutdebugging/src/components/debugtarget/ProcessDetail.js
new file mode 100644
index 0000000000..a9973e90e3
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/debugtarget/ProcessDetail.js
@@ -0,0 +1,32 @@
+/* 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 Types = require("resource://devtools/client/aboutdebugging/src/types/index.js");
+
+/**
+ * This component displays detail information for a process.
+ */
+class ProcessDetail extends PureComponent {
+ static get propTypes() {
+ return {
+ target: Types.debugTarget.isRequired,
+ };
+ }
+
+ render() {
+ const { description } = this.props.target.details;
+ return dom.p(
+ { className: "debug-target-item__subname ellipsis-text" },
+ description
+ );
+ }
+}
+
+module.exports = ProcessDetail;
diff --git a/devtools/client/aboutdebugging/src/components/debugtarget/ServiceWorkerAction.css b/devtools/client/aboutdebugging/src/components/debugtarget/ServiceWorkerAction.css
new file mode 100644
index 0000000000..c3ac2dc872
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/debugtarget/ServiceWorkerAction.css
@@ -0,0 +1,26 @@
+/* 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/. */
+
+.service-worker-action {
+ display: flex;
+ column-gap: calc(var(--base-unit) * 2);
+}
+
+.service-worker-action__status {
+ display: flex;
+ align-items: center;
+}
+
+.service-worker-action__status::before {
+ background-color: var(--grey-50);
+ border-radius: 100%;
+ content: "";
+ height: calc(var(--base-unit) * 2);
+ margin-inline-end: var(--base-unit);
+ width: calc(var(--base-unit) * 2);
+}
+
+.service-worker-action__status--running::before {
+ background-color: var(--success-background);
+}
diff --git a/devtools/client/aboutdebugging/src/components/debugtarget/ServiceWorkerAction.js b/devtools/client/aboutdebugging/src/components/debugtarget/ServiceWorkerAction.js
new file mode 100644
index 0000000000..38aede94f4
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/debugtarget/ServiceWorkerAction.js
@@ -0,0 +1,124 @@
+/* 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 {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const {
+ getCurrentRuntimeDetails,
+} = require("resource://devtools/client/aboutdebugging/src/modules/runtimes-state-helper.js");
+
+const InspectAction = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/debugtarget/InspectAction.js")
+);
+
+const Types = require("resource://devtools/client/aboutdebugging/src/types/index.js");
+const {
+ SERVICE_WORKER_STATUSES,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+/**
+ * This component displays buttons for service worker.
+ */
+class ServiceWorkerAction extends PureComponent {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ // Provided by redux state
+ runtimeDetails: Types.runtimeDetails.isRequired,
+ target: Types.debugTarget.isRequired,
+ };
+ }
+
+ _renderInspectAction() {
+ const { status } = this.props.target.details;
+ const shallRenderInspectAction =
+ status === SERVICE_WORKER_STATUSES.RUNNING ||
+ status === SERVICE_WORKER_STATUSES.REGISTERING;
+
+ if (!shallRenderInspectAction) {
+ return null;
+ }
+
+ const { canDebugServiceWorkers } = this.props.runtimeDetails;
+ return Localized(
+ {
+ id: "about-debugging-worker-inspect-action-disabled",
+ attrs: {
+ // Show an explanatory title only if the action is disabled.
+ title: !canDebugServiceWorkers,
+ },
+ },
+ InspectAction({
+ disabled: !canDebugServiceWorkers,
+ dispatch: this.props.dispatch,
+ target: this.props.target,
+ })
+ );
+ }
+
+ _getStatusLocalizationId(status) {
+ switch (status) {
+ case SERVICE_WORKER_STATUSES.REGISTERING.toLowerCase():
+ return "about-debugging-worker-status-registering";
+ case SERVICE_WORKER_STATUSES.RUNNING.toLowerCase():
+ return "about-debugging-worker-status-running";
+ case SERVICE_WORKER_STATUSES.STOPPED.toLowerCase():
+ return "about-debugging-worker-status-stopped";
+ default:
+ // Assume status is stopped for unknown status value.
+ return "about-debugging-worker-status-stopped";
+ }
+ }
+
+ _renderStatus() {
+ const status = this.props.target.details.status.toLowerCase();
+ const statusClassName =
+ status === SERVICE_WORKER_STATUSES.RUNNING.toLowerCase()
+ ? "service-worker-action__status--running"
+ : "";
+
+ return Localized(
+ {
+ id: this._getStatusLocalizationId(status),
+ },
+ dom.span(
+ {
+ className: `service-worker-action__status qa-worker-status ${statusClassName}`,
+ },
+ status
+ )
+ );
+ }
+
+ render() {
+ return dom.div(
+ {
+ className: "service-worker-action",
+ },
+ this._renderStatus(),
+ this._renderInspectAction()
+ );
+ }
+}
+
+const mapStateToProps = state => {
+ return {
+ runtimeDetails: getCurrentRuntimeDetails(state.runtimes),
+ };
+};
+
+module.exports = connect(mapStateToProps)(ServiceWorkerAction);
diff --git a/devtools/client/aboutdebugging/src/components/debugtarget/ServiceWorkerAdditionalActions.js b/devtools/client/aboutdebugging/src/components/debugtarget/ServiceWorkerAdditionalActions.js
new file mode 100644
index 0000000000..38262ad511
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/debugtarget/ServiceWorkerAdditionalActions.js
@@ -0,0 +1,176 @@
+/* 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 {
+ connect,
+} = require("resource://devtools/client/shared/vendor/react-redux.js");
+
+const FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const {
+ getCurrentRuntimeDetails,
+} = require("resource://devtools/client/aboutdebugging/src/modules/runtimes-state-helper.js");
+
+const Actions = require("resource://devtools/client/aboutdebugging/src/actions/index.js");
+const Types = require("resource://devtools/client/aboutdebugging/src/types/index.js");
+const {
+ SERVICE_WORKER_STATUSES,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+/**
+ * The main purpose of this component is to expose a meaningful prop
+ * disabledTitle that can be used with fluent localization.
+ */
+class _ActionButton extends PureComponent {
+ static get propTypes() {
+ return {
+ children: PropTypes.node,
+ className: PropTypes.string.isRequired,
+ disabled: PropTypes.bool,
+ disabledTitle: PropTypes.string,
+ onClick: PropTypes.func.isRequired,
+ };
+ }
+
+ render() {
+ const { className, disabled, disabledTitle, onClick } = this.props;
+ return dom.button(
+ {
+ className,
+ disabled,
+ onClick: e => onClick(),
+ title: disabled && disabledTitle ? disabledTitle : undefined,
+ },
+ this.props.children
+ );
+ }
+}
+const ActionButtonFactory = createFactory(_ActionButton);
+
+/**
+ * This component displays buttons for service worker.
+ */
+class ServiceWorkerAdditionalActions extends PureComponent {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ // Provided by wrapping the component with FluentReact.withLocalization.
+ getString: PropTypes.func.isRequired,
+ // Provided by redux state
+ runtimeDetails: Types.runtimeDetails.isRequired,
+ target: Types.debugTarget.isRequired,
+ };
+ }
+
+ push() {
+ const { dispatch, target } = this.props;
+ dispatch(
+ Actions.pushServiceWorker(target.id, target.details.registrationFront)
+ );
+ }
+
+ start() {
+ const { dispatch, target } = this.props;
+ dispatch(Actions.startServiceWorker(target.details.registrationFront));
+ }
+
+ unregister() {
+ const { dispatch, target } = this.props;
+ dispatch(Actions.unregisterServiceWorker(target.details.registrationFront));
+ }
+
+ _renderButton({ className, disabled, key, labelId, onClick }) {
+ return Localized(
+ {
+ id: labelId,
+ attrs: {
+ disabledTitle: !!disabled,
+ },
+ key,
+ },
+ ActionButtonFactory(
+ {
+ className,
+ disabled,
+ onClick: e => onClick(),
+ },
+ labelId
+ )
+ );
+ }
+
+ _renderPushButton() {
+ return this._renderButton({
+ className: "default-button default-button--micro qa-push-button",
+ disabled: !this.props.runtimeDetails.canDebugServiceWorkers,
+ key: "service-worker-push-button",
+ labelId: "about-debugging-worker-action-push2",
+ onClick: this.push.bind(this),
+ });
+ }
+
+ _renderStartButton() {
+ return this._renderButton({
+ className: "default-button default-button--micro qa-start-button",
+ disabled: !this.props.runtimeDetails.canDebugServiceWorkers,
+ key: "service-worker-start-button",
+ labelId: "about-debugging-worker-action-start2",
+ onClick: this.start.bind(this),
+ });
+ }
+
+ _renderUnregisterButton() {
+ return this._renderButton({
+ className: "default-button default-button--micro qa-unregister-button",
+ key: "service-worker-unregister-button",
+ labelId: "about-debugging-worker-action-unregister",
+ disabled: false,
+ onClick: this.unregister.bind(this),
+ });
+ }
+
+ _renderActionButtons() {
+ const { status } = this.props.target.details;
+
+ switch (status) {
+ case SERVICE_WORKER_STATUSES.RUNNING:
+ return [this._renderUnregisterButton(), this._renderPushButton()];
+ case SERVICE_WORKER_STATUSES.REGISTERING:
+ return null;
+ case SERVICE_WORKER_STATUSES.STOPPED:
+ return [this._renderUnregisterButton(), this._renderStartButton()];
+ default:
+ console.error("Unexpected service worker status: " + status);
+ return null;
+ }
+ }
+
+ render() {
+ return dom.div(
+ {
+ className: "toolbar toolbar--right-align",
+ },
+ this._renderActionButtons()
+ );
+ }
+}
+
+const mapStateToProps = state => {
+ return {
+ runtimeDetails: getCurrentRuntimeDetails(state.runtimes),
+ };
+};
+
+module.exports = FluentReact.withLocalization(
+ connect(mapStateToProps)(ServiceWorkerAdditionalActions)
+);
diff --git a/devtools/client/aboutdebugging/src/components/debugtarget/TabAction.js b/devtools/client/aboutdebugging/src/components/debugtarget/TabAction.js
new file mode 100644
index 0000000000..132f1e5f44
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/debugtarget/TabAction.js
@@ -0,0 +1,52 @@
+/* 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 FluentReact = require("resource://devtools/client/shared/vendor/fluent-react.js");
+const Localized = createFactory(FluentReact.Localized);
+
+const InspectAction = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/debugtarget/InspectAction.js")
+);
+
+const Types = require("resource://devtools/client/aboutdebugging/src/types/index.js");
+
+/**
+ * This component displays the inspect button for tabs.
+ */
+class TabAction extends PureComponent {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ target: Types.debugTarget.isRequired,
+ };
+ }
+
+ render() {
+ const isZombieTab = this.props.target.details.isZombieTab;
+ return Localized(
+ {
+ id: "about-debugging-zombie-tab-inspect-action-disabled",
+ attrs: {
+ // Show an explanatory title only if the action is disabled.
+ title: isZombieTab,
+ },
+ },
+ InspectAction({
+ disabled: isZombieTab,
+ dispatch: this.props.dispatch,
+ target: this.props.target,
+ })
+ );
+ }
+}
+
+module.exports = FluentReact.withLocalization(TabAction);
diff --git a/devtools/client/aboutdebugging/src/components/debugtarget/TabDetail.js b/devtools/client/aboutdebugging/src/components/debugtarget/TabDetail.js
new file mode 100644
index 0000000000..dcbd3f0a4d
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/debugtarget/TabDetail.js
@@ -0,0 +1,34 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"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 Types = require("resource://devtools/client/aboutdebugging/src/types/index.js");
+
+/**
+ * This component displays detail information for tab.
+ */
+class TabDetail extends PureComponent {
+ static get propTypes() {
+ return {
+ target: Types.debugTarget.isRequired,
+ };
+ }
+
+ render() {
+ return dom.div(
+ {
+ className: "debug-target-item__subname ellipsis-text",
+ dir: "ltr",
+ },
+ this.props.target.details.url
+ );
+ }
+}
+
+module.exports = TabDetail;
diff --git a/devtools/client/aboutdebugging/src/components/debugtarget/TemporaryExtensionAdditionalActions.js b/devtools/client/aboutdebugging/src/components/debugtarget/TemporaryExtensionAdditionalActions.js
new file mode 100644
index 0000000000..44b7d3e167
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/debugtarget/TemporaryExtensionAdditionalActions.js
@@ -0,0 +1,182 @@
+/* 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 Actions = require("resource://devtools/client/aboutdebugging/src/actions/index.js");
+const Types = require("resource://devtools/client/aboutdebugging/src/types/index.js");
+
+const DetailsLog = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/shared/DetailsLog.js")
+);
+const Message = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/shared/Message.js")
+);
+const {
+ MESSAGE_LEVEL,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+/**
+ * This component provides components that reload/remove temporary extension.
+ */
+class TemporaryExtensionAdditionalActions extends PureComponent {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ target: Types.debugTarget.isRequired,
+ };
+ }
+
+ reload() {
+ const { dispatch, target } = this.props;
+ dispatch(Actions.reloadTemporaryExtension(target.id));
+ }
+
+ terminateBackgroundScript() {
+ const { dispatch, target } = this.props;
+ dispatch(Actions.terminateExtensionBackgroundScript(target.id));
+ }
+
+ remove() {
+ const { dispatch, target } = this.props;
+ dispatch(Actions.removeTemporaryExtension(target.id));
+ }
+
+ renderReloadError() {
+ const { reloadError } = this.props.target.details;
+
+ if (!reloadError) {
+ return null;
+ }
+
+ return Message(
+ {
+ className: "qa-temporary-extension-reload-error",
+ level: MESSAGE_LEVEL.ERROR,
+ key: "reload-error",
+ },
+ DetailsLog(
+ {
+ type: MESSAGE_LEVEL.ERROR,
+ },
+ dom.p(
+ {
+ className: "technical-text",
+ },
+ reloadError
+ )
+ )
+ );
+ }
+
+ renderTerminateBackgroundScriptError() {
+ const { lastTerminateBackgroundScriptError } = this.props.target.details;
+
+ if (!lastTerminateBackgroundScriptError) {
+ return null;
+ }
+
+ return Message(
+ {
+ className: "qa-temporary-extension-terminate-backgroundscript-error",
+ level: MESSAGE_LEVEL.ERROR,
+ key: "terminate-backgroundscript-error",
+ },
+ DetailsLog(
+ {
+ type: MESSAGE_LEVEL.ERROR,
+ },
+ dom.p(
+ {
+ className: "technical-text",
+ },
+ lastTerminateBackgroundScriptError
+ )
+ )
+ );
+ }
+
+ renderTerminateBackgroundScriptButton() {
+ const { persistentBackgroundScript } = this.props.target.details;
+
+ // For extensions with a non persistent background script
+ // also include a "terminate background script" action.
+ if (persistentBackgroundScript !== false) {
+ return null;
+ }
+
+ return Localized(
+ {
+ id: "about-debugging-tmp-extension-terminate-bgscript-button",
+ },
+ dom.button(
+ {
+ className:
+ "default-button default-button--micro " +
+ "qa-temporary-extension-terminate-bgscript-button",
+ onClick: e => this.terminateBackgroundScript(),
+ },
+ "Terminate Background Script"
+ )
+ );
+ }
+
+ renderRemoveButton() {
+ return Localized(
+ {
+ id: "about-debugging-tmp-extension-remove-button",
+ },
+ dom.button(
+ {
+ className:
+ "default-button default-button--micro " +
+ "qa-temporary-extension-remove-button",
+ onClick: e => this.remove(),
+ },
+ "Remove"
+ )
+ );
+ }
+
+ render() {
+ return [
+ dom.div(
+ {
+ className: "toolbar toolbar--right-align",
+ key: "actions",
+ },
+ this.renderTerminateBackgroundScriptButton(),
+ Localized(
+ {
+ id: "about-debugging-tmp-extension-reload-button",
+ },
+ dom.button(
+ {
+ className:
+ "default-button default-button--micro " +
+ "qa-temporary-extension-reload-button",
+ onClick: e => this.reload(),
+ },
+ "Reload"
+ )
+ ),
+ this.renderRemoveButton()
+ ),
+ this.renderReloadError(),
+ this.renderTerminateBackgroundScriptError(),
+ ];
+ }
+}
+
+module.exports = TemporaryExtensionAdditionalActions;
diff --git a/devtools/client/aboutdebugging/src/components/debugtarget/TemporaryExtensionDetail.js b/devtools/client/aboutdebugging/src/components/debugtarget/TemporaryExtensionDetail.js
new file mode 100644
index 0000000000..6c589472d6
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/debugtarget/TemporaryExtensionDetail.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 {
+ 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 ExtensionDetail = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/debugtarget/ExtensionDetail.js")
+);
+const FieldPair = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/debugtarget/FieldPair.js")
+);
+
+const Types = require("resource://devtools/client/aboutdebugging/src/types/index.js");
+
+const TEMP_ID_DOC_URL =
+ "https://developer.mozilla.org/Add-ons/WebExtensions/WebExtensions_and_the_Add-on_ID";
+
+/**
+ * This component displays detail information for a temporary extension.
+ */
+class TemporaryExtensionDetail extends PureComponent {
+ static get propTypes() {
+ return {
+ // Provided by wrapping the component with FluentReact.withLocalization.
+ getString: PropTypes.func.isRequired,
+ target: Types.debugTarget.isRequired,
+ };
+ }
+
+ renderTemporaryIdMessage() {
+ return Localized(
+ {
+ id: "about-debugging-tmp-extension-temporary-id",
+ a: dom.a({
+ className: "qa-temporary-id-link",
+ href: TEMP_ID_DOC_URL,
+ target: "_blank",
+ }),
+ },
+ dom.div({
+ className: "qa-temporary-id-message",
+ })
+ );
+ }
+
+ render() {
+ return ExtensionDetail(
+ {
+ target: this.props.target,
+ },
+ FieldPair({ label: this.renderTemporaryIdMessage() })
+ );
+ }
+}
+
+module.exports = FluentReact.withLocalization(TemporaryExtensionDetail);
diff --git a/devtools/client/aboutdebugging/src/components/debugtarget/TemporaryExtensionInstallSection.css b/devtools/client/aboutdebugging/src/components/debugtarget/TemporaryExtensionInstallSection.css
new file mode 100644
index 0000000000..9166a3b615
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/debugtarget/TemporaryExtensionInstallSection.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/. */
+
+.temporary-extension-install-section__toolbar {
+ display: flex;
+ justify-content: end;
+}
diff --git a/devtools/client/aboutdebugging/src/components/debugtarget/TemporaryExtensionInstallSection.js b/devtools/client/aboutdebugging/src/components/debugtarget/TemporaryExtensionInstallSection.js
new file mode 100644
index 0000000000..f85618f73d
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/debugtarget/TemporaryExtensionInstallSection.js
@@ -0,0 +1,101 @@
+/* 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 DetailsLog = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/shared/DetailsLog.js")
+);
+const Message = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/shared/Message.js")
+);
+const TemporaryExtensionInstaller = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/debugtarget/TemporaryExtensionInstaller.js")
+);
+
+const {
+ MESSAGE_LEVEL,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+/**
+ * This component provides an installer and error message area for temporary extension.
+ */
+class TemporaryExtensionInstallSection extends PureComponent {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ temporaryInstallError: PropTypes.object,
+ };
+ }
+
+ renderError() {
+ const { temporaryInstallError } = this.props;
+
+ if (!temporaryInstallError) {
+ return null;
+ }
+
+ const errorMessages = [
+ temporaryInstallError.message,
+ ...(temporaryInstallError.additionalErrors || []),
+ ];
+
+ const errors = errorMessages.map((message, index) => {
+ return dom.p(
+ {
+ className: "technical-text",
+ key: "tmp-extension-install-error-" + index,
+ },
+ message
+ );
+ });
+
+ return Message(
+ {
+ level: MESSAGE_LEVEL.ERROR,
+ className: "qa-tmp-extension-install-error",
+ isCloseable: true,
+ },
+ Localized(
+ {
+ id: "about-debugging-tmp-extension-install-error",
+ },
+ dom.p({}, "about-debugging-tmp-extension-install-error")
+ ),
+ DetailsLog(
+ {
+ type: MESSAGE_LEVEL.ERROR,
+ },
+ errors
+ )
+ );
+ }
+
+ render() {
+ const { dispatch } = this.props;
+
+ return dom.section(
+ {},
+ dom.div(
+ {
+ className: "temporary-extension-install-section__toolbar",
+ },
+ TemporaryExtensionInstaller({ dispatch })
+ ),
+ this.renderError()
+ );
+ }
+}
+
+module.exports = TemporaryExtensionInstallSection;
diff --git a/devtools/client/aboutdebugging/src/components/debugtarget/TemporaryExtensionInstaller.js b/devtools/client/aboutdebugging/src/components/debugtarget/TemporaryExtensionInstaller.js
new file mode 100644
index 0000000000..e515c647ec
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/debugtarget/TemporaryExtensionInstaller.js
@@ -0,0 +1,52 @@
+/* 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 Actions = require("resource://devtools/client/aboutdebugging/src/actions/index.js");
+
+/**
+ * This component provides an installer for temporary extension.
+ */
+class TemporaryExtensionInstaller extends PureComponent {
+ static get propTypes() {
+ return {
+ className: PropTypes.string,
+ dispatch: PropTypes.func.isRequired,
+ };
+ }
+
+ install() {
+ this.props.dispatch(Actions.installTemporaryExtension());
+ }
+
+ render() {
+ const { className } = this.props;
+
+ return Localized(
+ {
+ id: "about-debugging-tmp-extension-install-button",
+ },
+ dom.button(
+ {
+ className: `${className} default-button qa-temporary-extension-install-button`,
+ onClick: e => this.install(),
+ },
+ "Load Temporary Add-on…"
+ )
+ );
+ }
+}
+
+module.exports = TemporaryExtensionInstaller;
diff --git a/devtools/client/aboutdebugging/src/components/debugtarget/WorkerDetail.js b/devtools/client/aboutdebugging/src/components/debugtarget/WorkerDetail.js
new file mode 100644
index 0000000000..b69252c430
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/debugtarget/WorkerDetail.js
@@ -0,0 +1,120 @@
+/* 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 {
+ SERVICE_WORKER_FETCH_STATES,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+const FieldPair = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/debugtarget/FieldPair.js")
+);
+
+const Types = require("resource://devtools/client/aboutdebugging/src/types/index.js");
+
+/**
+ * This component displays detail information for worker.
+ */
+class WorkerDetail extends PureComponent {
+ static get propTypes() {
+ return {
+ // Provided by wrapping the component with FluentReact.withLocalization.
+ getString: PropTypes.func.isRequired,
+ target: Types.debugTarget.isRequired,
+ };
+ }
+
+ renderFetch() {
+ const { fetch } = this.props.target.details;
+ const isListening = fetch === SERVICE_WORKER_FETCH_STATES.LISTENING;
+ const localizationId = isListening
+ ? "about-debugging-worker-fetch-listening"
+ : "about-debugging-worker-fetch-not-listening";
+
+ return Localized(
+ {
+ id: localizationId,
+ attrs: {
+ label: true,
+ value: true,
+ },
+ },
+ FieldPair({
+ className: isListening
+ ? "qa-worker-fetch-listening"
+ : "qa-worker-fetch-not-listening",
+ label: "Fetch",
+ slug: "fetch",
+ value: "about-debugging-worker-fetch-value",
+ })
+ );
+ }
+
+ renderPushService() {
+ const { pushServiceEndpoint } = this.props.target.details;
+
+ return Localized(
+ {
+ id: "about-debugging-worker-push-service",
+ attrs: { label: true },
+ },
+ FieldPair({
+ slug: "push-service",
+ label: "Push Service",
+ value: dom.span(
+ {
+ className: "qa-worker-push-service-value",
+ },
+ pushServiceEndpoint
+ ),
+ })
+ );
+ }
+
+ renderScope() {
+ const { scope } = this.props.target.details;
+
+ return Localized(
+ {
+ id: "about-debugging-worker-scope",
+ attrs: { label: true },
+ },
+ FieldPair({
+ slug: "scope",
+ label: "Scope",
+ value: scope,
+ })
+ );
+ }
+
+ render() {
+ const { fetch, pushServiceEndpoint, scope } = this.props.target.details;
+
+ const isEmptyList = !pushServiceEndpoint && !fetch && !scope && !status;
+
+ return dom.dl(
+ {
+ className:
+ "debug-target-item__detail" +
+ (isEmptyList ? " debug-target-item__detail--empty" : ""),
+ },
+ pushServiceEndpoint ? this.renderPushService() : null,
+ fetch ? this.renderFetch() : null,
+ scope ? this.renderScope() : null
+ );
+ }
+}
+
+module.exports = FluentReact.withLocalization(WorkerDetail);
diff --git a/devtools/client/aboutdebugging/src/components/debugtarget/moz.build b/devtools/client/aboutdebugging/src/components/debugtarget/moz.build
new file mode 100644
index 0000000000..981e6887a2
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/debugtarget/moz.build
@@ -0,0 +1,22 @@
+# 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(
+ "DebugTargetItem.js",
+ "DebugTargetList.js",
+ "DebugTargetPane.js",
+ "ExtensionDetail.js",
+ "FieldPair.js",
+ "InspectAction.js",
+ "ProcessDetail.js",
+ "ServiceWorkerAction.js",
+ "ServiceWorkerAdditionalActions.js",
+ "TabAction.js",
+ "TabDetail.js",
+ "TemporaryExtensionAdditionalActions.js",
+ "TemporaryExtensionDetail.js",
+ "TemporaryExtensionInstaller.js",
+ "TemporaryExtensionInstallSection.js",
+ "WorkerDetail.js",
+)
diff --git a/devtools/client/aboutdebugging/src/components/moz.build b/devtools/client/aboutdebugging/src/components/moz.build
new file mode 100644
index 0000000000..c48d384b3d
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/moz.build
@@ -0,0 +1,21 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DIRS += [
+ "connect",
+ "debugtarget",
+ "shared",
+ "sidebar",
+]
+
+DevToolsModules(
+ "App.js",
+ "CompatibilityWarning.js",
+ "ConnectionPromptSetting.js",
+ "ProfilerDialog.js",
+ "RuntimeActions.js",
+ "RuntimeInfo.js",
+ "RuntimePage.js",
+ "ServiceWorkersWarning.js",
+)
diff --git a/devtools/client/aboutdebugging/src/components/shared/DetailsLog.js b/devtools/client/aboutdebugging/src/components/shared/DetailsLog.js
new file mode 100644
index 0000000000..f10c8a487d
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/shared/DetailsLog.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 {
+ 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 {
+ MESSAGE_LEVEL,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+/**
+ * This component is designed to wrap a warning / error log message
+ * in the details tag to hide long texts and make the message expendable
+ * out of the box.
+ */
+class DetailsLog extends PureComponent {
+ static get propTypes() {
+ return {
+ children: PropTypes.node.isRequired,
+ type: PropTypes.string,
+ };
+ }
+ getLocalizationString() {
+ const { type } = this.props;
+
+ switch (type) {
+ case MESSAGE_LEVEL.WARNING:
+ return "about-debugging-message-details-label-warning";
+ case MESSAGE_LEVEL.ERROR:
+ return "about-debugging-message-details-label-error";
+ default:
+ return "about-debugging-message-details-label";
+ }
+ }
+
+ render() {
+ const { children } = this.props;
+
+ return dom.details(
+ {
+ className: "details--log",
+ },
+ Localized(
+ {
+ id: this.getLocalizationString(),
+ },
+ dom.summary({}, this.getLocalizationString())
+ ),
+ children
+ );
+ }
+}
+
+module.exports = DetailsLog;
diff --git a/devtools/client/aboutdebugging/src/components/shared/IconLabel.css b/devtools/client/aboutdebugging/src/components/shared/IconLabel.css
new file mode 100644
index 0000000000..ddb7c3929a
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/shared/IconLabel.css
@@ -0,0 +1,27 @@
+/* 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/. */
+
+.icon-label {
+ display: flex;
+ column-gap: var(--base-unit);
+ align-items: center;
+ margin: calc(var(--base-unit) * 2) 0;
+ -moz-context-properties: fill;
+
+ font-size: var(--icon-label-font-size);
+}
+
+.icon-label--ok {
+ --icon-color: var(--icon-ok-color);
+}
+.icon-label--info {
+ --icon-color: var(--icon-info-color);
+}
+
+.icon-label__icon {
+ padding: var(--base-unit);
+ fill: var(--icon-color);
+ width: calc(var(--base-unit) * 4);
+ height: calc(var(--base-unit) * 4);
+}
diff --git a/devtools/client/aboutdebugging/src/components/shared/IconLabel.js b/devtools/client/aboutdebugging/src/components/shared/IconLabel.js
new file mode 100644
index 0000000000..3141e75640
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/shared/IconLabel.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 {
+ 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 {
+ ICON_LABEL_LEVEL,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+const ICONS = {
+ [ICON_LABEL_LEVEL.INFO]: "chrome://devtools/skin/images/info.svg",
+ [ICON_LABEL_LEVEL.OK]: "chrome://devtools/skin/images/check.svg",
+};
+
+/* This component displays an icon accompanied by some content. It's similar
+ to a message, but it's not interactive */
+class IconLabel extends PureComponent {
+ static get propTypes() {
+ return {
+ children: PropTypes.node.isRequired,
+ className: PropTypes.string,
+ level: PropTypes.oneOf(Object.values(ICON_LABEL_LEVEL)).isRequired,
+ };
+ }
+
+ render() {
+ const { children, className, level } = this.props;
+ return dom.span(
+ {
+ className: `icon-label icon-label--${level} ${className || ""}`,
+ },
+ dom.img({
+ className: "icon-label__icon",
+ src: ICONS[level],
+ }),
+ children
+ );
+ }
+}
+
+module.exports = IconLabel;
diff --git a/devtools/client/aboutdebugging/src/components/shared/Message.css b/devtools/client/aboutdebugging/src/components/shared/Message.css
new file mode 100644
index 0000000000..e2c71c50b4
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/shared/Message.css
@@ -0,0 +1,79 @@
+/* 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/. */
+
+.message--level-error {
+ --message-background-color: var(--error-background);
+ --message-border-color: var(--error-border);
+ --message-color: var(--error-text);
+ --message-icon-color: var(--error-icon);
+}
+
+.message--level-info {
+ --message-background-color: var(--grey-20);
+ --message-border-color: transparent;
+ --message-color: var(--grey-90);
+ --message-icon-color: var(--grey-90);
+}
+
+.message--level-warning {
+ --message-background-color: var(--warning-background);
+ --message-border-color: var(--warning-border);
+ --message-color: var(--warning-text);
+ --message-icon-color: var(--warning-icon);
+}
+
+/*
+ * Layout of the message
+ *
+ * +--------+-----------------+----------+
+ * | Icon | Message content | closing |
+ * | | (several lines) | button |
+ * | | ( ... ) |(optional)|
+ * +--------+-----------------+----------+
+ */
+.message {
+ background-color: var(--message-background-color);
+ border: 1px solid var(--message-border-color);
+ border-radius: var(--base-unit);
+ color: var(--message-color);
+ display: grid;
+ grid-column-gap: var(--base-unit);
+ grid-template-columns: calc(var(--base-unit) * 6) 1fr auto;
+ grid-template-areas:
+ "icon body button";
+ margin: calc(var(--base-unit) * 2) 0;
+ padding: var(--base-unit);
+ -moz-context-properties: fill;
+}
+
+.message__icon {
+ margin: var(--base-unit);
+ fill: var(--message-icon-color);
+ grid-area: icon;
+}
+
+.message__body {
+ align-self: center;
+ font-size: var(--message-font-size);
+ font-weight: 400;
+ grid-area: body;
+ line-height: 1.6;
+}
+
+.message__button {
+ grid-area: button;
+ fill: var(--message-icon-color);
+}
+
+.message__button:hover {
+ /* reverse colors with icon when hover state */
+ background-color: var(--message-icon-color);
+ fill: var(--message-background-color);
+}
+
+.message__button:active {
+ /* reverse colors with text when active state */
+ background-color: var(--message-color);
+ fill: var(--message-background-color);
+}
diff --git a/devtools/client/aboutdebugging/src/components/shared/Message.js b/devtools/client/aboutdebugging/src/components/shared/Message.js
new file mode 100644
index 0000000000..248db58dd3
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/shared/Message.js
@@ -0,0 +1,109 @@
+/* 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 {
+ MESSAGE_LEVEL,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+const ICONS = {
+ [MESSAGE_LEVEL.ERROR]:
+ "chrome://devtools/skin/images/aboutdebugging-error.svg",
+ [MESSAGE_LEVEL.INFO]:
+ "chrome://devtools/skin/images/aboutdebugging-information.svg",
+ [MESSAGE_LEVEL.WARNING]: "chrome://devtools/skin/images/alert.svg",
+};
+const CLOSE_ICON_SRC = "chrome://devtools/skin/images/close.svg";
+
+/**
+ * This component is designed to display a photon-style message bar. The component is
+ * responsible for displaying the message container with the appropriate icon. The content
+ * of the message should be passed as the children of this component.
+ */
+class Message extends PureComponent {
+ static get propTypes() {
+ return {
+ children: PropTypes.node.isRequired,
+ className: PropTypes.string,
+ isCloseable: PropTypes.bool,
+ level: PropTypes.oneOf(Object.values(MESSAGE_LEVEL)).isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ isClosed: false,
+ };
+ }
+
+ closeMessage() {
+ this.setState({ isClosed: true });
+ }
+
+ renderButton(level) {
+ return dom.button(
+ {
+ className:
+ `ghost-button message__button message__button--${level} ` +
+ `qa-message-button-close-button`,
+ onClick: () => this.closeMessage(),
+ },
+ Localized(
+ {
+ id: "about-debugging-message-close-icon",
+ attrs: {
+ alt: true,
+ },
+ },
+ dom.img({
+ className: "qa-message-button-close-icon",
+ src: CLOSE_ICON_SRC,
+ })
+ )
+ );
+ }
+
+ render() {
+ const { children, className, level, isCloseable } = this.props;
+ const { isClosed } = this.state;
+
+ if (isClosed) {
+ return null;
+ }
+
+ return dom.aside(
+ {
+ className:
+ `message message--level-${level} qa-message` +
+ (className ? ` ${className}` : ""),
+ },
+ dom.img({
+ className: "message__icon",
+ src: ICONS[level],
+ }),
+ dom.div(
+ {
+ className: "message__body",
+ },
+ children
+ ),
+ // if the message is closeable, render a closing button
+ isCloseable ? this.renderButton(level) : null
+ );
+ }
+}
+
+module.exports = Message;
diff --git a/devtools/client/aboutdebugging/src/components/shared/moz.build b/devtools/client/aboutdebugging/src/components/shared/moz.build
new file mode 100644
index 0000000000..7e0e89f2a0
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/shared/moz.build
@@ -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/.
+
+DevToolsModules(
+ "DetailsLog.js",
+ "IconLabel.js",
+ "Message.js",
+)
diff --git a/devtools/client/aboutdebugging/src/components/sidebar/RefreshDevicesButton.js b/devtools/client/aboutdebugging/src/components/sidebar/RefreshDevicesButton.js
new file mode 100644
index 0000000000..78ebb61661
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/sidebar/RefreshDevicesButton.js
@@ -0,0 +1,46 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ createFactory,
+ PureComponent,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const dom = 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 PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const Actions = require("resource://devtools/client/aboutdebugging/src/actions/index.js");
+
+class RefreshDevicesButton extends PureComponent {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ isScanning: PropTypes.bool.isRequired,
+ };
+ }
+
+ refreshDevices() {
+ this.props.dispatch(Actions.scanUSBRuntimes());
+ }
+
+ render() {
+ return Localized(
+ { id: "about-debugging-refresh-usb-devices-button" },
+ dom.button(
+ {
+ className: "default-button qa-refresh-devices-button",
+ disabled: this.props.isScanning,
+ onClick: () => this.refreshDevices(),
+ },
+ "Refresh devices"
+ )
+ );
+ }
+}
+
+module.exports = RefreshDevicesButton;
diff --git a/devtools/client/aboutdebugging/src/components/sidebar/Sidebar.css b/devtools/client/aboutdebugging/src/components/sidebar/Sidebar.css
new file mode 100644
index 0000000000..afad630f6e
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/sidebar/Sidebar.css
@@ -0,0 +1,43 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+.sidebar {
+ display: grid;
+ grid-template-rows: auto auto;
+}
+
+.sidebar__label {
+ color: var(--secondary-text-color);
+ display: block;
+ padding: 12px 0;
+ text-align: center;
+ font-size: var(--message-font-size);
+}
+
+.sidebar__adb-status {
+ margin-block-end: calc(var(--base-unit) * 2);
+}
+
+.sidebar__refresh-usb {
+ text-align: center;
+}
+
+.sidebar__footer {
+ align-self: flex-end;
+}
+
+.sidebar__footer__support-help {
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ column-gap: calc(var(--base-unit) * 4);
+ height: 100%;
+}
+
+.sidebar__footer__icon {
+ width: calc(var(--base-unit) * 4);
+ height: calc(var(--base-unit) * 4);
+ -moz-context-properties: fill;
+ fill: currentColor;
+}
diff --git a/devtools/client/aboutdebugging/src/components/sidebar/Sidebar.js b/devtools/client/aboutdebugging/src/components/sidebar/Sidebar.js
new file mode 100644
index 0000000000..3213b8141c
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/sidebar/Sidebar.js
@@ -0,0 +1,259 @@
+/* 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 {
+ ICON_LABEL_LEVEL,
+ PAGE_TYPES,
+ RUNTIMES,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+const Types = require("resource://devtools/client/aboutdebugging/src/types/index.js");
+loader.lazyRequireGetter(
+ this,
+ "ADB_ADDON_STATES",
+ "resource://devtools/client/shared/remote-debugging/adb/adb-addon.js",
+ true
+);
+
+const IconLabel = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/shared/IconLabel.js")
+);
+const SidebarItem = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/sidebar/SidebarItem.js")
+);
+const SidebarFixedItem = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/sidebar/SidebarFixedItem.js")
+);
+const SidebarRuntimeItem = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/sidebar/SidebarRuntimeItem.js")
+);
+const RefreshDevicesButton = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/sidebar/RefreshDevicesButton.js")
+);
+const FIREFOX_ICON =
+ "chrome://devtools/skin/images/aboutdebugging-firefox-logo.svg";
+const CONNECT_ICON = "chrome://devtools/skin/images/settings.svg";
+const GLOBE_ICON =
+ "chrome://devtools/skin/images/aboutdebugging-globe-icon.svg";
+const USB_ICON =
+ "chrome://devtools/skin/images/aboutdebugging-connect-icon.svg";
+
+class Sidebar extends PureComponent {
+ static get propTypes() {
+ return {
+ adbAddonStatus: Types.adbAddonStatus,
+ className: PropTypes.string,
+ dispatch: PropTypes.func.isRequired,
+ isAdbReady: PropTypes.bool.isRequired,
+ isScanningUsb: PropTypes.bool.isRequired,
+ networkRuntimes: PropTypes.arrayOf(Types.runtime).isRequired,
+ selectedPage: Types.page,
+ selectedRuntimeId: PropTypes.string,
+ usbRuntimes: PropTypes.arrayOf(Types.runtime).isRequired,
+ };
+ }
+
+ renderAdbStatus() {
+ const isUsbEnabled =
+ this.props.isAdbReady &&
+ this.props.adbAddonStatus === ADB_ADDON_STATES.INSTALLED;
+ const localizationId = isUsbEnabled
+ ? "about-debugging-sidebar-usb-enabled"
+ : "about-debugging-sidebar-usb-disabled";
+ return IconLabel(
+ {
+ level: isUsbEnabled ? ICON_LABEL_LEVEL.OK : ICON_LABEL_LEVEL.INFO,
+ },
+ Localized(
+ {
+ id: localizationId,
+ },
+ dom.span(
+ {
+ className: "qa-sidebar-usb-status",
+ },
+ localizationId
+ )
+ )
+ );
+ }
+
+ renderDevicesEmpty() {
+ return SidebarItem(
+ {},
+ Localized(
+ {
+ id: "about-debugging-sidebar-no-devices",
+ },
+ dom.aside(
+ {
+ className: "sidebar__label qa-sidebar-no-devices",
+ },
+ "No devices discovered"
+ )
+ )
+ );
+ }
+
+ renderDevices() {
+ const { networkRuntimes, usbRuntimes } = this.props;
+
+ // render a "no devices" messages when the lists are empty
+ if (!networkRuntimes.length && !usbRuntimes.length) {
+ return this.renderDevicesEmpty();
+ }
+ // render all devices otherwise
+ return [
+ ...this.renderRuntimeItems(GLOBE_ICON, networkRuntimes),
+ ...this.renderRuntimeItems(USB_ICON, usbRuntimes),
+ ];
+ }
+
+ renderRuntimeItems(icon, runtimes) {
+ const { dispatch, selectedPage, selectedRuntimeId } = this.props;
+
+ return runtimes.map(runtime => {
+ const keyId = `${runtime.type}-${runtime.id}`;
+ const runtimeHasDetails = !!runtime.runtimeDetails;
+ const isSelected =
+ selectedPage === PAGE_TYPES.RUNTIME && runtime.id === selectedRuntimeId;
+
+ let name = runtime.name;
+ if (runtime.type === RUNTIMES.USB && runtimeHasDetails) {
+ // Update the name to be same to the runtime page.
+ name = runtime.runtimeDetails.info.name;
+ }
+
+ return SidebarRuntimeItem({
+ deviceName: runtime.extra.deviceName,
+ dispatch,
+ icon,
+ key: keyId,
+ isConnected: runtimeHasDetails,
+ isConnecting: runtime.isConnecting,
+ isConnectionFailed: runtime.isConnectionFailed,
+ isConnectionNotResponding: runtime.isConnectionNotResponding,
+ isConnectionTimeout: runtime.isConnectionTimeout,
+ isSelected,
+ isUnavailable: runtime.isUnavailable,
+ isUnplugged: runtime.isUnplugged,
+ name,
+ runtimeId: runtime.id,
+ });
+ });
+ }
+
+ renderFooter() {
+ const HELP_ICON_SRC = "chrome://global/skin/icons/help.svg";
+ const SUPPORT_URL =
+ "https://firefox-source-docs.mozilla.org/devtools-user/about_colon_debugging/";
+
+ return dom.footer(
+ {
+ className: "sidebar__footer",
+ },
+ dom.ul(
+ {},
+ SidebarItem(
+ {
+ className: "sidebar-item--condensed",
+ to: SUPPORT_URL,
+ },
+ dom.span(
+ {
+ className: "sidebar__footer__support-help",
+ },
+ Localized(
+ {
+ id: "about-debugging-sidebar-support-icon",
+ attrs: {
+ alt: true,
+ },
+ },
+ dom.img({
+ className: "sidebar__footer__icon",
+ src: HELP_ICON_SRC,
+ })
+ ),
+ Localized(
+ {
+ id: "about-debugging-sidebar-support",
+ },
+ dom.span({}, "about-debugging-sidebar-support")
+ )
+ )
+ )
+ )
+ );
+ }
+
+ render() {
+ const { dispatch, selectedPage, selectedRuntimeId, isScanningUsb } =
+ this.props;
+
+ return dom.aside(
+ {
+ className: `sidebar ${this.props.className || ""}`,
+ },
+ dom.ul(
+ {},
+ Localized(
+ { id: "about-debugging-sidebar-setup", attrs: { name: true } },
+ SidebarFixedItem({
+ dispatch,
+ icon: CONNECT_ICON,
+ isSelected: PAGE_TYPES.CONNECT === selectedPage,
+ key: PAGE_TYPES.CONNECT,
+ name: "Setup",
+ to: "/setup",
+ })
+ ),
+ Localized(
+ { id: "about-debugging-sidebar-this-firefox", attrs: { name: true } },
+ SidebarFixedItem({
+ icon: FIREFOX_ICON,
+ isSelected:
+ PAGE_TYPES.RUNTIME === selectedPage &&
+ selectedRuntimeId === RUNTIMES.THIS_FIREFOX,
+ key: RUNTIMES.THIS_FIREFOX,
+ name: "This Firefox",
+ to: `/runtime/${RUNTIMES.THIS_FIREFOX}`,
+ })
+ ),
+ SidebarItem(
+ {
+ className: "sidebar__adb-status",
+ },
+ dom.hr({ className: "separator separator--breathe" }),
+ this.renderAdbStatus()
+ ),
+ this.renderDevices(),
+ SidebarItem(
+ {
+ className: "sidebar-item--breathe sidebar__refresh-usb",
+ key: "refresh-devices",
+ },
+ RefreshDevicesButton({
+ dispatch,
+ isScanning: isScanningUsb,
+ })
+ )
+ ),
+ this.renderFooter()
+ );
+ }
+}
+
+module.exports = Sidebar;
diff --git a/devtools/client/aboutdebugging/src/components/sidebar/SidebarFixedItem.css b/devtools/client/aboutdebugging/src/components/sidebar/SidebarFixedItem.css
new file mode 100644
index 0000000000..7345b8e80f
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/sidebar/SidebarFixedItem.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/. */
+
+/*
+ * Layout of a fixed sidebar item
+ *
+ * +--------+----------------+
+ * | Icon | Name |
+ * +--------+----------------+
+ */
+
+.sidebar-fixed-item__container {
+ align-items: center;
+ border-radius: 2px;
+ display: grid;
+ grid-template-columns: 34px 1fr;
+ height: 100%;
+ font-size: var(--body-20-font-size);
+ font-weight: var(--body-20-font-weight);
+}
+
+.sidebar-fixed-item__icon {
+ fill: currentColor;
+ height: 24px;
+ margin-inline-end: 9px;
+ width: 24px;
+ -moz-context-properties: fill;
+}
diff --git a/devtools/client/aboutdebugging/src/components/sidebar/SidebarFixedItem.js b/devtools/client/aboutdebugging/src/components/sidebar/SidebarFixedItem.js
new file mode 100644
index 0000000000..cf5dbab31a
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/sidebar/SidebarFixedItem.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 {
+ PureComponent,
+ createFactory,
+} = 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 SidebarItem = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/sidebar/SidebarItem.js")
+);
+
+/**
+ * This component displays a fixed item in the Sidebar component.
+ */
+class SidebarFixedItem extends PureComponent {
+ static get propTypes() {
+ return {
+ icon: PropTypes.string.isRequired,
+ isSelected: PropTypes.bool.isRequired,
+ name: PropTypes.string.isRequired,
+ to: PropTypes.string,
+ };
+ }
+
+ render() {
+ const { icon, isSelected, name, to } = this.props;
+
+ return SidebarItem(
+ {
+ className: "sidebar-item--tall",
+ isSelected,
+ to,
+ },
+ dom.div(
+ {
+ className: "sidebar-fixed-item__container",
+ },
+ dom.img({
+ className: "sidebar-fixed-item__icon",
+ src: icon,
+ }),
+ dom.span(
+ {
+ className: "ellipsis-text",
+ title: name,
+ },
+ name
+ )
+ )
+ );
+ }
+}
+
+module.exports = SidebarFixedItem;
diff --git a/devtools/client/aboutdebugging/src/components/sidebar/SidebarItem.css b/devtools/client/aboutdebugging/src/components/sidebar/SidebarItem.css
new file mode 100644
index 0000000000..3f12964012
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/sidebar/SidebarItem.css
@@ -0,0 +1,71 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+.sidebar-item {
+ color: var(--sidebar-text-color);
+ border-radius: 2px;
+ padding-inline-end: var(--category-padding);
+ padding-inline-start: var(--category-padding);
+ transition: background-color var(--category-transition-duration);
+ user-select: none;
+}
+
+.sidebar-item--tall {
+ height: var(--category-height);
+}
+
+.sidebar-item--condensed {
+ height: calc(var(--base-unit) * 9);
+}
+
+.sidebar-item__link {
+ display: block;
+ height: 100%;
+}
+
+.sidebar-item__link,
+.sidebar-item__link:hover {
+ color: inherit; /* do not apply usual link colors, but grab this element parent's */
+}
+
+.sidebar-item:not(.sidebar-item--selectable) {
+ color: var(--secondary-text-color);
+}
+
+.sidebar-item--selectable:hover {
+ background-color: var(--sidebar-background-hover);
+}
+
+.sidebar-item--selected {
+ color: var(--sidebar-selected-color);
+}
+
+.sidebar-item--breathe {
+ margin-block-start: calc(6 * var(--base-unit));
+ margin-block-end: calc(2 * var(--base-unit));
+}
+
+@media (prefers-contrast) {
+ /* Color transitions (black <-> white) look bad in high contrast */
+ .sidebar-item {
+ transition: background 0s;
+ }
+
+ .sidebar-item--selected,
+ .sidebar-item--selected:hover {
+ background-color: ButtonText;
+ }
+
+ /* `color: inherit` should not be used in high contrast mode
+ otherwise the link inherits the <a> color from ua.css */
+ .sidebar-item__link,
+ .sidebar-item__link:hover {
+ color: ButtonText;
+ }
+
+ .sidebar-item--selected .sidebar-item__link,
+ .sidebar-item--selected .sidebar-item__link:hover {
+ color: ButtonFace;
+ }
+}
diff --git a/devtools/client/aboutdebugging/src/components/sidebar/SidebarItem.js b/devtools/client/aboutdebugging/src/components/sidebar/SidebarItem.js
new file mode 100644
index 0000000000..cd17d8e6bc
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/sidebar/SidebarItem.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 {
+ 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 Link = createFactory(
+ require("resource://devtools/client/shared/vendor/react-router-dom.js").Link
+);
+
+/**
+ * This component is used as a wrapper by items in the sidebar.
+ */
+class SidebarItem extends PureComponent {
+ static get propTypes() {
+ return {
+ children: PropTypes.node.isRequired,
+ className: PropTypes.string,
+ isSelected: PropTypes.bool.isRequired,
+ to: PropTypes.string,
+ };
+ }
+
+ static get defaultProps() {
+ return {
+ isSelected: false,
+ };
+ }
+
+ renderContent() {
+ const { children, to } = this.props;
+
+ if (to) {
+ const isExternalUrl = /^http/.test(to);
+
+ return isExternalUrl
+ ? dom.a(
+ {
+ className: "sidebar-item__link undecorated-link",
+ href: to,
+ target: "_blank",
+ },
+ children
+ )
+ : Link(
+ {
+ className: "sidebar-item__link qa-sidebar-link undecorated-link",
+ to,
+ },
+ children
+ );
+ }
+
+ return children;
+ }
+
+ render() {
+ const { className, isSelected, to } = this.props;
+
+ return dom.li(
+ {
+ className:
+ "sidebar-item qa-sidebar-item" +
+ (className ? ` ${className}` : "") +
+ (isSelected
+ ? " sidebar-item--selected qa-sidebar-item-selected"
+ : "") +
+ (to ? " sidebar-item--selectable" : ""),
+ },
+ this.renderContent()
+ );
+ }
+}
+
+module.exports = SidebarItem;
diff --git a/devtools/client/aboutdebugging/src/components/sidebar/SidebarRuntimeItem.css b/devtools/client/aboutdebugging/src/components/sidebar/SidebarRuntimeItem.css
new file mode 100644
index 0000000000..423723a27f
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/sidebar/SidebarRuntimeItem.css
@@ -0,0 +1,41 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Layout of a runtime sidebar item
+ *
+ * +--------+----------------+---------------------------+
+ * | Icon | Runtime name | Connect button |
+ * +--------+----------------+---------------------------+
+ */
+
+.sidebar-runtime-item__container {
+ box-sizing: border-box;
+ height: var(--category-height);
+ align-items: center;
+ display: grid;
+ grid-column-gap: var(--base-unit);
+ grid-template-columns: calc(var(--base-unit) * 6) 1fr auto;
+ font-size: var(--body-20-font-size);
+ font-weight: var(--body-20-font-weight);
+}
+
+.sidebar-runtime-item__icon {
+ fill: currentColor;
+ -moz-context-properties: fill;
+}
+
+.sidebar-runtime-item__runtime {
+ line-height: 1;
+}
+
+.sidebar-runtime-item__runtime__details {
+ font-size: var(--caption-10-font-size);
+ font-weight: var(--caption-10-font-weight);
+ line-height: 1.2;
+}
+
+.sidebar-runtime-item__message:first-of-type {
+ margin-block-start: calc(var(--base-unit) * -1);
+}
diff --git a/devtools/client/aboutdebugging/src/components/sidebar/SidebarRuntimeItem.js b/devtools/client/aboutdebugging/src/components/sidebar/SidebarRuntimeItem.js
new file mode 100644
index 0000000000..a5c5fe6d55
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/sidebar/SidebarRuntimeItem.js
@@ -0,0 +1,216 @@
+/* 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 Message = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/shared/Message.js")
+);
+const SidebarItem = createFactory(
+ require("resource://devtools/client/aboutdebugging/src/components/sidebar/SidebarItem.js")
+);
+const Actions = require("resource://devtools/client/aboutdebugging/src/actions/index.js");
+const {
+ MESSAGE_LEVEL,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+/**
+ * This component displays a runtime item of the Sidebar component.
+ */
+class SidebarRuntimeItem extends PureComponent {
+ static get propTypes() {
+ return {
+ deviceName: PropTypes.string,
+ dispatch: PropTypes.func.isRequired,
+ // Provided by wrapping the component with FluentReact.withLocalization.
+ getString: PropTypes.func.isRequired,
+ icon: PropTypes.string.isRequired,
+ isConnected: PropTypes.bool.isRequired,
+ isConnecting: PropTypes.bool.isRequired,
+ isConnectionFailed: PropTypes.bool.isRequired,
+ isConnectionNotResponding: PropTypes.bool.isRequired,
+ isConnectionTimeout: PropTypes.bool.isRequired,
+ isSelected: PropTypes.bool.isRequired,
+ isUnavailable: PropTypes.bool.isRequired,
+ isUnplugged: PropTypes.bool.isRequired,
+ name: PropTypes.string.isRequired,
+ runtimeId: PropTypes.string.isRequired,
+ };
+ }
+
+ renderConnectButton() {
+ const { isConnecting } = this.props;
+ const localizationId = isConnecting
+ ? "about-debugging-sidebar-item-connect-button-connecting"
+ : "about-debugging-sidebar-item-connect-button";
+ return Localized(
+ {
+ id: localizationId,
+ },
+ dom.button(
+ {
+ className: "default-button default-button--micro qa-connect-button",
+ disabled: isConnecting,
+ onClick: () => {
+ const { dispatch, runtimeId } = this.props;
+ dispatch(Actions.connectRuntime(runtimeId));
+ },
+ },
+ localizationId
+ )
+ );
+ }
+
+ renderMessage(flag, level, localizationId, className) {
+ if (!flag) {
+ return null;
+ }
+
+ return Message(
+ {
+ level,
+ className: `${className} sidebar-runtime-item__message`,
+ isCloseable: true,
+ },
+ Localized(
+ {
+ id: localizationId,
+ },
+ dom.p({ className: "word-wrap-anywhere" }, localizationId)
+ )
+ );
+ }
+
+ renderName() {
+ const { deviceName, getString, isUnavailable, isUnplugged, name } =
+ this.props;
+
+ let displayName, qaClassName;
+ if (isUnplugged) {
+ displayName = getString("about-debugging-sidebar-runtime-item-unplugged");
+ qaClassName = "qa-runtime-item-unplugged";
+ } else if (isUnavailable) {
+ displayName = getString(
+ "about-debugging-sidebar-runtime-item-waiting-for-browser"
+ );
+ qaClassName = "qa-runtime-item-waiting-for-browser";
+ } else {
+ displayName = name;
+ qaClassName = "qa-runtime-item-standard";
+ }
+
+ const localizationId = deviceName
+ ? "about-debugging-sidebar-runtime-item-name"
+ : "about-debugging-sidebar-runtime-item-name-no-device";
+
+ const className = "ellipsis-text sidebar-runtime-item__runtime";
+
+ function renderWithDevice() {
+ return dom.span(
+ {
+ className,
+ title: localizationId,
+ },
+ deviceName,
+ dom.br({}),
+ dom.span(
+ {
+ className: `sidebar-runtime-item__runtime__details ${qaClassName}`,
+ },
+ displayName
+ )
+ );
+ }
+
+ function renderNoDevice() {
+ return dom.span(
+ {
+ className,
+ title: localizationId,
+ },
+ displayName
+ );
+ }
+
+ return Localized(
+ {
+ id: localizationId,
+ attrs: { title: true },
+ $deviceName: deviceName,
+ $displayName: displayName,
+ },
+ deviceName ? renderWithDevice() : renderNoDevice()
+ );
+ }
+
+ render() {
+ const {
+ getString,
+ icon,
+ isConnected,
+ isConnectionFailed,
+ isConnectionTimeout,
+ isConnectionNotResponding,
+ isSelected,
+ isUnavailable,
+ runtimeId,
+ } = this.props;
+
+ const connectionStatus = isConnected
+ ? getString("aboutdebugging-sidebar-runtime-connection-status-connected")
+ : getString(
+ "aboutdebugging-sidebar-runtime-connection-status-disconnected"
+ );
+
+ return SidebarItem(
+ {
+ isSelected,
+ to: isConnected ? `/runtime/${encodeURIComponent(runtimeId)}` : null,
+ },
+ dom.section(
+ {
+ className: "sidebar-runtime-item__container",
+ },
+ dom.img({
+ className: "sidebar-runtime-item__icon ",
+ src: icon,
+ alt: connectionStatus,
+ title: connectionStatus,
+ }),
+ this.renderName(),
+ !isUnavailable && !isConnected ? this.renderConnectButton() : null
+ ),
+ this.renderMessage(
+ isConnectionFailed,
+ MESSAGE_LEVEL.ERROR,
+ "about-debugging-sidebar-item-connect-button-connection-failed",
+ "qa-connection-error"
+ ),
+ this.renderMessage(
+ isConnectionTimeout,
+ MESSAGE_LEVEL.ERROR,
+ "about-debugging-sidebar-item-connect-button-connection-timeout",
+ "qa-connection-timeout"
+ ),
+ this.renderMessage(
+ isConnectionNotResponding,
+ MESSAGE_LEVEL.WARNING,
+ "about-debugging-sidebar-item-connect-button-connection-not-responding",
+ "qa-connection-not-responding"
+ )
+ );
+ }
+}
+
+module.exports = FluentReact.withLocalization(SidebarRuntimeItem);
diff --git a/devtools/client/aboutdebugging/src/components/sidebar/moz.build b/devtools/client/aboutdebugging/src/components/sidebar/moz.build
new file mode 100644
index 0000000000..081ea2a848
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/components/sidebar/moz.build
@@ -0,0 +1,11 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ "RefreshDevicesButton.js",
+ "Sidebar.js",
+ "SidebarFixedItem.js",
+ "SidebarItem.js",
+ "SidebarRuntimeItem.js",
+)