summaryrefslogtreecommitdiffstats
path: root/devtools/client/aboutdebugging/src
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /devtools/client/aboutdebugging/src
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/aboutdebugging/src')
-rw-r--r--devtools/client/aboutdebugging/src/actions/debug-targets.js356
-rw-r--r--devtools/client/aboutdebugging/src/actions/index.js12
-rw-r--r--devtools/client/aboutdebugging/src/actions/moz.build11
-rw-r--r--devtools/client/aboutdebugging/src/actions/runtimes.js515
-rw-r--r--devtools/client/aboutdebugging/src/actions/telemetry.js23
-rw-r--r--devtools/client/aboutdebugging/src/actions/ui.js202
-rw-r--r--devtools/client/aboutdebugging/src/base.css521
-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
-rw-r--r--devtools/client/aboutdebugging/src/constants.js185
-rw-r--r--devtools/client/aboutdebugging/src/create-store.js78
-rw-r--r--devtools/client/aboutdebugging/src/middleware/debug-target-listener.js111
-rw-r--r--devtools/client/aboutdebugging/src/middleware/error-logging.js35
-rw-r--r--devtools/client/aboutdebugging/src/middleware/event-recording.js268
-rw-r--r--devtools/client/aboutdebugging/src/middleware/extension-component-data.js84
-rw-r--r--devtools/client/aboutdebugging/src/middleware/moz.build13
-rw-r--r--devtools/client/aboutdebugging/src/middleware/process-component-data.js55
-rw-r--r--devtools/client/aboutdebugging/src/middleware/tab-component-data.js51
-rw-r--r--devtools/client/aboutdebugging/src/middleware/worker-component-data.js82
-rw-r--r--devtools/client/aboutdebugging/src/modules/client-wrapper.js217
-rw-r--r--devtools/client/aboutdebugging/src/modules/debug-target-collapsibilities.js46
-rw-r--r--devtools/client/aboutdebugging/src/modules/debug-target-support.js98
-rw-r--r--devtools/client/aboutdebugging/src/modules/extensions-helper.js92
-rw-r--r--devtools/client/aboutdebugging/src/modules/l10n.js12
-rw-r--r--devtools/client/aboutdebugging/src/modules/moz.build16
-rw-r--r--devtools/client/aboutdebugging/src/modules/network-locations.js69
-rw-r--r--devtools/client/aboutdebugging/src/modules/runtime-client-factory.js68
-rw-r--r--devtools/client/aboutdebugging/src/modules/runtime-default-preferences.js101
-rw-r--r--devtools/client/aboutdebugging/src/modules/runtimes-state-helper.js37
-rw-r--r--devtools/client/aboutdebugging/src/modules/usb-runtimes.js122
-rw-r--r--devtools/client/aboutdebugging/src/moz.build17
-rw-r--r--devtools/client/aboutdebugging/src/reducers/debug-targets-state.js155
-rw-r--r--devtools/client/aboutdebugging/src/reducers/index.js24
-rw-r--r--devtools/client/aboutdebugging/src/reducers/moz.build10
-rw-r--r--devtools/client/aboutdebugging/src/reducers/runtimes-state.js178
-rw-r--r--devtools/client/aboutdebugging/src/reducers/ui-state.js115
-rw-r--r--devtools/client/aboutdebugging/src/types/debug-target.js71
-rw-r--r--devtools/client/aboutdebugging/src/types/index.js18
-rw-r--r--devtools/client/aboutdebugging/src/types/moz.build10
-rw-r--r--devtools/client/aboutdebugging/src/types/runtime.js164
-rw-r--r--devtools/client/aboutdebugging/src/types/ui.js85
103 files changed, 9485 insertions, 0 deletions
diff --git a/devtools/client/aboutdebugging/src/actions/debug-targets.js b/devtools/client/aboutdebugging/src/actions/debug-targets.js
new file mode 100644
index 0000000000..33b2f1cbea
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/actions/debug-targets.js
@@ -0,0 +1,356 @@
+/* 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 { AddonManager } = ChromeUtils.importESModule(
+ "resource://gre/modules/AddonManager.sys.mjs",
+ // AddonManager is a singleton, never create two instances of it.
+ { loadInDevToolsLoader: false }
+);
+const {
+ remoteClientManager,
+} = require("resource://devtools/client/shared/remote-debugging/remote-client-manager.js");
+
+const {
+ l10n,
+} = require("resource://devtools/client/aboutdebugging/src/modules/l10n.js");
+
+const {
+ isSupportedDebugTargetPane,
+} = require("resource://devtools/client/aboutdebugging/src/modules/debug-target-support.js");
+
+const {
+ openTemporaryExtension,
+} = require("resource://devtools/client/aboutdebugging/src/modules/extensions-helper.js");
+
+const {
+ getCurrentClient,
+ getCurrentRuntime,
+} = require("resource://devtools/client/aboutdebugging/src/modules/runtimes-state-helper.js");
+
+const {
+ gDevTools,
+} = require("resource://devtools/client/framework/devtools.js");
+
+const {
+ DEBUG_TARGETS,
+ DEBUG_TARGET_PANE,
+ REQUEST_EXTENSIONS_FAILURE,
+ REQUEST_EXTENSIONS_START,
+ REQUEST_EXTENSIONS_SUCCESS,
+ REQUEST_PROCESSES_FAILURE,
+ REQUEST_PROCESSES_START,
+ REQUEST_PROCESSES_SUCCESS,
+ REQUEST_TABS_FAILURE,
+ REQUEST_TABS_START,
+ REQUEST_TABS_SUCCESS,
+ REQUEST_WORKERS_FAILURE,
+ REQUEST_WORKERS_START,
+ REQUEST_WORKERS_SUCCESS,
+ TEMPORARY_EXTENSION_INSTALL_FAILURE,
+ TEMPORARY_EXTENSION_INSTALL_START,
+ TEMPORARY_EXTENSION_INSTALL_SUCCESS,
+ TEMPORARY_EXTENSION_RELOAD_FAILURE,
+ TEMPORARY_EXTENSION_RELOAD_START,
+ TEMPORARY_EXTENSION_RELOAD_SUCCESS,
+ TERMINATE_EXTENSION_BGSCRIPT_FAILURE,
+ TERMINATE_EXTENSION_BGSCRIPT_SUCCESS,
+ TERMINATE_EXTENSION_BGSCRIPT_START,
+ RUNTIMES,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+const Actions = require("resource://devtools/client/aboutdebugging/src/actions/index.js");
+
+function getTabForUrl(url) {
+ for (const navigator of Services.wm.getEnumerator("navigator:browser")) {
+ for (const browser of navigator.gBrowser.browsers) {
+ if (
+ browser.contentWindow &&
+ browser.contentWindow.location.href === url
+ ) {
+ return navigator.gBrowser.getTabForBrowser(browser);
+ }
+ }
+ }
+
+ return null;
+}
+
+function inspectDebugTarget(type, id) {
+ return async ({ dispatch, getState }) => {
+ const runtime = getCurrentRuntime(getState().runtimes);
+
+ if (
+ type == DEBUG_TARGETS.EXTENSION &&
+ runtime.id === RUNTIMES.THIS_FIREFOX
+ ) {
+ // Bug 1780912: To avoid UX issues when debugging local web extensions,
+ // we are opening the toolbox in an independant window.
+ // Whereas all others are opened in new tabs.
+ gDevTools.showToolboxForWebExtension(id);
+ } else {
+ const urlParams = {
+ type,
+ };
+ // Main process may not provide any ID.
+ if (id) {
+ urlParams.id = id;
+ }
+
+ if (runtime.id !== RUNTIMES.THIS_FIREFOX) {
+ urlParams.remoteId = remoteClientManager.getRemoteId(
+ runtime.id,
+ runtime.type
+ );
+ }
+
+ const url = `about:devtools-toolbox?${new window.URLSearchParams(
+ urlParams
+ )}`;
+
+ const existingTab = getTabForUrl(url);
+ if (existingTab) {
+ const navigator = existingTab.ownerGlobal;
+ navigator.gBrowser.selectedTab = existingTab;
+ navigator.focus();
+ } else {
+ window.open(url);
+ }
+ }
+
+ dispatch(
+ Actions.recordTelemetryEvent("inspect", {
+ target_type: type.toUpperCase(),
+ runtime_type: runtime.type,
+ })
+ );
+ };
+}
+
+function installTemporaryExtension() {
+ const message = l10n.getString(
+ "about-debugging-tmp-extension-install-message"
+ );
+ return async ({ dispatch, getState }) => {
+ dispatch({ type: TEMPORARY_EXTENSION_INSTALL_START });
+ const file = await openTemporaryExtension(window, message);
+ try {
+ await AddonManager.installTemporaryAddon(file);
+ dispatch({ type: TEMPORARY_EXTENSION_INSTALL_SUCCESS });
+ } catch (e) {
+ dispatch({ type: TEMPORARY_EXTENSION_INSTALL_FAILURE, error: e });
+ }
+ };
+}
+
+function pushServiceWorker(id, registrationFront) {
+ return async ({ dispatch, getState }) => {
+ try {
+ // The push button is only available if canDebugServiceWorkers is true.
+ // With this configuration, `push` should always be called on the
+ // registration front, and not on the (service) WorkerTargetActor.
+ await registrationFront.push();
+ } catch (e) {
+ console.error(e);
+ }
+ };
+}
+
+function reloadTemporaryExtension(id) {
+ return async ({ dispatch, getState }) => {
+ dispatch({ type: TEMPORARY_EXTENSION_RELOAD_START, id });
+ const clientWrapper = getCurrentClient(getState().runtimes);
+
+ try {
+ const addonTargetFront = await clientWrapper.getAddon({ id });
+ await addonTargetFront.reload();
+ dispatch({ type: TEMPORARY_EXTENSION_RELOAD_SUCCESS, id });
+ } catch (e) {
+ const error = typeof e === "string" ? new Error(e) : e;
+ dispatch({ type: TEMPORARY_EXTENSION_RELOAD_FAILURE, id, error });
+ }
+ };
+}
+
+function removeTemporaryExtension(id) {
+ return async ({ getState }) => {
+ const clientWrapper = getCurrentClient(getState().runtimes);
+
+ try {
+ await clientWrapper.uninstallAddon({ id });
+ } catch (e) {
+ console.error(e);
+ }
+ };
+}
+
+function terminateExtensionBackgroundScript(id) {
+ return async ({ dispatch, getState }) => {
+ dispatch({ type: TERMINATE_EXTENSION_BGSCRIPT_START, id });
+ const clientWrapper = getCurrentClient(getState().runtimes);
+
+ try {
+ const addonTargetFront = await clientWrapper.getAddon({ id });
+ await addonTargetFront.terminateBackgroundScript();
+ dispatch({ type: TERMINATE_EXTENSION_BGSCRIPT_SUCCESS, id });
+ } catch (e) {
+ const error = typeof e === "string" ? new Error(e) : e;
+ dispatch({ type: TERMINATE_EXTENSION_BGSCRIPT_FAILURE, id, error });
+ }
+ };
+}
+
+function requestTabs() {
+ return async ({ dispatch, getState }) => {
+ dispatch({ type: REQUEST_TABS_START });
+
+ const runtime = getCurrentRuntime(getState().runtimes);
+ const clientWrapper = getCurrentClient(getState().runtimes);
+
+ try {
+ const isSupported = isSupportedDebugTargetPane(
+ runtime.runtimeDetails.info.type,
+ DEBUG_TARGET_PANE.TAB
+ );
+ const tabs = isSupported ? await clientWrapper.listTabs() : [];
+
+ // Fetch the favicon for all tabs.
+ await Promise.all(
+ tabs.map(descriptorFront => descriptorFront.retrieveFavicon())
+ );
+
+ dispatch({ type: REQUEST_TABS_SUCCESS, tabs });
+ } catch (e) {
+ dispatch({ type: REQUEST_TABS_FAILURE, error: e });
+ }
+ };
+}
+
+function requestExtensions() {
+ return async ({ dispatch, getState }) => {
+ dispatch({ type: REQUEST_EXTENSIONS_START });
+
+ const runtime = getCurrentRuntime(getState().runtimes);
+ const clientWrapper = getCurrentClient(getState().runtimes);
+
+ try {
+ const isIconDataURLRequired = runtime.type !== RUNTIMES.THIS_FIREFOX;
+ const addons = await clientWrapper.listAddons({
+ iconDataURL: isIconDataURLRequired,
+ });
+
+ const showHiddenAddons = getState().ui.showHiddenAddons;
+
+ // Filter out non-debuggable addons as well as hidden ones, unless the dedicated
+ // preference is set to true.
+ const extensions = addons.filter(
+ a => a.debuggable && (!a.hidden || showHiddenAddons)
+ );
+
+ const installedExtensions = extensions.filter(
+ e => !e.temporarilyInstalled
+ );
+ const temporaryExtensions = extensions.filter(
+ e => e.temporarilyInstalled
+ );
+
+ dispatch({
+ type: REQUEST_EXTENSIONS_SUCCESS,
+ installedExtensions,
+ temporaryExtensions,
+ });
+ } catch (e) {
+ dispatch({ type: REQUEST_EXTENSIONS_FAILURE, error: e });
+ }
+ };
+}
+
+function requestProcesses() {
+ return async ({ dispatch, getState }) => {
+ dispatch({ type: REQUEST_PROCESSES_START });
+
+ const clientWrapper = getCurrentClient(getState().runtimes);
+
+ try {
+ const mainProcessDescriptorFront = await clientWrapper.getMainProcess();
+ dispatch({
+ type: REQUEST_PROCESSES_SUCCESS,
+ mainProcess: {
+ id: 0,
+ processFront: mainProcessDescriptorFront,
+ },
+ });
+ } catch (e) {
+ dispatch({ type: REQUEST_PROCESSES_FAILURE, error: e });
+ }
+ };
+}
+
+function requestWorkers() {
+ return async ({ dispatch, getState }) => {
+ dispatch({ type: REQUEST_WORKERS_START });
+
+ const clientWrapper = getCurrentClient(getState().runtimes);
+
+ try {
+ const { otherWorkers, serviceWorkers, sharedWorkers } =
+ await clientWrapper.listWorkers();
+
+ for (const serviceWorker of serviceWorkers) {
+ const { registrationFront } = serviceWorker;
+ if (!registrationFront) {
+ continue;
+ }
+
+ const subscription = await registrationFront.getPushSubscription();
+ serviceWorker.subscription = subscription;
+ }
+
+ dispatch({
+ type: REQUEST_WORKERS_SUCCESS,
+ otherWorkers,
+ serviceWorkers,
+ sharedWorkers,
+ });
+ } catch (e) {
+ dispatch({ type: REQUEST_WORKERS_FAILURE, error: e });
+ }
+ };
+}
+
+function startServiceWorker(registrationFront) {
+ return async () => {
+ try {
+ await registrationFront.start();
+ } catch (e) {
+ console.error(e);
+ }
+ };
+}
+
+function unregisterServiceWorker(registrationFront) {
+ return async () => {
+ try {
+ await registrationFront.unregister();
+ } catch (e) {
+ console.error(e);
+ }
+ };
+}
+
+module.exports = {
+ inspectDebugTarget,
+ installTemporaryExtension,
+ pushServiceWorker,
+ reloadTemporaryExtension,
+ removeTemporaryExtension,
+ requestTabs,
+ requestExtensions,
+ requestProcesses,
+ requestWorkers,
+ startServiceWorker,
+ terminateExtensionBackgroundScript,
+ unregisterServiceWorker,
+};
diff --git a/devtools/client/aboutdebugging/src/actions/index.js b/devtools/client/aboutdebugging/src/actions/index.js
new file mode 100644
index 0000000000..797d2c5831
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/actions/index.js
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const debugTargets = require("resource://devtools/client/aboutdebugging/src/actions/debug-targets.js");
+const runtimes = require("resource://devtools/client/aboutdebugging/src/actions/runtimes.js");
+const telemetry = require("resource://devtools/client/aboutdebugging/src/actions/telemetry.js");
+const ui = require("resource://devtools/client/aboutdebugging/src/actions/ui.js");
+
+Object.assign(exports, ui, runtimes, telemetry, debugTargets);
diff --git a/devtools/client/aboutdebugging/src/actions/moz.build b/devtools/client/aboutdebugging/src/actions/moz.build
new file mode 100644
index 0000000000..a750640d06
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/actions/moz.build
@@ -0,0 +1,11 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ "debug-targets.js",
+ "index.js",
+ "runtimes.js",
+ "telemetry.js",
+ "ui.js",
+)
diff --git a/devtools/client/aboutdebugging/src/actions/runtimes.js b/devtools/client/aboutdebugging/src/actions/runtimes.js
new file mode 100644
index 0000000000..fba620951e
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/actions/runtimes.js
@@ -0,0 +1,515 @@
+/* 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 Actions = require("resource://devtools/client/aboutdebugging/src/actions/index.js");
+
+const {
+ getAllRuntimes,
+ getCurrentRuntime,
+ findRuntimeById,
+} = require("resource://devtools/client/aboutdebugging/src/modules/runtimes-state-helper.js");
+
+const {
+ l10n,
+} = require("resource://devtools/client/aboutdebugging/src/modules/l10n.js");
+const {
+ setDefaultPreferencesIfNeeded,
+ DEFAULT_PREFERENCES,
+} = require("resource://devtools/client/aboutdebugging/src/modules/runtime-default-preferences.js");
+const {
+ createClientForRuntime,
+} = require("resource://devtools/client/aboutdebugging/src/modules/runtime-client-factory.js");
+const {
+ isSupportedDebugTargetPane,
+} = require("resource://devtools/client/aboutdebugging/src/modules/debug-target-support.js");
+
+const {
+ remoteClientManager,
+} = require("resource://devtools/client/shared/remote-debugging/remote-client-manager.js");
+
+const {
+ CONNECT_RUNTIME_CANCEL,
+ CONNECT_RUNTIME_FAILURE,
+ CONNECT_RUNTIME_NOT_RESPONDING,
+ CONNECT_RUNTIME_START,
+ CONNECT_RUNTIME_SUCCESS,
+ DEBUG_TARGET_PANE,
+ DISCONNECT_RUNTIME_FAILURE,
+ DISCONNECT_RUNTIME_START,
+ DISCONNECT_RUNTIME_SUCCESS,
+ PAGE_TYPES,
+ REMOTE_RUNTIMES_UPDATED,
+ RUNTIME_PREFERENCE,
+ RUNTIMES,
+ THIS_FIREFOX_RUNTIME_CREATED,
+ UNWATCH_RUNTIME_FAILURE,
+ UNWATCH_RUNTIME_START,
+ UNWATCH_RUNTIME_SUCCESS,
+ UPDATE_CONNECTION_PROMPT_SETTING_FAILURE,
+ UPDATE_CONNECTION_PROMPT_SETTING_START,
+ UPDATE_CONNECTION_PROMPT_SETTING_SUCCESS,
+ WATCH_RUNTIME_FAILURE,
+ WATCH_RUNTIME_START,
+ WATCH_RUNTIME_SUCCESS,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+const CONNECTION_TIMING_OUT_DELAY = 3000;
+const CONNECTION_CANCEL_DELAY = 13000;
+
+async function getRuntimeIcon(runtime, channel) {
+ if (runtime.isFenix) {
+ switch (channel) {
+ case "release":
+ case "beta":
+ return "chrome://devtools/skin/images/aboutdebugging-fenix.svg";
+ case "aurora":
+ default:
+ return "chrome://devtools/skin/images/aboutdebugging-fenix-nightly.svg";
+ }
+ }
+
+ return channel === "release" || channel === "beta" || channel === "aurora"
+ ? `chrome://devtools/skin/images/aboutdebugging-firefox-${channel}.svg`
+ : "chrome://devtools/skin/images/aboutdebugging-firefox-nightly.svg";
+}
+
+function onRemoteDevToolsClientClosed() {
+ window.AboutDebugging.onNetworkLocationsUpdated();
+ window.AboutDebugging.onUSBRuntimesUpdated();
+}
+
+function connectRuntime(id) {
+ // Create a random connection id to track the connection attempt in telemetry.
+ const connectionId = (Math.random() * 100000) | 0;
+
+ return async ({ dispatch, getState }) => {
+ dispatch({ type: CONNECT_RUNTIME_START, connectionId, id });
+
+ // The preferences test-connection-timing-out-delay and test-connection-cancel-delay
+ // don't have a default value but will be overridden during our tests.
+ const connectionTimingOutDelay = Services.prefs.getIntPref(
+ "devtools.aboutdebugging.test-connection-timing-out-delay",
+ CONNECTION_TIMING_OUT_DELAY
+ );
+ const connectionCancelDelay = Services.prefs.getIntPref(
+ "devtools.aboutdebugging.test-connection-cancel-delay",
+ CONNECTION_CANCEL_DELAY
+ );
+
+ const connectionNotRespondingTimer = setTimeout(() => {
+ // If connecting to the runtime takes time over CONNECTION_TIMING_OUT_DELAY,
+ // we assume the connection prompt is showing on the runtime, show a dialog
+ // to let user know that.
+ dispatch({ type: CONNECT_RUNTIME_NOT_RESPONDING, connectionId, id });
+ }, connectionTimingOutDelay);
+ const connectionCancelTimer = setTimeout(() => {
+ // Connect button of the runtime will be disabled during connection, but the status
+ // continues till the connection was either succeed or failed. This may have a
+ // possibility that the disabling continues unless page reloading, user will not be
+ // able to click again. To avoid this, revert the connect button status after
+ // CONNECTION_CANCEL_DELAY ms.
+ dispatch({ type: CONNECT_RUNTIME_CANCEL, connectionId, id });
+ }, connectionCancelDelay);
+
+ try {
+ const runtime = findRuntimeById(id, getState().runtimes);
+ const clientWrapper = await createClientForRuntime(runtime);
+
+ await setDefaultPreferencesIfNeeded(clientWrapper, DEFAULT_PREFERENCES);
+
+ const deviceDescription = await clientWrapper.getDeviceDescription();
+ const compatibilityReport =
+ await clientWrapper.checkVersionCompatibility();
+ const icon = await getRuntimeIcon(runtime, deviceDescription.channel);
+
+ const {
+ CONNECTION_PROMPT,
+ PERMANENT_PRIVATE_BROWSING,
+ SERVICE_WORKERS_ENABLED,
+ } = RUNTIME_PREFERENCE;
+ const connectionPromptEnabled = await clientWrapper.getPreference(
+ CONNECTION_PROMPT,
+ false
+ );
+ const privateBrowsing = await clientWrapper.getPreference(
+ PERMANENT_PRIVATE_BROWSING,
+ false
+ );
+ const serviceWorkersEnabled = await clientWrapper.getPreference(
+ SERVICE_WORKERS_ENABLED,
+ true
+ );
+ const serviceWorkersAvailable = serviceWorkersEnabled && !privateBrowsing;
+
+ // Fenix specific workarounds are needed until we can get proper server side APIs
+ // to detect Fenix and get the proper application names and versions.
+ // See https://github.com/mozilla-mobile/fenix/issues/2016.
+
+ // For Fenix runtimes, the ADB runtime name is more accurate than the one returned
+ // by the Device actor.
+ const runtimeName = runtime.isFenix
+ ? runtime.name
+ : deviceDescription.name;
+
+ // For Fenix runtimes, the version we should display is the application version
+ // retrieved from ADB, and not the Gecko version returned by the Device actor.
+ const version = runtime.isFenix
+ ? runtime.extra.adbPackageVersion
+ : deviceDescription.version;
+
+ const runtimeDetails = {
+ canDebugServiceWorkers: deviceDescription.canDebugServiceWorkers,
+ clientWrapper,
+ compatibilityReport,
+ connectionPromptEnabled,
+ info: {
+ deviceName: deviceDescription.deviceName,
+ icon,
+ isFenix: runtime.isFenix,
+ name: runtimeName,
+ os: deviceDescription.os,
+ type: runtime.type,
+ version,
+ },
+ serviceWorkersAvailable,
+ };
+
+ if (runtime.type !== RUNTIMES.THIS_FIREFOX) {
+ // `closed` event will be emitted when disabling remote debugging
+ // on the connected remote runtime.
+ clientWrapper.once("closed", onRemoteDevToolsClientClosed);
+ }
+
+ dispatch({
+ type: CONNECT_RUNTIME_SUCCESS,
+ connectionId,
+ runtime: {
+ id,
+ runtimeDetails,
+ type: runtime.type,
+ },
+ });
+ } catch (e) {
+ dispatch({ type: CONNECT_RUNTIME_FAILURE, connectionId, id, error: e });
+ } finally {
+ clearTimeout(connectionNotRespondingTimer);
+ clearTimeout(connectionCancelTimer);
+ }
+ };
+}
+
+function createThisFirefoxRuntime() {
+ return ({ dispatch, getState }) => {
+ const thisFirefoxRuntime = {
+ id: RUNTIMES.THIS_FIREFOX,
+ isConnecting: false,
+ isConnectionFailed: false,
+ isConnectionNotResponding: false,
+ isConnectionTimeout: false,
+ isUnavailable: false,
+ isUnplugged: false,
+ name: l10n.getString("about-debugging-this-firefox-runtime-name"),
+ type: RUNTIMES.THIS_FIREFOX,
+ };
+ dispatch({
+ type: THIS_FIREFOX_RUNTIME_CREATED,
+ runtime: thisFirefoxRuntime,
+ });
+ };
+}
+
+function disconnectRuntime(id, shouldRedirect = false) {
+ return async ({ dispatch, getState }) => {
+ dispatch({ type: DISCONNECT_RUNTIME_START });
+ try {
+ const runtime = findRuntimeById(id, getState().runtimes);
+ const { clientWrapper } = runtime.runtimeDetails;
+
+ if (runtime.type !== RUNTIMES.THIS_FIREFOX) {
+ clientWrapper.off("closed", onRemoteDevToolsClientClosed);
+ }
+ await clientWrapper.close();
+ if (shouldRedirect) {
+ await dispatch(
+ Actions.selectPage(PAGE_TYPES.RUNTIME, RUNTIMES.THIS_FIREFOX)
+ );
+ }
+
+ dispatch({
+ type: DISCONNECT_RUNTIME_SUCCESS,
+ runtime: {
+ id,
+ type: runtime.type,
+ },
+ });
+ } catch (e) {
+ dispatch({ type: DISCONNECT_RUNTIME_FAILURE, error: e });
+ }
+ };
+}
+
+function updateConnectionPromptSetting(connectionPromptEnabled) {
+ return async ({ dispatch, getState }) => {
+ dispatch({ type: UPDATE_CONNECTION_PROMPT_SETTING_START });
+ try {
+ const runtime = getCurrentRuntime(getState().runtimes);
+ const { clientWrapper } = runtime.runtimeDetails;
+ const promptPrefName = RUNTIME_PREFERENCE.CONNECTION_PROMPT;
+ await clientWrapper.setPreference(
+ promptPrefName,
+ connectionPromptEnabled
+ );
+ // Re-get actual value from the runtime.
+ connectionPromptEnabled = await clientWrapper.getPreference(
+ promptPrefName,
+ connectionPromptEnabled
+ );
+
+ dispatch({
+ type: UPDATE_CONNECTION_PROMPT_SETTING_SUCCESS,
+ connectionPromptEnabled,
+ runtime,
+ });
+ } catch (e) {
+ dispatch({ type: UPDATE_CONNECTION_PROMPT_SETTING_FAILURE, error: e });
+ }
+ };
+}
+
+function watchRuntime(id) {
+ return async ({ dispatch, getState }) => {
+ dispatch({ type: WATCH_RUNTIME_START });
+
+ try {
+ if (id === RUNTIMES.THIS_FIREFOX) {
+ // THIS_FIREFOX connects and disconnects on the fly when opening the page.
+ await dispatch(connectRuntime(RUNTIMES.THIS_FIREFOX));
+ }
+
+ // The selected runtime should already have a connected client assigned.
+ const runtime = findRuntimeById(id, getState().runtimes);
+ await dispatch({ type: WATCH_RUNTIME_SUCCESS, runtime });
+
+ dispatch(Actions.requestExtensions());
+ // we have to wait for tabs, otherwise the requests to getTarget may interfer
+ // with listProcesses
+ await dispatch(Actions.requestTabs());
+ dispatch(Actions.requestWorkers());
+
+ if (
+ isSupportedDebugTargetPane(
+ runtime.runtimeDetails.info.type,
+ DEBUG_TARGET_PANE.PROCESSES
+ )
+ ) {
+ dispatch(Actions.requestProcesses());
+ }
+ } catch (e) {
+ dispatch({ type: WATCH_RUNTIME_FAILURE, error: e });
+ }
+ };
+}
+
+function unwatchRuntime(id) {
+ return async ({ dispatch, getState }) => {
+ const runtime = findRuntimeById(id, getState().runtimes);
+
+ dispatch({ type: UNWATCH_RUNTIME_START, runtime });
+
+ try {
+ if (id === RUNTIMES.THIS_FIREFOX) {
+ // THIS_FIREFOX connects and disconnects on the fly when opening the page.
+ await dispatch(disconnectRuntime(RUNTIMES.THIS_FIREFOX));
+ }
+
+ dispatch({ type: UNWATCH_RUNTIME_SUCCESS });
+ } catch (e) {
+ dispatch({ type: UNWATCH_RUNTIME_FAILURE, error: e });
+ }
+ };
+}
+
+function updateNetworkRuntimes(locations) {
+ const runtimes = locations.map(location => {
+ const [host, port] = location.split(":");
+ return {
+ id: location,
+ extra: {
+ connectionParameters: { host, port: parseInt(port, 10) },
+ },
+ isConnecting: false,
+ isConnectionFailed: false,
+ isConnectionNotResponding: false,
+ isConnectionTimeout: false,
+ isFenix: false,
+ isUnavailable: false,
+ isUnplugged: false,
+ isUnknown: false,
+ name: location,
+ type: RUNTIMES.NETWORK,
+ };
+ });
+ return updateRemoteRuntimes(runtimes, RUNTIMES.NETWORK);
+}
+
+function updateUSBRuntimes(adbRuntimes) {
+ const runtimes = adbRuntimes.map(adbRuntime => {
+ // Set connectionParameters only for known runtimes.
+ const socketPath = adbRuntime.socketPath;
+ const deviceId = adbRuntime.deviceId;
+ const connectionParameters = socketPath ? { deviceId, socketPath } : null;
+ return {
+ id: adbRuntime.id,
+ extra: {
+ connectionParameters,
+ deviceName: adbRuntime.deviceName,
+ adbPackageVersion: adbRuntime.versionName,
+ },
+ isConnecting: false,
+ isConnectionFailed: false,
+ isConnectionNotResponding: false,
+ isConnectionTimeout: false,
+ isFenix: adbRuntime.isFenix,
+ isUnavailable: adbRuntime.isUnavailable,
+ isUnplugged: adbRuntime.isUnplugged,
+ name: adbRuntime.shortName,
+ type: RUNTIMES.USB,
+ };
+ });
+ return updateRemoteRuntimes(runtimes, RUNTIMES.USB);
+}
+
+/**
+ * Check that a given runtime can still be found in the provided array of runtimes, and
+ * that the connection of the associated DevToolsClient is still valid.
+ * Note that this check is only valid for runtimes which match the type of the runtimes
+ * in the array.
+ */
+function _isRuntimeValid(runtime, runtimes) {
+ const isRuntimeAvailable = runtimes.some(r => r.id === runtime.id);
+ const isConnectionValid =
+ runtime.runtimeDetails && !runtime.runtimeDetails.clientWrapper.isClosed();
+ return isRuntimeAvailable && isConnectionValid;
+}
+
+function updateRemoteRuntimes(runtimes, type) {
+ return async ({ dispatch, getState }) => {
+ const currentRuntime = getCurrentRuntime(getState().runtimes);
+
+ // Check if the updated remote runtimes should trigger a navigation out of the current
+ // runtime page.
+ if (
+ currentRuntime &&
+ currentRuntime.type === type &&
+ !_isRuntimeValid(currentRuntime, runtimes)
+ ) {
+ // Since current remote runtime is invalid, move to this firefox page.
+ // This case is considered as followings and so on:
+ // * Remove ADB addon
+ // * (Physically) Disconnect USB runtime
+ //
+ // The reason we call selectPage before REMOTE_RUNTIMES_UPDATED is fired is below.
+ // Current runtime can not be retrieved after REMOTE_RUNTIMES_UPDATED action, since
+ // that updates runtime state. So, before that we fire selectPage action to execute
+ // `unwatchRuntime` correctly.
+ await dispatch(
+ Actions.selectPage(PAGE_TYPES.RUNTIME, RUNTIMES.THIS_FIREFOX)
+ );
+ }
+
+ // For existing runtimes, transfer all properties that are not available in the
+ // runtime objects passed to this method:
+ // - runtimeDetails (set by about:debugging after a successful connection)
+ // - isConnecting (set by about:debugging during the connection)
+ // - isConnectionFailed (set by about:debugging if connection was failed)
+ // - isConnectionNotResponding
+ // (set by about:debugging if connection is taking too much time)
+ // - isConnectionTimeout (set by about:debugging if connection was timeout)
+ runtimes.forEach(runtime => {
+ const existingRuntime = findRuntimeById(runtime.id, getState().runtimes);
+ const isConnectionValid =
+ existingRuntime?.runtimeDetails &&
+ !existingRuntime.runtimeDetails.clientWrapper.isClosed();
+ runtime.runtimeDetails = isConnectionValid
+ ? existingRuntime.runtimeDetails
+ : null;
+ runtime.isConnecting = existingRuntime
+ ? existingRuntime.isConnecting
+ : false;
+ runtime.isConnectionFailed = existingRuntime
+ ? existingRuntime.isConnectionFailed
+ : false;
+ runtime.isConnectionNotResponding = existingRuntime
+ ? existingRuntime.isConnectionNotResponding
+ : false;
+ runtime.isConnectionTimeout = existingRuntime
+ ? existingRuntime.isConnectionTimeout
+ : false;
+ });
+
+ const existingRuntimes = getAllRuntimes(getState().runtimes);
+ for (const runtime of existingRuntimes) {
+ // Runtime was connected before.
+ const isConnected = runtime.runtimeDetails;
+ // Runtime is of the same type as the updated runtimes array, so we should check it.
+ const isSameType = runtime.type === type;
+ if (isConnected && isSameType && !_isRuntimeValid(runtime, runtimes)) {
+ // Disconnect runtimes that were no longer valid.
+ await dispatch(disconnectRuntime(runtime.id));
+ }
+ }
+
+ dispatch({ type: REMOTE_RUNTIMES_UPDATED, runtimes, runtimeType: type });
+
+ for (const runtime of getAllRuntimes(getState().runtimes)) {
+ if (runtime.type !== type) {
+ continue;
+ }
+
+ // Reconnect clients already available in the RemoteClientManager.
+ const isConnected = !!runtime.runtimeDetails;
+ const hasConnectedClient = remoteClientManager.hasClient(
+ runtime.id,
+ runtime.type
+ );
+ if (!isConnected && hasConnectedClient) {
+ await dispatch(connectRuntime(runtime.id));
+ }
+ }
+ };
+}
+
+/**
+ * Remove all the listeners added on client objects. Since those objects are persisted
+ * regardless of the about:debugging lifecycle, all the added events should be removed
+ * before leaving about:debugging.
+ */
+function removeRuntimeListeners() {
+ return ({ dispatch, getState }) => {
+ const allRuntimes = getAllRuntimes(getState().runtimes);
+ const remoteRuntimes = allRuntimes.filter(
+ r => r.type !== RUNTIMES.THIS_FIREFOX
+ );
+ for (const runtime of remoteRuntimes) {
+ if (runtime.runtimeDetails) {
+ const { clientWrapper } = runtime.runtimeDetails;
+ clientWrapper.off("closed", onRemoteDevToolsClientClosed);
+ }
+ }
+ };
+}
+
+module.exports = {
+ connectRuntime,
+ createThisFirefoxRuntime,
+ disconnectRuntime,
+ removeRuntimeListeners,
+ unwatchRuntime,
+ updateConnectionPromptSetting,
+ updateNetworkRuntimes,
+ updateUSBRuntimes,
+ watchRuntime,
+};
diff --git a/devtools/client/aboutdebugging/src/actions/telemetry.js b/devtools/client/aboutdebugging/src/actions/telemetry.js
new file mode 100644
index 0000000000..b418c77a50
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/actions/telemetry.js
@@ -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/. */
+
+"use strict";
+
+const {
+ TELEMETRY_RECORD,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+/**
+ * If a given event cannot be mapped to an existing action, use this action that will only
+ * be processed by the event recording middleware.
+ */
+function recordTelemetryEvent(method, details) {
+ return ({ dispatch, getState }) => {
+ dispatch({ type: TELEMETRY_RECORD, method, details });
+ };
+}
+
+module.exports = {
+ recordTelemetryEvent,
+};
diff --git a/devtools/client/aboutdebugging/src/actions/ui.js b/devtools/client/aboutdebugging/src/actions/ui.js
new file mode 100644
index 0000000000..fb676cefd6
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/actions/ui.js
@@ -0,0 +1,202 @@
+/* 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 {
+ ADB_ADDON_INSTALL_START,
+ ADB_ADDON_INSTALL_SUCCESS,
+ ADB_ADDON_INSTALL_FAILURE,
+ ADB_ADDON_UNINSTALL_START,
+ ADB_ADDON_UNINSTALL_SUCCESS,
+ ADB_ADDON_UNINSTALL_FAILURE,
+ ADB_ADDON_STATUS_UPDATED,
+ ADB_READY_UPDATED,
+ DEBUG_TARGET_COLLAPSIBILITY_UPDATED,
+ HIDE_PROFILER_DIALOG,
+ NETWORK_LOCATIONS_UPDATE_FAILURE,
+ NETWORK_LOCATIONS_UPDATE_START,
+ NETWORK_LOCATIONS_UPDATE_SUCCESS,
+ PAGE_TYPES,
+ SELECT_PAGE_FAILURE,
+ SELECT_PAGE_START,
+ SELECT_PAGE_SUCCESS,
+ SELECTED_RUNTIME_ID_UPDATED,
+ SHOW_PROFILER_DIALOG,
+ SWITCH_PROFILER_CONTEXT,
+ USB_RUNTIMES_SCAN_START,
+ USB_RUNTIMES_SCAN_SUCCESS,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+const NetworkLocationsModule = require("resource://devtools/client/aboutdebugging/src/modules/network-locations.js");
+const {
+ adbAddon,
+} = require("resource://devtools/client/shared/remote-debugging/adb/adb-addon.js");
+const {
+ refreshUSBRuntimes,
+} = require("resource://devtools/client/aboutdebugging/src/modules/usb-runtimes.js");
+
+const Actions = require("resource://devtools/client/aboutdebugging/src/actions/index.js");
+
+function selectPage(page, runtimeId) {
+ return async ({ dispatch, getState }) => {
+ dispatch({ type: SELECT_PAGE_START });
+
+ try {
+ const isSamePage = (oldPage, newPage) => {
+ if (newPage === PAGE_TYPES.RUNTIME && oldPage === PAGE_TYPES.RUNTIME) {
+ return runtimeId === getState().runtimes.selectedRuntimeId;
+ }
+ return newPage === oldPage;
+ };
+
+ if (!page) {
+ throw new Error("No page provided.");
+ }
+
+ const currentPage = getState().ui.selectedPage;
+ // Nothing to dispatch if the page is the same as the current page
+ if (isSamePage(currentPage, page)) {
+ return;
+ }
+
+ // Stop showing the profiler dialog if we are navigating to another page.
+ if (getState().ui.showProfilerDialog) {
+ await dispatch({ type: HIDE_PROFILER_DIALOG });
+ }
+
+ // Stop watching current runtime, if currently on a RUNTIME page.
+ if (currentPage === PAGE_TYPES.RUNTIME) {
+ const currentRuntimeId = getState().runtimes.selectedRuntimeId;
+ await dispatch(Actions.unwatchRuntime(currentRuntimeId));
+ }
+
+ // Always update the selected runtime id.
+ // If we are navigating to a non-runtime page, the Runtime page components are no
+ // longer rendered so it is safe to nullify the runtimeId.
+ // If we are navigating to a runtime page, the runtime corresponding to runtimeId
+ // is already connected, so components can safely get runtimeDetails on this new
+ // runtime.
+ dispatch({ type: SELECTED_RUNTIME_ID_UPDATED, runtimeId });
+
+ // Start watching current runtime, if moving to a RUNTIME page.
+ if (page === PAGE_TYPES.RUNTIME) {
+ await dispatch(Actions.watchRuntime(runtimeId));
+ }
+
+ dispatch({ type: SELECT_PAGE_SUCCESS, page });
+ } catch (e) {
+ dispatch({ type: SELECT_PAGE_FAILURE, error: e });
+ }
+ };
+}
+
+function updateDebugTargetCollapsibility(key, isCollapsed) {
+ return { type: DEBUG_TARGET_COLLAPSIBILITY_UPDATED, key, isCollapsed };
+}
+
+function addNetworkLocation(location) {
+ return ({ dispatch, getState }) => {
+ NetworkLocationsModule.addNetworkLocation(location);
+ };
+}
+
+function removeNetworkLocation(location) {
+ return ({ dispatch, getState }) => {
+ NetworkLocationsModule.removeNetworkLocation(location);
+ };
+}
+
+function showProfilerDialog() {
+ return { type: SHOW_PROFILER_DIALOG };
+}
+
+/**
+ * The profiler can switch between "devtools-remote" and "aboutprofiling-remote"
+ * page contexts.
+ */
+function switchProfilerContext(profilerContext) {
+ return { type: SWITCH_PROFILER_CONTEXT, profilerContext };
+}
+
+function hideProfilerDialog() {
+ return { type: HIDE_PROFILER_DIALOG };
+}
+
+function updateAdbAddonStatus(adbAddonStatus) {
+ return { type: ADB_ADDON_STATUS_UPDATED, adbAddonStatus };
+}
+
+function updateAdbReady(isAdbReady) {
+ return { type: ADB_READY_UPDATED, isAdbReady };
+}
+
+function updateNetworkLocations(locations) {
+ return async ({ dispatch, getState }) => {
+ dispatch({ type: NETWORK_LOCATIONS_UPDATE_START });
+ try {
+ await dispatch(Actions.updateNetworkRuntimes(locations));
+ dispatch({ type: NETWORK_LOCATIONS_UPDATE_SUCCESS, locations });
+ } catch (e) {
+ dispatch({ type: NETWORK_LOCATIONS_UPDATE_FAILURE, error: e });
+ }
+ };
+}
+
+function installAdbAddon() {
+ return async ({ dispatch, getState }) => {
+ dispatch({ type: ADB_ADDON_INSTALL_START });
+
+ try {
+ // "aboutdebugging" will be forwarded to telemetry as the installation source
+ // for the addon.
+ await adbAddon.install("about:debugging");
+ dispatch({ type: ADB_ADDON_INSTALL_SUCCESS });
+ } catch (e) {
+ dispatch({ type: ADB_ADDON_INSTALL_FAILURE, error: e });
+ }
+ };
+}
+
+function uninstallAdbAddon() {
+ return async ({ dispatch, getState }) => {
+ dispatch({ type: ADB_ADDON_UNINSTALL_START });
+
+ try {
+ await adbAddon.uninstall();
+ dispatch({ type: ADB_ADDON_UNINSTALL_SUCCESS });
+ } catch (e) {
+ dispatch({ type: ADB_ADDON_UNINSTALL_FAILURE, error: e });
+ }
+ };
+}
+
+function scanUSBRuntimes() {
+ return async ({ dispatch, getState }) => {
+ // do not re-scan if we are already doing it
+ if (getState().ui.isScanningUsb) {
+ return;
+ }
+
+ dispatch({ type: USB_RUNTIMES_SCAN_START });
+ await refreshUSBRuntimes();
+ dispatch({ type: USB_RUNTIMES_SCAN_SUCCESS });
+ };
+}
+
+module.exports = {
+ addNetworkLocation,
+ hideProfilerDialog,
+ installAdbAddon,
+ removeNetworkLocation,
+ scanUSBRuntimes,
+ selectPage,
+ showProfilerDialog,
+ switchProfilerContext,
+ uninstallAdbAddon,
+ updateAdbAddonStatus,
+ updateAdbReady,
+ updateDebugTargetCollapsibility,
+ updateNetworkLocations,
+};
diff --git a/devtools/client/aboutdebugging/src/base.css b/devtools/client/aboutdebugging/src/base.css
new file mode 100644
index 0000000000..728cbeed0c
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/base.css
@@ -0,0 +1,521 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+:root {
+ /* Colors from common.css */
+ --in-content-background-color: f9f9fa;
+ --in-content-border-color: #d7d7db;
+ --in-content-primary-button-background: rgb(0, 97, 224);
+ --in-content-primary-button-background-active: rgb(5, 62, 148);
+ --in-content-primary-button-background-hover: rgb(2, 80, 187);
+ --in-content-text-color: #0c0c0d;
+
+ --bg-color: var(--in-content-background-color);
+ --text-color: var(--in-content-text-color);
+ --secondary-text-color: var(--grey-50);
+
+ --border-color: var(--in-content-border-color);
+
+ --box-background: #fff;
+ --box-border-color: var(--in-content-border-color);
+
+ --button-background-color: var(--grey-90-a10); /* Note: this is from Photon Default button */
+ --button-color: var(--grey-90); /* Note: this is from Photon Default button */
+ --button-hover-background-color: var(--grey-90-a20); /* Note: this is from Photon Default button */
+ --button-active-background-color: var(--grey-90-a30); /* Note: this is from Photon Default button */
+
+ --category-background-hover: rgba(12,12,13,0.1);
+ --category-text: rgba(12,12,13);
+ --category-text-selected: var(--in-content-primary-button-background);
+
+ --fieldpair-text-color: var(--grey-50);
+
+ --sidebar-text-color: var(--category-text);
+ --sidebar-selected-color: var(--category-text-selected);
+ --sidebar-background-hover: var(--category-background-hover);
+
+ --card-background-color: var(--white-100);
+ --card-separator-color: var(--grey-20);
+
+ /* Dimensions from common.css #categories > .category */
+ /* TODO: Values are not based on photon's 4px base distance, see bug 1501638 */
+ --category-height: 48px;
+ --category-padding: 10px;
+ --category-transition-duration: 150ms;
+
+ --icon-ok-color: var(--green-70);
+ --icon-info-color: var(--grey-90);
+
+ --link-color: var(--in-content-primary-button-background);
+ --link-color-active: var(--in-content-primary-button-background-active);
+ --link-color-hover: var(--in-content-primary-button-background-hover);
+
+ --primary-button-background-color: var(--blue-60);
+ --primary-button-color: var(--white-100);
+ --primary-button-hover-background-color: var(--blue-70);
+ --primary-button-active-background-color: var(--blue-80);
+
+ --popup-header-background-color: var(--grey-20);
+ --popup-header-color: var(--grey-90);
+
+ /* Colors from Photon */
+ --success-background: #30e60b;
+ --warning-background: #fffbd6; /* from the Web Console */
+ --warning-border: rgba(164, 127, 0, 0.27); /* yellow-70(#a47f00) at 27% */
+ --warning-icon: var(--yellow-65); /* from the Web Console */
+ --warning-text: var(--yellow-80); /* from the Web Console */
+ --error-background: #fdf2f5; /* from the Web Console */
+ --error-border: rgba(90, 0, 2, 0.16); /* red-80(#5a0002) at 16% */
+ --error-icon: var(--red-60); /* from the Web Console */
+ --error-text: var(--red-70); /* from the Web Console */
+ --highlight-50: #0a84ff;
+ --grey-20: #ededf0; /* for ui, no special semantic */
+ --grey-30: #d7d7db; /* for ui, no special semantic */
+ --grey-50: #737373; /* for ui, no special semantic */
+ --grey-90: #0c0c0d; /* for ui, no special semantic */
+ --grey-90-a10: rgba(12, 12, 13, 0.1);
+ --grey-90-a20: rgba(12, 12, 13, 0.2);
+ --grey-90-a30: rgba(12, 12, 13, 0.3);
+ --grey-90-a60: rgba(12, 12, 13, 0.6);
+ --red-70: #a4000f; /* for ui, no special semantic */
+ --white-100: #fff; /* for ui, no special semantic */
+ --yellow-60: #d7b600; /* for ui, no special semantic */
+ --yellow-70: #a47f00; /* for ui, no special semantic */
+
+ /* Typography from Photon */
+ /* See https://firefox-dev.tools/photon/visuals/typography.html */
+ --body-10-font-size: 13px;
+ --body-10-font-weight: 400;
+ --body-20-font-size: 15px;
+ --body-20-font-weight: 400;
+ --body-20-font-weight-bold: 700;
+ --caption-10-font-size: 11px;
+ --caption-10-font-weight: 400;
+ --caption-20-font-size: 13px;
+ --caption-20-font-weight: 400;
+ --display-10-font-size: 28px;
+ --display-10-font-weight: 200;
+ --title-20-font-size: 17px;
+ --title-20-font-weight: 600;
+ --title-30-font-size: 22px;
+ --title-30-font-weight: 300;
+
+ /* Global layout vars */
+ --page-width: 664px;
+ --base-unit: 4px;
+
+ /* Global styles */
+ --base-font-style: message-box;
+ --base-font-size: var(--body-10-font-size);
+ --base-font-weight: var(--body-10-font-weight);
+ --base-line-height: 1.8;
+ --icon-label-font-size: var(--body-10-font-size);
+ --message-font-size: var(--body-10-font-size);
+ --button-font-size: var(--base-font-size);
+ --micro-font-size: 11px;
+ --monospace-font-family: monospace;
+
+ --card-shadow-blur-radius: var(--base-unit);
+
+
+ /*
+ * Variables particular to about:debugging
+ */
+ --alt-heading-icon-size: calc(var(--base-unit) * 6);
+ --alt-heading-icon-gap: var(--base-unit);
+ --main-heading-icon-size: calc(var(--base-unit) * 17); /* 4px * 17 = 68px */
+ --main-heading-icon-gap: calc(var(--base-unit) * 3);
+ --main-subheading-icon-size: calc(var(--base-unit) * 4);
+ --main-subheading-heading-icon-gap: calc(var(--base-unit) * 2);
+}
+
+/* Dark Theme variables */
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --in-content-background-color: rgb(28, 27, 34);
+ --in-content-border-color: rgba(249,249,250,0.2);
+ --in-content-primary-button-background: #00ddff;
+ --in-content-primary-button-background-active: rgb(170,242,255);
+ --in-content-primary-button-background-hover: rgb(128,235,255);
+ --in-content-text-color: #eee;
+
+ --secondary-text-color: rgb(168, 168, 168);
+
+ --box-background: rgb(35, 34, 43);
+
+ --button-background-color: rgb(72, 72, 84);
+ --button-color: var(--white-100);
+ --button-hover-background-color: rgb(92, 92, 106);
+
+ --category-background-hover: rgba(12,12,13,0.1);
+ --category-text: var(--text-color);
+
+ --fieldpair-text-color: var(--text-color);
+
+ --sidebar-text-color: var(--text-color);
+ --sidebar-background-hover: rgb(92, 92, 106);
+
+ --card-background-color: rgb(35, 34, 43);
+ --card-separator-color: var(--grey-50);
+
+ --icon-ok-color: var(--white-100);
+ --icon-info-color: var(--white-100);
+
+ --popup-header-background-color: var(--grey-50);
+ --popup-header-color: var(--white-100);
+
+ /*
+ * From common.inc.css
+ * https://searchfox.org/mozilla-central/rev/b52cf6bbe214bd9d93ed9333d0403f7d556ad7c8/toolkit/themes/shared/in-content/common.inc.css#165-168
+ */
+ --primary-button-background-color: #00ddff;
+ --primary-button-color: rgb(43,42,51);
+ --primary-button-active-background-color: rgb(170,242,255);
+ --primary-button-hover-background-color: rgb(128,235,255);
+ }
+}
+
+/*
+* Reset some tags
+*/
+
+html {
+ font: var(--base-font-style);
+}
+
+body {
+ margin: 0;
+ padding: 0;
+ color: var(--text-color);
+ font-size: var(--base-font-size);
+ font-weight: var(--base-font-weight);
+ line-height: var(--base-line-height);
+ background: var(--bg-color);
+}
+
+dd {
+ margin: 0;
+ padding: 0;
+}
+
+ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+a {
+ color: var(--link-color);
+}
+a:hover {
+ color: var(--link-color-hover);
+}
+a:active {
+ color: var(--link-color-active);
+}
+
+p, h1 {
+ margin: 0;
+}
+
+/*
+* Utils
+*/
+
+/* text that needs to be cut with … */
+.ellipsis-text {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+/* Technical text that should use a monospace font, such as code, error messages. */
+.technical-text {
+ font-family: var(--monospace-font-family);
+}
+
+/* Links that need to look like current text */
+.undecorated-link,
+.undecorated-link:hover {
+ text-decoration: none;
+ color: currentColor;
+}
+
+/* Text needs to wrap anywhere */
+.word-wrap-anywhere {
+ word-wrap: anywhere;
+}
+
+/*
+* Typography
+*/
+
+/* Main style for heading (i.e. h1) */
+.main-heading {
+ font-size: var(--display-10-font-size);
+ font-weight: var(--display-10-font-weight);
+ line-height: 1.2;
+}
+
+.main-heading__icon {
+ width: 100%;
+}
+
+.main-heading-subtitle {
+ font-size: var(--title-30-font-size);
+ font-weight: var(--title-30-font-weight);
+}
+
+/* Main style for a subheading (i.e. h2). It features an icon */
+/* +--------+-------------+
+* | [Icon] | Lorem ipsum |
+* +--------+-------------+
+*/
+.main-subheading {
+ margin-block: calc(var(--base-unit) * 4) 0;
+ font-size: var(--title-20-font-size); /* Note: this is from Photon Title 20 */
+ font-weight: var(--title-20-font-weight); /* Note: this is from Photon Title 20 */
+
+ display: grid;
+ grid-template-columns: var(--main-subheading-icon-size) 1fr;
+ grid-column-gap: var(--main-subheading-heading-icon-gap);
+ align-items: center;
+}
+
+.main-subheading__icon {
+ width: 100%;
+ fill: currentColor;
+ -moz-context-properties: fill;
+}
+
+/* Alternative style for a heading (i.e. h1) */
+.alt-heading {
+ font-weight: var(--title-20-font-weight);
+ font-size: var(--title-20-font-size);
+
+ margin-block-start: 0;
+ margin-block-end: calc(var(--base-unit) * 4);
+}
+
+.alt-heading--larger {
+ font-size: var(--title-30-font-size);
+ font-weight: var(--title-30-font-weight);
+}
+
+/* Alternative style for a subheading (i.e. h2). It features an icon */
+/* +--------+-------------+
+* | [Icon] | Lorem ipsum |
+* +--------+-------------+
+*/
+.alt-subheading {
+ margin-block-start: calc(var(--base-unit) * 4);
+ font-weight: 600;
+ font-size: 1.14em;
+ line-height: 1.4em; /* odd value - from common.inc.css */
+
+ display: grid;
+ grid-template-columns: var(--alt-heading-icon-size) 1fr;
+ grid-column-gap: var(--alt-heading-icon-gap);
+ align-items: center;
+}
+
+.alt-subheading__icon {
+ width: 100%;
+ fill: currentColor;
+ -moz-context-properties: fill;
+}
+
+/*
+* Layout elements
+*/
+
+/* for horizontal rules / separators */
+.separator {
+ border-style: solid none none none;
+ border-color: var(--border-color);
+}
+
+/* adds breathing space to the separator */
+.separator--breathe {
+ margin: calc(var(--base-unit) * 5) 0;
+}
+
+/* a series of button-like elements, layed out horizontally */
+.toolbar {
+ display: flex;
+ column-gap: calc(var(--base-unit) * 3);
+}
+
+.toolbar--right-align {
+ justify-content: end;
+}
+
+/*
+Form controls
+*/
+.default-button, .default-input {
+ box-sizing: border-box;
+ font-size: 1em;
+}
+
+/* Buttons from Photon */
+.default-button, .primary-button {
+ appearance: none;
+ margin: 0;
+ height: calc(var(--base-unit) * 8);
+ padding-inline-start: calc(var(--base-unit) * 5);
+ padding-inline-end: calc(var(--base-unit) * 5);
+
+ border: none;
+ border-radius: calc(var(--base-unit) / 2);
+
+ font-size: var(--button-font-size);
+}
+
+/* Disabled state for buttons from Photon */
+.default-button:disabled, .primary-button:disabled {
+ opacity: 0.4;
+}
+
+/* Smaller variant size for buttons, from Photon */
+.default-button--micro, .primary-button--micro {
+ padding-inline-start: calc(2 * var(--base-unit));
+ padding-inline-end: calc(2 * var(--base-unit));
+ font-size: var(--micro-font-size);
+ height: calc(var(--base-unit) * 6);
+}
+
+/* Photon button representing a primary action */
+.primary-button {
+ color: var(--primary-button-color);
+ background-color: var(--primary-button-background-color);
+}
+
+.primary-button:enabled:hover {
+ background: var(--primary-button-hover-background-color);
+}
+
+.primary-button:enabled:active {
+ background: var(--primary-button-active-background-color);
+}
+
+/* Photon standard button */
+.default-button {
+ color: var(--button-color);
+ background-color: var(--button-background-color);
+}
+
+.default-button:enabled:hover {
+ background: var(--button-hover-background-color);
+}
+
+.default-button:enabled:active {
+ background: var(--button-active-background-color);
+}
+
+@media (prefers-contrast) {
+ .default-button,
+ .ghost-button,
+ .primary-button {
+ background-color: ButtonFace;
+ /* Add a border to make buttons visible in high contrast */
+ border: 1px solid ButtonText;
+ color: ButtonText;
+ }
+
+ .ghost-button {
+ fill: ButtonText;
+ }
+
+ :is(
+ .default-button,
+ .ghost-button,
+ .primary-button
+ ):enabled:is(:hover, :active) {
+ background-color: ButtonText;
+ color: ButtonFace;
+ }
+}
+
+/* Photon ghost button. Icon button with no background */
+.ghost-button {
+ background: transparent;
+ border: none;
+ border-radius: calc(var(--base-unit) / 2);
+ fill: var(--button-color);
+ height: calc(var(--base-unit) * 6);
+ padding: calc(var(--base-unit));
+ width: calc(var(--base-unit) * 6);
+
+ -moz-context-properties: fill;
+}
+
+.ghost-button:hover {
+ background: var(--button-hover-background-color);
+}
+
+.ghost-button:active {
+ background: var(--button-active-background-color);
+}
+
+/* Standard inputs */
+.default-input {
+ line-height: unset;
+ padding: 0 calc(var(--base-unit) * 2);
+ height: 100%;
+
+ border: 1px solid var(--box-border-color);
+ border-radius: 2px;
+ color: var(--text-color);
+ background-color: var(--box-background);
+}
+
+/*
+* Other UI components
+*/
+
+/*
+* A small, colored badge.
+* NOTE: styles borrowed from Photon's micro buttons (there aren't badges)
+*/
+.badge {
+ background: var(--grey-30);
+ border-radius: calc(var(--base-unit) / 2);
+ font-size: var(--micro-font-size);
+ padding: var(--base-unit) calc(2 * var(--base-unit));
+}
+
+.badge--info {
+ background: var(--highlight-50);
+}
+
+.badge--success {
+ background: var(--success-background);
+}
+
+.badge--warning {
+ background: var(--warning-background);
+}
+
+.badge--error {
+ background: var(--error-background);
+}
+
+/*
+ * Card UI, from Photon
+ */
+.card {
+ background-color: var(--card-background-color); /* from common.inc.css */
+ border-radius: var(--card-shadow-blur-radius); /* from common.inc.css */
+ box-shadow: 0 1px 4px var(--grey-90-a10); /* from common.inc.css */
+ box-sizing: border-box;
+ min-width: min-content;
+ padding-block: calc(var(--base-unit) * 5);
+}
+
+.card__heading {
+ font-size: var(--title-20-font-size); /* Note: this is from Photon Title 20 */
+ font-weight: var(--title-20-font-weight); /* Note: this is from Photon Title 20 */
+}
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",
+)
diff --git a/devtools/client/aboutdebugging/src/constants.js b/devtools/client/aboutdebugging/src/constants.js
new file mode 100644
index 0000000000..fa0b847a32
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/constants.js
@@ -0,0 +1,185 @@
+/* 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 {
+ CONNECTION_TYPES,
+ DEBUG_TARGET_TYPES,
+} = require("resource://devtools/client/shared/remote-debugging/constants.js");
+
+const actionTypes = {
+ ADB_ADDON_INSTALL_START: "ADB_ADDON_INSTALL_START",
+ ADB_ADDON_INSTALL_SUCCESS: "ADB_ADDON_INSTALL_SUCCESS",
+ ADB_ADDON_INSTALL_FAILURE: "ADB_ADDON_INSTALL_FAILURE",
+ ADB_ADDON_UNINSTALL_START: "ADB_ADDON_UNINSTALL_START",
+ ADB_ADDON_UNINSTALL_SUCCESS: "ADB_ADDON_UNINSTALL_SUCCESS",
+ ADB_ADDON_UNINSTALL_FAILURE: "ADB_ADDON_UNINSTALL_FAILURE",
+ ADB_ADDON_STATUS_UPDATED: "ADB_ADDON_STATUS_UPDATED",
+ ADB_READY_UPDATED: "ADB_READY_UPDATED",
+ CONNECT_RUNTIME_CANCEL: "CONNECT_RUNTIME_CANCEL",
+ CONNECT_RUNTIME_FAILURE: "CONNECT_RUNTIME_FAILURE",
+ CONNECT_RUNTIME_NOT_RESPONDING: "CONNECT_RUNTIME_NOT_RESPONDING",
+ CONNECT_RUNTIME_START: "CONNECT_RUNTIME_START",
+ CONNECT_RUNTIME_SUCCESS: "CONNECT_RUNTIME_SUCCESS",
+ DEBUG_TARGET_COLLAPSIBILITY_UPDATED: "DEBUG_TARGET_COLLAPSIBILITY_UPDATED",
+ DISCONNECT_RUNTIME_FAILURE: "DISCONNECT_RUNTIME_FAILURE",
+ DISCONNECT_RUNTIME_START: "DISCONNECT_RUNTIME_START",
+ DISCONNECT_RUNTIME_SUCCESS: "DISCONNECT_RUNTIME_SUCCESS",
+ EXTENSION_BGSCRIPT_STATUS_UPDATED: "EXTENSION_BGSCRIPT_STATUS_UPDATED",
+ HIDE_PROFILER_DIALOG: "HIDE_PROFILER_DIALOG",
+ SWITCH_PROFILER_CONTEXT: "SWITCH_PROFILER_CONTEXT",
+ NETWORK_LOCATIONS_UPDATE_FAILURE: "NETWORK_LOCATIONS_UPDATE_FAILURE",
+ NETWORK_LOCATIONS_UPDATE_START: "NETWORK_LOCATIONS_UPDATE_START",
+ NETWORK_LOCATIONS_UPDATE_SUCCESS: "NETWORK_LOCATIONS_UPDATE_SUCCESS",
+ REMOTE_RUNTIMES_UPDATED: "REMOTE_RUNTIMES_UPDATED",
+ REQUEST_EXTENSIONS_FAILURE: "REQUEST_EXTENSIONS_FAILURE",
+ REQUEST_EXTENSIONS_START: "REQUEST_EXTENSIONS_START",
+ REQUEST_EXTENSIONS_SUCCESS: "REQUEST_EXTENSIONS_SUCCESS",
+ REQUEST_PROCESSES_FAILURE: "REQUEST_PROCESSES_FAILURE",
+ REQUEST_PROCESSES_START: "REQUEST_PROCESSES_START",
+ REQUEST_PROCESSES_SUCCESS: "REQUEST_PROCESSES_SUCCESS",
+ REQUEST_TABS_FAILURE: "REQUEST_TABS_FAILURE",
+ REQUEST_TABS_START: "REQUEST_TABS_START",
+ REQUEST_TABS_SUCCESS: "REQUEST_TABS_SUCCESS",
+ REQUEST_WORKERS_FAILURE: "REQUEST_WORKERS_FAILURE",
+ REQUEST_WORKERS_START: "REQUEST_WORKERS_START",
+ REQUEST_WORKERS_SUCCESS: "REQUEST_WORKERS_SUCCESS",
+ SELECT_PAGE_FAILURE: "SELECT_PAGE_FAILURE",
+ SELECT_PAGE_START: "SELECT_PAGE_START",
+ SELECT_PAGE_SUCCESS: "SELECT_PAGE_SUCCESS",
+ SELECTED_RUNTIME_ID_UPDATED: "SELECTED_RUNTIME_ID_UPDATED",
+ SHOW_PROFILER_DIALOG: "SHOW_PROFILER_DIALOG",
+ TELEMETRY_RECORD: "TELEMETRY_RECORD",
+ TEMPORARY_EXTENSION_INSTALL_FAILURE: "TEMPORARY_EXTENSION_INSTALL_FAILURE",
+ TEMPORARY_EXTENSION_INSTALL_START: "TEMPORARY_EXTENSION_INSTALL_START",
+ TEMPORARY_EXTENSION_INSTALL_SUCCESS: "TEMPORARY_EXTENSION_INSTALL_SUCCESS",
+ TEMPORARY_EXTENSION_RELOAD_FAILURE: "TEMPORARY_EXTENSION_RELOAD_FAILURE",
+ TEMPORARY_EXTENSION_RELOAD_START: "TEMPORARY_EXTENSION_RELOAD_START",
+ TEMPORARY_EXTENSION_RELOAD_SUCCESS: "TEMPORARY_EXTENSION_RELOAD_SUCCESS",
+ TERMINATE_EXTENSION_BGSCRIPT_FAILURE: "TERMINATE_EXTENSION_BGSCRIPT_FAILURE",
+ TERMINATE_EXTENSION_BGSCRIPT_START: "TERMINATE_EXTENSION_BGSCRIPT_START",
+ TERMINATE_EXTENSION_BGSCRIPT_SUCCESS: "TERMINATE_EXTENSION_BGSCRIPT_SUCCESS",
+ THIS_FIREFOX_RUNTIME_CREATED: "THIS_FIREFOX_RUNTIME_CREATED",
+ UNWATCH_RUNTIME_FAILURE: "UNWATCH_RUNTIME_FAILURE",
+ UNWATCH_RUNTIME_START: "UNWATCH_RUNTIME_START",
+ UNWATCH_RUNTIME_SUCCESS: "UNWATCH_RUNTIME_SUCCESS",
+ UPDATE_CONNECTION_PROMPT_SETTING_FAILURE:
+ "UPDATE_CONNECTION_PROMPT_SETTING_FAILURE",
+ UPDATE_CONNECTION_PROMPT_SETTING_START:
+ "UPDATE_CONNECTION_PROMPT_SETTING_START",
+ UPDATE_CONNECTION_PROMPT_SETTING_SUCCESS:
+ "UPDATE_CONNECTION_PROMPT_SETTING_SUCCESS",
+ USB_RUNTIMES_SCAN_START: "USB_RUNTIMES_SCAN_START",
+ USB_RUNTIMES_SCAN_SUCCESS: "USB_RUNTIMES_SCAN_SUCCESS",
+ WATCH_RUNTIME_FAILURE: "WATCH_RUNTIME_FAILURE",
+ WATCH_RUNTIME_START: "WATCH_RUNTIME_START",
+ WATCH_RUNTIME_SUCCESS: "WATCH_RUNTIME_SUCCESS",
+};
+
+const DEBUG_TARGETS = DEBUG_TARGET_TYPES;
+
+const DEBUG_TARGET_PANE = {
+ INSTALLED_EXTENSION: "installedExtension",
+ PROCESSES: "processes",
+ OTHER_WORKER: "otherWorker",
+ SERVICE_WORKER: "serviceWorker",
+ SHARED_WORKER: "sharedWorker",
+ TAB: "tab",
+ TEMPORARY_EXTENSION: "temporaryExtension",
+};
+
+const ICON_LABEL_LEVEL = {
+ INFO: "info",
+ OK: "ok",
+};
+
+const MESSAGE_LEVEL = {
+ ERROR: "error",
+ INFO: "info",
+ WARNING: "warning",
+};
+
+const PAGE_TYPES = {
+ RUNTIME: "runtime",
+ CONNECT: "connect",
+};
+
+const PREFERENCES = {
+ // Preference that drives the display of the "Tabs" category on This Firefox.
+ LOCAL_TAB_DEBUGGING_ENABLED: "devtools.aboutdebugging.local-tab-debugging",
+ // Preference that drives the display of the "Processes" debug target category.
+ PROCESS_DEBUGGING_ENABLED: "devtools.aboutdebugging.process-debugging",
+ // Preference that drives the display of hidden & system addons in about:debugging.
+ SHOW_HIDDEN_ADDONS: "devtools.aboutdebugging.showHiddenAddons",
+ // Preference to store the last path used for loading a temporary extension.
+ TEMPORARY_EXTENSION_PATH: "devtools.aboutdebugging.tmpExtDirPath",
+ // Preference that disables installing extensions when set to false.
+ XPINSTALL_ENABLED: "xpinstall.enabled",
+};
+
+const RUNTIME_PREFERENCE = {
+ CONNECTION_PROMPT: "devtools.debugger.prompt-connection",
+ PERMANENT_PRIVATE_BROWSING: "browser.privatebrowsing.autostart",
+ SERVICE_WORKERS_ENABLED: "dom.serviceWorkers.enabled",
+};
+
+const RUNTIMES = {
+ NETWORK: CONNECTION_TYPES.NETWORK,
+ THIS_FIREFOX: CONNECTION_TYPES.THIS_FIREFOX,
+ USB: CONNECTION_TYPES.USB,
+};
+
+const SERVICE_WORKER_FETCH_STATES = {
+ LISTENING: "LISTENING",
+ NOT_LISTENING: "NOT_LISTENING",
+};
+
+const SERVICE_WORKER_STATUSES = {
+ RUNNING: "RUNNING",
+ REGISTERING: "REGISTERING",
+ STOPPED: "STOPPED",
+};
+
+const USB_STATES = {
+ DISABLED_USB: "DISABLED_USB",
+ ENABLED_USB: "ENABLED_USB",
+ UPDATING_USB: "UPDATING_USB",
+};
+
+const EXTENSION_BGSCRIPT_STATUSES = {
+ RUNNING: "RUNNING",
+ STOPPED: "STOPPED",
+};
+
+/**
+ * These constants reference the performance-new's concept of a PageContext.
+ * These are defined in devtools/client/performance-new/@types/perf.d.ts
+ * about:debugging only uses the remote variants of the PageContexts.
+ */
+const PROFILER_PAGE_CONTEXT = {
+ DEVTOOLS_REMOTE: "devtools-remote",
+ ABOUTPROFILING_REMOTE: "aboutprofiling-remote",
+};
+
+// flatten constants
+module.exports = Object.assign(
+ {},
+ {
+ DEBUG_TARGETS,
+ DEBUG_TARGET_PANE,
+ EXTENSION_BGSCRIPT_STATUSES,
+ ICON_LABEL_LEVEL,
+ MESSAGE_LEVEL,
+ PAGE_TYPES,
+ PREFERENCES,
+ RUNTIME_PREFERENCE,
+ RUNTIMES,
+ SERVICE_WORKER_FETCH_STATES,
+ SERVICE_WORKER_STATUSES,
+ USB_STATES,
+ PROFILER_PAGE_CONTEXT,
+ },
+ actionTypes
+);
diff --git a/devtools/client/aboutdebugging/src/create-store.js b/devtools/client/aboutdebugging/src/create-store.js
new file mode 100644
index 0000000000..93fd240b00
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/create-store.js
@@ -0,0 +1,78 @@
+/* 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 {
+ applyMiddleware,
+ createStore,
+} = require("resource://devtools/client/shared/vendor/redux.js");
+const {
+ thunk,
+} = require("resource://devtools/client/shared/redux/middleware/thunk.js");
+const {
+ waitUntilService,
+} = require("resource://devtools/client/shared/redux/middleware/wait-service.js");
+
+const rootReducer = require("resource://devtools/client/aboutdebugging/src/reducers/index.js");
+const {
+ DebugTargetsState,
+} = require("resource://devtools/client/aboutdebugging/src/reducers/debug-targets-state.js");
+const {
+ RuntimesState,
+} = require("resource://devtools/client/aboutdebugging/src/reducers/runtimes-state.js");
+const {
+ UiState,
+} = require("resource://devtools/client/aboutdebugging/src/reducers/ui-state.js");
+const debugTargetListenerMiddleware = require("resource://devtools/client/aboutdebugging/src/middleware/debug-target-listener.js");
+const errorLoggingMiddleware = require("resource://devtools/client/aboutdebugging/src/middleware/error-logging.js");
+const eventRecordingMiddleware = require("resource://devtools/client/aboutdebugging/src/middleware/event-recording.js");
+const extensionComponentDataMiddleware = require("resource://devtools/client/aboutdebugging/src/middleware/extension-component-data.js");
+const processComponentDataMiddleware = require("resource://devtools/client/aboutdebugging/src/middleware/process-component-data.js");
+const tabComponentDataMiddleware = require("resource://devtools/client/aboutdebugging/src/middleware/tab-component-data.js");
+const workerComponentDataMiddleware = require("resource://devtools/client/aboutdebugging/src/middleware/worker-component-data.js");
+const {
+ getDebugTargetCollapsibilities,
+} = require("resource://devtools/client/aboutdebugging/src/modules/debug-target-collapsibilities.js");
+const {
+ getNetworkLocations,
+} = require("resource://devtools/client/aboutdebugging/src/modules/network-locations.js");
+
+const {
+ PREFERENCES,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+function configureStore() {
+ const initialState = {
+ debugTargets: new DebugTargetsState(),
+ runtimes: new RuntimesState(),
+ ui: getUiState(),
+ };
+
+ const middleware = applyMiddleware(
+ thunk(),
+ debugTargetListenerMiddleware,
+ errorLoggingMiddleware,
+ eventRecordingMiddleware,
+ extensionComponentDataMiddleware,
+ processComponentDataMiddleware,
+ tabComponentDataMiddleware,
+ workerComponentDataMiddleware,
+ waitUntilService
+ );
+
+ return createStore(rootReducer, initialState, middleware);
+}
+
+function getUiState() {
+ const collapsibilities = getDebugTargetCollapsibilities();
+ const locations = getNetworkLocations();
+ const showHiddenAddons = Services.prefs.getBoolPref(
+ PREFERENCES.SHOW_HIDDEN_ADDONS,
+ false
+ );
+ return new UiState(locations, collapsibilities, showHiddenAddons);
+}
+
+exports.configureStore = configureStore;
diff --git a/devtools/client/aboutdebugging/src/middleware/debug-target-listener.js b/devtools/client/aboutdebugging/src/middleware/debug-target-listener.js
new file mode 100644
index 0000000000..655e667a6d
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/middleware/debug-target-listener.js
@@ -0,0 +1,111 @@
+/* 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 {
+ EXTENSION_BGSCRIPT_STATUSES,
+ EXTENSION_BGSCRIPT_STATUS_UPDATED,
+ UNWATCH_RUNTIME_START,
+ WATCH_RUNTIME_SUCCESS,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+const Actions = require("resource://devtools/client/aboutdebugging/src/actions/index.js");
+
+const RootResourceCommand = require("resource://devtools/shared/commands/root-resource/root-resource-command.js");
+
+function debugTargetListenerMiddleware(store) {
+ const onExtensionsUpdated = () => {
+ store.dispatch(Actions.requestExtensions());
+ };
+
+ const onTabsUpdated = () => {
+ store.dispatch(Actions.requestTabs());
+ };
+
+ const onWorkersUpdated = () => {
+ store.dispatch(Actions.requestWorkers());
+ };
+
+ let rootResourceCommand;
+
+ function onExtensionsBackgroundScriptStatusAvailable(resources) {
+ for (const resource of resources) {
+ const backgroundScriptStatus = resource.payload.isRunning
+ ? EXTENSION_BGSCRIPT_STATUSES.RUNNING
+ : EXTENSION_BGSCRIPT_STATUSES.STOPPED;
+
+ store.dispatch({
+ type: EXTENSION_BGSCRIPT_STATUS_UPDATED,
+ id: resource.payload.addonId,
+ backgroundScriptStatus,
+ });
+ }
+ }
+
+ return next => async action => {
+ switch (action.type) {
+ case WATCH_RUNTIME_SUCCESS: {
+ const { runtime } = action;
+ const { clientWrapper } = runtime.runtimeDetails;
+
+ rootResourceCommand = clientWrapper.createRootResourceCommand();
+
+ // Watch extensions background script status updates.
+ await rootResourceCommand
+ .watchResources(
+ [RootResourceCommand.TYPES.EXTENSIONS_BGSCRIPT_STATUS],
+ { onAvailable: onExtensionsBackgroundScriptStatusAvailable }
+ )
+ .catch(e => {
+ // Log an error if watching this resource rejects (e.g. if
+ // the promise was not resolved yet when about:debugging tab
+ // or the RDP connection to a remote target has been closed).
+ console.error(e);
+ });
+
+ // Tabs
+ clientWrapper.on("tabListChanged", onTabsUpdated);
+
+ // Addons
+ clientWrapper.on("addonListChanged", onExtensionsUpdated);
+
+ // Workers
+ clientWrapper.on("workersUpdated", onWorkersUpdated);
+ break;
+ }
+ case UNWATCH_RUNTIME_START: {
+ const { runtime } = action;
+ const { clientWrapper } = runtime.runtimeDetails;
+
+ // Stop watching extensions background script status updates.
+ try {
+ rootResourceCommand?.unwatchResources(
+ [RootResourceCommand.TYPES.EXTENSIONS_BGSCRIPT_STATUS],
+ { onAvailable: onExtensionsBackgroundScriptStatusAvailable }
+ );
+ } catch (e) {
+ // Log an error if watching this resource rejects (e.g. if
+ // the promise was not resolved yet when about:debugging tab
+ // or the RDP connection to a remote target has been closed).
+ console.error(e);
+ }
+
+ // Tabs
+ clientWrapper.off("tabListChanged", onTabsUpdated);
+
+ // Addons
+ clientWrapper.off("addonListChanged", onExtensionsUpdated);
+
+ // Workers
+ clientWrapper.off("workersUpdated", onWorkersUpdated);
+ break;
+ }
+ }
+
+ return next(action);
+ };
+}
+
+module.exports = debugTargetListenerMiddleware;
diff --git a/devtools/client/aboutdebugging/src/middleware/error-logging.js b/devtools/client/aboutdebugging/src/middleware/error-logging.js
new file mode 100644
index 0000000000..fd04f34b76
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/middleware/error-logging.js
@@ -0,0 +1,35 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Error logging middleware that will forward all actions that contain an error property
+ * to the console.
+ */
+function errorLoggingMiddleware() {
+ return next => action => {
+ if (action.error) {
+ const { error } = action;
+ if (error.message) {
+ console.error(`[ACTION FAILED] ${action.type}: ${error.message}`);
+ } else if (typeof error === "string") {
+ // All failure actions should dispatch an error object instead of a message.
+ // We allow some flexibility to still provide some error logging.
+ console.error(`[ACTION FAILED] ${action.type}: ${error}`);
+ console.error(
+ `[ACTION FAILED] ${action.type} should dispatch the error object!`
+ );
+ }
+
+ if (error.stack) {
+ console.error(error.stack);
+ }
+ }
+
+ return next(action);
+ };
+}
+
+module.exports = errorLoggingMiddleware;
diff --git a/devtools/client/aboutdebugging/src/middleware/event-recording.js b/devtools/client/aboutdebugging/src/middleware/event-recording.js
new file mode 100644
index 0000000000..f926100b8b
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/middleware/event-recording.js
@@ -0,0 +1,268 @@
+/* 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 Telemetry = require("resource://devtools/client/shared/telemetry.js");
+loader.lazyGetter(
+ this,
+ "telemetry",
+ () => new Telemetry({ useSessionId: true })
+);
+
+const {
+ CONNECT_RUNTIME_CANCEL,
+ CONNECT_RUNTIME_FAILURE,
+ CONNECT_RUNTIME_NOT_RESPONDING,
+ CONNECT_RUNTIME_START,
+ CONNECT_RUNTIME_SUCCESS,
+ DISCONNECT_RUNTIME_SUCCESS,
+ REMOTE_RUNTIMES_UPDATED,
+ RUNTIMES,
+ SELECT_PAGE_SUCCESS,
+ SHOW_PROFILER_DIALOG,
+ TELEMETRY_RECORD,
+ UPDATE_CONNECTION_PROMPT_SETTING_SUCCESS,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+const {
+ findRuntimeById,
+ getAllRuntimes,
+ getCurrentRuntime,
+} = require("resource://devtools/client/aboutdebugging/src/modules/runtimes-state-helper.js");
+
+function recordEvent(method, details) {
+ telemetry.recordEvent(method, "aboutdebugging", null, details);
+
+ // For close and open events, also ping the regular telemetry helpers used
+ // for all DevTools UIs.
+ if (method === "open_adbg") {
+ telemetry.toolOpened("aboutdebugging", window.AboutDebugging);
+ } else if (method === "close_adbg") {
+ // XXX: Note that aboutdebugging has no histogram created for
+ // TIME_ACTIVE_SECOND, so calling toolClosed will not actually
+ // record anything.
+ telemetry.toolClosed("aboutdebugging", window.AboutDebugging);
+ }
+}
+
+const telemetryRuntimeIds = new Map();
+// Create an anonymous id that will allow to track all events related to a runtime without
+// leaking personal data related to this runtime.
+function getTelemetryRuntimeId(id) {
+ if (!telemetryRuntimeIds.has(id)) {
+ const randomId = (Math.random() * 100000) | 0;
+ telemetryRuntimeIds.set(id, "runtime-" + randomId);
+ }
+ return telemetryRuntimeIds.get(id);
+}
+
+function getCurrentRuntimeIdForTelemetry(store) {
+ const id = getCurrentRuntime(store.getState().runtimes).id;
+ return getTelemetryRuntimeId(id);
+}
+
+function getRuntimeEventExtras(runtime) {
+ const { extra, runtimeDetails } = runtime;
+
+ // deviceName can be undefined for non-usb devices, but we should not log "undefined".
+ const deviceName = extra?.deviceName || "";
+ const runtimeShortName = runtime.type === RUNTIMES.USB ? runtime.name : "";
+ const runtimeName = runtimeDetails?.info.name || "";
+ return {
+ connection_type: runtime.type,
+ device_name: deviceName,
+ runtime_id: getTelemetryRuntimeId(runtime.id),
+ runtime_name: runtimeName || runtimeShortName,
+ };
+}
+
+function onConnectRuntimeSuccess(action, store) {
+ if (action.runtime.type === RUNTIMES.THIS_FIREFOX) {
+ // Only record connection and disconnection events for remote runtimes.
+ return;
+ }
+ // When we just connected to a runtime, the runtimeDetails are not in the store yet,
+ // so we merge it here to retrieve the expected telemetry data.
+ const storeRuntime = findRuntimeById(
+ action.runtime.id,
+ store.getState().runtimes
+ );
+ const runtime = Object.assign({}, storeRuntime, {
+ runtimeDetails: action.runtime.runtimeDetails,
+ });
+ const extras = Object.assign({}, getRuntimeEventExtras(runtime), {
+ runtime_os: action.runtime.runtimeDetails.info.os,
+ runtime_version: action.runtime.runtimeDetails.info.version,
+ });
+ recordEvent("runtime_connected", extras);
+}
+
+function onDisconnectRuntimeSuccess(action, store) {
+ const runtime = findRuntimeById(action.runtime.id, store.getState().runtimes);
+ if (runtime.type === RUNTIMES.THIS_FIREFOX) {
+ // Only record connection and disconnection events for remote runtimes.
+ return;
+ }
+
+ recordEvent("runtime_disconnected", getRuntimeEventExtras(runtime));
+}
+
+function onRemoteRuntimesUpdated(action, store) {
+ // Compare new runtimes with the existing runtimes to detect if runtimes, devices
+ // have been added or removed.
+ const newRuntimes = action.runtimes;
+ const allRuntimes = getAllRuntimes(store.getState().runtimes);
+ const oldRuntimes = allRuntimes.filter(r => r.type === action.runtimeType);
+
+ // Check if all the old runtimes and devices are still available in the updated
+ // array.
+ for (const oldRuntime of oldRuntimes) {
+ const runtimeRemoved = newRuntimes.every(r => r.id !== oldRuntime.id);
+ if (runtimeRemoved && !oldRuntime.isUnplugged) {
+ recordEvent("runtime_removed", getRuntimeEventExtras(oldRuntime));
+ }
+ }
+
+ // Using device names as unique IDs is inaccurate. See Bug 1544582.
+ const oldDeviceNames = new Set(oldRuntimes.map(r => r.extra.deviceName));
+ for (const oldDeviceName of oldDeviceNames) {
+ const newRuntime = newRuntimes.find(
+ r => r.extra.deviceName === oldDeviceName
+ );
+ const oldRuntime = oldRuntimes.find(
+ r => r.extra.deviceName === oldDeviceName
+ );
+ const isUnplugged = newRuntime?.isUnplugged && !oldRuntime.isUnplugged;
+ if (oldDeviceName && (!newRuntime || isUnplugged)) {
+ recordEvent("device_removed", {
+ connection_type: action.runtimeType,
+ device_name: oldDeviceName,
+ });
+ }
+ }
+
+ // Check if the new runtimes and devices were already available in the existing
+ // array.
+ for (const newRuntime of newRuntimes) {
+ const runtimeAdded = oldRuntimes.every(r => r.id !== newRuntime.id);
+ if (runtimeAdded && !newRuntime.isUnplugged) {
+ recordEvent("runtime_added", getRuntimeEventExtras(newRuntime));
+ }
+ }
+
+ // Using device names as unique IDs is inaccurate. See Bug 1544582.
+ const newDeviceNames = new Set(newRuntimes.map(r => r.extra.deviceName));
+ for (const newDeviceName of newDeviceNames) {
+ const newRuntime = newRuntimes.find(
+ r => r.extra.deviceName === newDeviceName
+ );
+ const oldRuntime = oldRuntimes.find(
+ r => r.extra.deviceName === newDeviceName
+ );
+ const isPlugged = oldRuntime?.isUnplugged && !newRuntime.isUnplugged;
+
+ if (newDeviceName && (!oldRuntime || isPlugged)) {
+ recordEvent("device_added", {
+ connection_type: action.runtimeType,
+ device_name: newDeviceName,
+ });
+ }
+ }
+}
+
+function recordConnectionAttempt(connectionId, runtimeId, status, store) {
+ const runtime = findRuntimeById(runtimeId, store.getState().runtimes);
+ if (runtime.type === RUNTIMES.THIS_FIREFOX) {
+ // Only record connection_attempt events for remote runtimes.
+ return;
+ }
+
+ recordEvent("connection_attempt", {
+ connection_id: connectionId,
+ connection_type: runtime.type,
+ runtime_id: getTelemetryRuntimeId(runtimeId),
+ status,
+ });
+}
+
+/**
+ * This middleware will record events to telemetry for some specific actions.
+ */
+function eventRecordingMiddleware(store) {
+ return next => action => {
+ switch (action.type) {
+ case CONNECT_RUNTIME_CANCEL:
+ recordConnectionAttempt(
+ action.connectionId,
+ action.id,
+ "cancelled",
+ store
+ );
+ break;
+ case CONNECT_RUNTIME_FAILURE:
+ recordConnectionAttempt(
+ action.connectionId,
+ action.id,
+ "failed",
+ store
+ );
+ break;
+ case CONNECT_RUNTIME_NOT_RESPONDING:
+ recordConnectionAttempt(
+ action.connectionId,
+ action.id,
+ "not responding",
+ store
+ );
+ break;
+ case CONNECT_RUNTIME_START:
+ recordConnectionAttempt(action.connectionId, action.id, "start", store);
+ break;
+ case CONNECT_RUNTIME_SUCCESS:
+ recordConnectionAttempt(
+ action.connectionId,
+ action.runtime.id,
+ "success",
+ store
+ );
+ onConnectRuntimeSuccess(action, store);
+ break;
+ case DISCONNECT_RUNTIME_SUCCESS:
+ onDisconnectRuntimeSuccess(action, store);
+ break;
+ case REMOTE_RUNTIMES_UPDATED:
+ onRemoteRuntimesUpdated(action, store);
+ break;
+ case SELECT_PAGE_SUCCESS:
+ recordEvent("select_page", { page_type: action.page });
+ break;
+ case SHOW_PROFILER_DIALOG:
+ recordEvent("show_profiler", {
+ runtime_id: getCurrentRuntimeIdForTelemetry(store),
+ });
+ break;
+ case TELEMETRY_RECORD:
+ const { method, details } = action;
+ if (method) {
+ recordEvent(method, details);
+ } else {
+ console.error(
+ `[RECORD EVENT FAILED] ${action.type}: no "method" property`
+ );
+ }
+ break;
+ case UPDATE_CONNECTION_PROMPT_SETTING_SUCCESS:
+ recordEvent("update_conn_prompt", {
+ prompt_enabled: `${action.connectionPromptEnabled}`,
+ runtime_id: getCurrentRuntimeIdForTelemetry(store),
+ });
+ break;
+ }
+
+ return next(action);
+ };
+}
+
+module.exports = eventRecordingMiddleware;
diff --git a/devtools/client/aboutdebugging/src/middleware/extension-component-data.js b/devtools/client/aboutdebugging/src/middleware/extension-component-data.js
new file mode 100644
index 0000000000..5987f36398
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/middleware/extension-component-data.js
@@ -0,0 +1,84 @@
+/* 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 {
+ DEBUG_TARGETS,
+ REQUEST_EXTENSIONS_SUCCESS,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+const {
+ getExtensionUuid,
+ parseFileUri,
+} = require("resource://devtools/client/aboutdebugging/src/modules/extensions-helper.js");
+
+/**
+ * This middleware converts extensions object that get from DevToolsClient.listAddons()
+ * to data which is used in DebugTargetItem.
+ */
+const extensionComponentDataMiddleware = store => next => action => {
+ switch (action.type) {
+ case REQUEST_EXTENSIONS_SUCCESS: {
+ action.installedExtensions = toComponentData(action.installedExtensions);
+ action.temporaryExtensions = toComponentData(action.temporaryExtensions);
+ break;
+ }
+ }
+
+ return next(action);
+};
+
+function getFilePath(extension) {
+ // Only show file system paths, and only for temporarily installed add-ons.
+ if (
+ !extension.temporarilyInstalled ||
+ !extension.url ||
+ !extension.url.startsWith("file://")
+ ) {
+ return null;
+ }
+
+ return parseFileUri(extension.url);
+}
+
+function toComponentData(extensions) {
+ return extensions.map(extension => {
+ const type = DEBUG_TARGETS.EXTENSION;
+ const {
+ actor,
+ backgroundScriptStatus,
+ iconDataURL,
+ iconURL,
+ id,
+ manifestURL,
+ name,
+ persistentBackgroundScript,
+ warnings,
+ } = extension;
+ const icon =
+ iconDataURL ||
+ iconURL ||
+ "chrome://mozapps/skin/extensions/extensionGeneric.svg";
+ const location = getFilePath(extension);
+ const uuid = getExtensionUuid(extension);
+ return {
+ name,
+ icon,
+ id,
+ type,
+ details: {
+ actor,
+ backgroundScriptStatus,
+ location,
+ manifestURL,
+ persistentBackgroundScript,
+ uuid,
+ warnings: warnings || [],
+ },
+ };
+ });
+}
+
+module.exports = extensionComponentDataMiddleware;
diff --git a/devtools/client/aboutdebugging/src/middleware/moz.build b/devtools/client/aboutdebugging/src/middleware/moz.build
new file mode 100644
index 0000000000..f50150f569
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/middleware/moz.build
@@ -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/.
+
+DevToolsModules(
+ "debug-target-listener.js",
+ "error-logging.js",
+ "event-recording.js",
+ "extension-component-data.js",
+ "process-component-data.js",
+ "tab-component-data.js",
+ "worker-component-data.js",
+)
diff --git a/devtools/client/aboutdebugging/src/middleware/process-component-data.js b/devtools/client/aboutdebugging/src/middleware/process-component-data.js
new file mode 100644
index 0000000000..d5cdc6365b
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/middleware/process-component-data.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 {
+ l10n,
+} = require("resource://devtools/client/aboutdebugging/src/modules/l10n.js");
+
+const {
+ DEBUG_TARGETS,
+ REQUEST_PROCESSES_SUCCESS,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+/**
+ * This middleware converts tabs object that get from DevToolsClient.listProcesses() to
+ * data which is used in DebugTargetItem.
+ */
+const processComponentDataMiddleware = store => next => action => {
+ switch (action.type) {
+ case REQUEST_PROCESSES_SUCCESS: {
+ const mainProcessComponentData = toMainProcessComponentData(
+ action.mainProcess
+ );
+ action.processes = [mainProcessComponentData];
+ break;
+ }
+ }
+
+ return next(action);
+};
+
+function toMainProcessComponentData(process) {
+ const type = DEBUG_TARGETS.PROCESS;
+ const icon = "chrome://devtools/skin/images/aboutdebugging-process-icon.svg";
+
+ // For now, we assume there is only one process and this is the main process
+ // So the name and title are for a remote (multiprocess) browser toolbox.
+ const name = l10n.getString("about-debugging-multiprocess-toolbox-name");
+ const description = l10n.getString(
+ "about-debugging-multiprocess-toolbox-description"
+ );
+
+ return {
+ name,
+ icon,
+ type,
+ details: {
+ description,
+ },
+ };
+}
+
+module.exports = processComponentDataMiddleware;
diff --git a/devtools/client/aboutdebugging/src/middleware/tab-component-data.js b/devtools/client/aboutdebugging/src/middleware/tab-component-data.js
new file mode 100644
index 0000000000..f468926f81
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/middleware/tab-component-data.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 {
+ DEBUG_TARGETS,
+ REQUEST_TABS_SUCCESS,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+/**
+ * This middleware converts tabs object that get from DevToolsClient.listTabs() to data
+ * which is used in DebugTargetItem.
+ */
+const tabComponentDataMiddleware = store => next => action => {
+ switch (action.type) {
+ case REQUEST_TABS_SUCCESS: {
+ action.tabs = toComponentData(action.tabs);
+ break;
+ }
+ }
+
+ return next(action);
+};
+
+function toComponentData(tabs) {
+ return tabs.map(tab => {
+ const type = DEBUG_TARGETS.TAB;
+ const id = tab.browserId;
+ const icon = tab.favicon
+ ? `data:image/png;base64,${btoa(
+ String.fromCharCode.apply(String, tab.favicon)
+ )}`
+ : "chrome://devtools/skin/images/globe.svg";
+ const name = tab.title || tab.url;
+ const { url, isZombieTab } = tab;
+ return {
+ name,
+ icon,
+ id,
+ type,
+ details: {
+ isZombieTab,
+ url,
+ },
+ };
+ });
+}
+
+module.exports = tabComponentDataMiddleware;
diff --git a/devtools/client/aboutdebugging/src/middleware/worker-component-data.js b/devtools/client/aboutdebugging/src/middleware/worker-component-data.js
new file mode 100644
index 0000000000..178c99e322
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/middleware/worker-component-data.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 {
+ DEBUG_TARGETS,
+ REQUEST_WORKERS_SUCCESS,
+ SERVICE_WORKER_FETCH_STATES,
+ SERVICE_WORKER_STATUSES,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+/**
+ * This middleware converts workers object that get from DevToolsClient.listAllWorkers()
+ * to data which is used in DebugTargetItem.
+ */
+const workerComponentDataMiddleware = store => next => action => {
+ switch (action.type) {
+ case REQUEST_WORKERS_SUCCESS: {
+ action.otherWorkers = toComponentData(action.otherWorkers);
+ action.serviceWorkers = toComponentData(action.serviceWorkers, true);
+ action.sharedWorkers = toComponentData(action.sharedWorkers);
+ break;
+ }
+ }
+
+ return next(action);
+};
+
+function getServiceWorkerStatus(worker) {
+ const isActive = worker.state === Ci.nsIServiceWorkerInfo.STATE_ACTIVATED;
+ const isRunning = !!worker.workerDescriptorFront;
+
+ if (isActive && isRunning) {
+ return SERVICE_WORKER_STATUSES.RUNNING;
+ } else if (isActive) {
+ return SERVICE_WORKER_STATUSES.STOPPED;
+ }
+
+ // We cannot get service worker registrations unless the registration is in
+ // ACTIVE state. Unable to know the actual state ("installing", "waiting"), we
+ // display a custom state "registering" for now. See Bug 1153292.
+ return SERVICE_WORKER_STATUSES.REGISTERING;
+}
+
+function toComponentData(workers, isServiceWorker) {
+ return workers.map(worker => {
+ // Here `worker` is the worker object created by RootFront.listAllWorkers
+ const type = DEBUG_TARGETS.WORKER;
+ const icon = "chrome://devtools/skin/images/debugging-workers.svg";
+ let { fetch } = worker;
+ const { id, name, registrationFront, scope, subscription } = worker;
+
+ let pushServiceEndpoint = null;
+ let status = null;
+
+ if (isServiceWorker) {
+ fetch = fetch
+ ? SERVICE_WORKER_FETCH_STATES.LISTENING
+ : SERVICE_WORKER_FETCH_STATES.NOT_LISTENING;
+ status = getServiceWorkerStatus(worker);
+ pushServiceEndpoint = subscription ? subscription.endpoint : null;
+ }
+
+ return {
+ details: {
+ fetch,
+ pushServiceEndpoint,
+ registrationFront,
+ scope,
+ status,
+ },
+ icon,
+ id,
+ name,
+ type,
+ };
+ });
+}
+
+module.exports = workerComponentDataMiddleware;
diff --git a/devtools/client/aboutdebugging/src/modules/client-wrapper.js b/devtools/client/aboutdebugging/src/modules/client-wrapper.js
new file mode 100644
index 0000000000..6d537b60a4
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/modules/client-wrapper.js
@@ -0,0 +1,217 @@
+/* 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 {
+ checkVersionCompatibility,
+} = require("resource://devtools/client/shared/remote-debugging/version-checker.js");
+
+const {
+ RUNTIME_PREFERENCE,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+const {
+ WorkersListener,
+} = require("resource://devtools/client/shared/workers-listener.js");
+const RootResourceCommand = require("resource://devtools/shared/commands/root-resource/root-resource-command.js");
+
+const PREF_TYPES = {
+ BOOL: "BOOL",
+};
+
+// Map of preference to preference type.
+const PREF_TO_TYPE = {
+ [RUNTIME_PREFERENCE.CHROME_DEBUG_ENABLED]: PREF_TYPES.BOOL,
+ [RUNTIME_PREFERENCE.CONNECTION_PROMPT]: PREF_TYPES.BOOL,
+ [RUNTIME_PREFERENCE.PERMANENT_PRIVATE_BROWSING]: PREF_TYPES.BOOL,
+ [RUNTIME_PREFERENCE.REMOTE_DEBUG_ENABLED]: PREF_TYPES.BOOL,
+ [RUNTIME_PREFERENCE.SERVICE_WORKERS_ENABLED]: PREF_TYPES.BOOL,
+};
+
+// Some events are fired by mainRoot rather than client.
+const MAIN_ROOT_EVENTS = ["addonListChanged", "tabListChanged"];
+
+/**
+ * The ClientWrapper class is used to isolate aboutdebugging from the DevTools client API
+ * The modules of about:debugging should never call DevTools client APIs directly.
+ */
+class ClientWrapper {
+ constructor(client) {
+ this.client = client;
+ this.workersListener = new WorkersListener(client.mainRoot);
+ }
+
+ once(evt, listener) {
+ if (MAIN_ROOT_EVENTS.includes(evt)) {
+ this.client.mainRoot.once(evt, listener);
+ } else {
+ this.client.once(evt, listener);
+ }
+ }
+
+ on(evt, listener) {
+ if (evt === "workersUpdated") {
+ this.workersListener.addListener(listener);
+ } else if (MAIN_ROOT_EVENTS.includes(evt)) {
+ this.client.mainRoot.on(evt, listener);
+ } else {
+ this.client.on(evt, listener);
+ }
+ }
+
+ off(evt, listener) {
+ if (evt === "workersUpdated") {
+ this.workersListener.removeListener(listener);
+ } else if (MAIN_ROOT_EVENTS.includes(evt)) {
+ this.client.mainRoot.off(evt, listener);
+ } else {
+ this.client.off(evt, listener);
+ }
+ }
+
+ async getFront(typeName) {
+ return this.client.mainRoot.getFront(typeName);
+ }
+
+ async getDeviceDescription() {
+ const deviceFront = await this.getFront("device");
+ const description = await deviceFront.getDescription();
+
+ // Only expose a specific set of properties.
+ return {
+ canDebugServiceWorkers: description.canDebugServiceWorkers,
+ channel: description.channel,
+ deviceName: description.deviceName,
+ name: description.brandName,
+ os: description.os,
+ version: description.version,
+ };
+ }
+
+ createRootResourceCommand() {
+ return new RootResourceCommand({ rootFront: this.client.mainRoot });
+ }
+
+ async checkVersionCompatibility() {
+ return checkVersionCompatibility(this.client);
+ }
+
+ async setPreference(prefName, value) {
+ const prefType = PREF_TO_TYPE[prefName];
+ const preferenceFront = await this.client.mainRoot.getFront("preference");
+ switch (prefType) {
+ case PREF_TYPES.BOOL:
+ return preferenceFront.setBoolPref(prefName, value);
+ default:
+ throw new Error("Unsupported preference" + prefName);
+ }
+ }
+
+ async getPreference(prefName, defaultValue) {
+ if (typeof defaultValue === "undefined") {
+ throw new Error(
+ "Default value is mandatory for getPreference, the actor will " +
+ "throw if the preference is not set on the target runtime"
+ );
+ }
+
+ const prefType = PREF_TO_TYPE[prefName];
+ const preferenceFront = await this.client.mainRoot.getFront("preference");
+ switch (prefType) {
+ case PREF_TYPES.BOOL:
+ // TODO: Add server-side trait and methods to pass a default value to getBoolPref.
+ // See Bug 1522588.
+ let prefValue;
+ try {
+ prefValue = await preferenceFront.getBoolPref(prefName);
+ } catch (e) {
+ prefValue = defaultValue;
+ }
+ return prefValue;
+ default:
+ throw new Error("Unsupported preference:" + prefName);
+ }
+ }
+
+ async listTabs() {
+ return this.client.mainRoot.listTabs();
+ }
+
+ async listAddons(options) {
+ return this.client.mainRoot.listAddons(options);
+ }
+
+ async getAddon({ id }) {
+ return this.client.mainRoot.getAddon({ id });
+ }
+
+ async uninstallAddon({ id }) {
+ const addonsFront = await this.getFront("addons");
+ return addonsFront.uninstallAddon(id);
+ }
+
+ async getMainProcess() {
+ return this.client.mainRoot.getMainProcess();
+ }
+
+ async getServiceWorkerFront({ id }) {
+ return this.client.mainRoot.getWorker(id);
+ }
+
+ async listWorkers() {
+ const { other, service, shared } =
+ await this.client.mainRoot.listAllWorkers();
+
+ return {
+ otherWorkers: other,
+ serviceWorkers: service,
+ sharedWorkers: shared,
+ };
+ }
+
+ async close() {
+ return this.client.close();
+ }
+
+ isClosed() {
+ return this.client._transportClosed;
+ }
+
+ // This method will be mocked to return a dummy URL during mochitests
+ getPerformancePanelUrl() {
+ return "chrome://devtools/content/performance-new/panel/index.xhtml";
+ }
+
+ /**
+ * @param {Window} win - The window of the dialog window.
+ * @param {Function} openAboutProfiling
+ */
+ async loadPerformanceProfiler(win, openAboutProfiling) {
+ const perfFront = await this.getFront("perf");
+ const { traits } = this.client;
+ await win.gInit(perfFront, traits, "devtools-remote", openAboutProfiling);
+ }
+
+ /**
+ * @param {Window} win - The window of the dialog window.
+ * @param {Function} openRemoteDevTools
+ */
+ async loadAboutProfiling(win, openRemoteDevTools) {
+ const perfFront = await this.getFront("perf");
+ const isSupportedPlatform = await perfFront.isSupportedPlatform();
+ const supportedFeatures = await perfFront.getSupportedFeatures();
+ await win.gInit(
+ "aboutprofiling-remote",
+ isSupportedPlatform,
+ supportedFeatures,
+ openRemoteDevTools
+ );
+ }
+
+ get traits() {
+ return { ...this.client.mainRoot.traits };
+ }
+}
+
+exports.ClientWrapper = ClientWrapper;
diff --git a/devtools/client/aboutdebugging/src/modules/debug-target-collapsibilities.js b/devtools/client/aboutdebugging/src/modules/debug-target-collapsibilities.js
new file mode 100644
index 0000000000..efba47e03a
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/modules/debug-target-collapsibilities.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 PREF_PREFIX = "devtools.aboutdebugging.collapsibilities.";
+const {
+ DEBUG_TARGET_PANE,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+/**
+ * This module provides a collection of helper methods to read and update the debug
+ * target pane's collapsibilities.
+ */
+
+/**
+ * @return {Object}
+ * {
+ * key: constants.DEBUG_TARGET_PANE
+ * value: true - collapsed
+ * false - expanded
+ * }
+ */
+function getDebugTargetCollapsibilities() {
+ const map = new Map();
+
+ for (const key of Object.values(DEBUG_TARGET_PANE)) {
+ const pref = Services.prefs.getBoolPref(PREF_PREFIX + key, false);
+ map.set(key, pref);
+ }
+
+ return map;
+}
+exports.getDebugTargetCollapsibilities = getDebugTargetCollapsibilities;
+
+/**
+ * @param collapsibilities - Same format to getDebugTargetCollapsibilities.
+ */
+function setDebugTargetCollapsibilities(collapsibilities) {
+ for (const key of Object.values(DEBUG_TARGET_PANE)) {
+ const isCollapsed = collapsibilities.get(key);
+ Services.prefs.setBoolPref(PREF_PREFIX + key, isCollapsed);
+ }
+}
+exports.setDebugTargetCollapsibilities = setDebugTargetCollapsibilities;
diff --git a/devtools/client/aboutdebugging/src/modules/debug-target-support.js b/devtools/client/aboutdebugging/src/modules/debug-target-support.js
new file mode 100644
index 0000000000..2b8d4afa5b
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/modules/debug-target-support.js
@@ -0,0 +1,98 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ DEBUG_TARGET_PANE,
+ PREFERENCES,
+ RUNTIMES,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+// Process target debugging is disabled by default.
+function isProcessDebuggingSupported() {
+ return Services.prefs.getBoolPref(
+ PREFERENCES.PROCESS_DEBUGGING_ENABLED,
+ false
+ );
+}
+
+// Local tab debugging is disabled by default.
+function isLocalTabDebuggingSupported() {
+ return Services.prefs.getBoolPref(
+ PREFERENCES.LOCAL_TAB_DEBUGGING_ENABLED,
+ false
+ );
+}
+
+// Local process debugging is disabled by default.
+// This preference has no default value in
+// devtools/client/preferences/devtools-client.js
+// because it is only intended for tests.
+function isLocalProcessDebuggingSupported() {
+ return Services.prefs.getBoolPref(
+ "devtools.aboutdebugging.test-local-process-debugging",
+ false
+ );
+}
+
+// Installing extensions can be disabled in enterprise policy.
+// Note: Temporary Extensions are only supported when debugging This Firefox, so checking
+// the local preference is acceptable here. If we enable Temporary extensions for remote
+// runtimes, we should retrieve the preference from the target runtime instead.
+function isTemporaryExtensionSupported() {
+ return Services.prefs.getBoolPref(PREFERENCES.XPINSTALL_ENABLED, true);
+}
+
+const ALL_DEBUG_TARGET_PANES = [
+ DEBUG_TARGET_PANE.INSTALLED_EXTENSION,
+ ...(isProcessDebuggingSupported() ? [DEBUG_TARGET_PANE.PROCESSES] : []),
+ DEBUG_TARGET_PANE.OTHER_WORKER,
+ DEBUG_TARGET_PANE.SERVICE_WORKER,
+ DEBUG_TARGET_PANE.SHARED_WORKER,
+ DEBUG_TARGET_PANE.TAB,
+ ...(isTemporaryExtensionSupported()
+ ? [DEBUG_TARGET_PANE.TEMPORARY_EXTENSION]
+ : []),
+];
+
+// All debug target panes (to filter out if any of the panels should be excluded).
+const REMOTE_DEBUG_TARGET_PANES = [...ALL_DEBUG_TARGET_PANES];
+
+const THIS_FIREFOX_DEBUG_TARGET_PANES = ALL_DEBUG_TARGET_PANES
+ // Main process debugging is not available for This Firefox.
+ // At the moment only the main process is listed under processes, so remove the category
+ // for this runtime.
+ .filter(
+ p => p !== DEBUG_TARGET_PANE.PROCESSES || isLocalProcessDebuggingSupported()
+ )
+ // Showing tab targets for This Firefox is behind a preference.
+ .filter(p => p !== DEBUG_TARGET_PANE.TAB || isLocalTabDebuggingSupported());
+
+const SUPPORTED_TARGET_PANE_BY_RUNTIME = {
+ [RUNTIMES.THIS_FIREFOX]: THIS_FIREFOX_DEBUG_TARGET_PANES,
+ [RUNTIMES.USB]: REMOTE_DEBUG_TARGET_PANES,
+ [RUNTIMES.NETWORK]: REMOTE_DEBUG_TARGET_PANES,
+};
+
+/**
+ * A debug target pane is more specialized than a debug target. For instance EXTENSION is
+ * a DEBUG_TARGET but INSTALLED_EXTENSION and TEMPORARY_EXTENSION are DEBUG_TARGET_PANES.
+ */
+function isSupportedDebugTargetPane(runtimeType, debugTargetPaneKey) {
+ return SUPPORTED_TARGET_PANE_BY_RUNTIME[runtimeType].includes(
+ debugTargetPaneKey
+ );
+}
+exports.isSupportedDebugTargetPane = isSupportedDebugTargetPane;
+
+/**
+ * Check if the given runtimeType supports temporary extension installation
+ * from about:debugging (currently disallowed on non-local runtimes).
+ */
+function supportsTemporaryExtensionInstaller(runtimeType) {
+ return runtimeType === RUNTIMES.THIS_FIREFOX;
+}
+exports.supportsTemporaryExtensionInstaller =
+ supportsTemporaryExtensionInstaller;
diff --git a/devtools/client/aboutdebugging/src/modules/extensions-helper.js b/devtools/client/aboutdebugging/src/modules/extensions-helper.js
new file mode 100644
index 0000000000..ec0d7c2661
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/modules/extensions-helper.js
@@ -0,0 +1,92 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
+});
+
+const {
+ PREFERENCES,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+exports.parseFileUri = function (url) {
+ // Strip a leading slash from Windows drive letter URIs.
+ // file:///home/foo ~> /home/foo
+ // file:///C:/foo ~> C:/foo
+ const windowsRegex = /^file:\/\/\/([a-zA-Z]:\/.*)/;
+ if (windowsRegex.test(url)) {
+ return windowsRegex.exec(url)[1];
+ }
+ return url.slice("file://".length);
+};
+
+exports.getExtensionUuid = function (extension) {
+ const { manifestURL } = extension;
+ // Strip off the protocol and rest, leaving us with just the UUID.
+ return manifestURL ? /moz-extension:\/\/([^/]*)/.exec(manifestURL)[1] : null;
+};
+
+/**
+ * Open a file picker to allow the user to locate a temporary extension. A temporary
+ * extension can either be:
+ * - a folder
+ * - a .xpi file
+ * - a .zip file
+ *
+ * @param {Window} win
+ * The window object where the filepicker should be opened.
+ * Note: We cannot use the global window object here because it is undefined if
+ * this module is loaded from a file outside of devtools/client/aboutdebugging/.
+ * See browser-loader.js `uri.startsWith(baseURI)` for more details.
+ * @param {String} message
+ * The help message that should be displayed to the user in the filepicker.
+ * @return {Promise} returns a promise that resolves a File object corresponding to the
+ * file selected by the user.
+ */
+exports.openTemporaryExtension = function (win, message) {
+ return new Promise(resolve => {
+ const fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ fp.init(win, message, Ci.nsIFilePicker.modeOpen);
+
+ // Try to set the last directory used as "displayDirectory".
+ try {
+ const lastDirPath = Services.prefs.getCharPref(
+ PREFERENCES.TEMPORARY_EXTENSION_PATH,
+ ""
+ );
+ const lastDir = new lazy.FileUtils.File(lastDirPath);
+ fp.displayDirectory = lastDir;
+ } catch (e) {
+ // Empty or invalid value, nothing to handle.
+ }
+
+ fp.open(res => {
+ if (res == Ci.nsIFilePicker.returnCancel || !fp.file) {
+ return;
+ }
+ let file = fp.file;
+ // AddonManager.installTemporaryAddon accepts either
+ // addon directory or final xpi file.
+ if (
+ !file.isDirectory() &&
+ !file.leafName.endsWith(".xpi") &&
+ !file.leafName.endsWith(".zip")
+ ) {
+ file = file.parent;
+ }
+
+ // We are about to resolve, store the path to the file for the next call.
+ Services.prefs.setCharPref(
+ PREFERENCES.TEMPORARY_EXTENSION_PATH,
+ file.path
+ );
+
+ resolve(file);
+ });
+ });
+};
diff --git a/devtools/client/aboutdebugging/src/modules/l10n.js b/devtools/client/aboutdebugging/src/modules/l10n.js
new file mode 100644
index 0000000000..88c7ae8bb1
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/modules/l10n.js
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ FluentL10n,
+} = require("resource://devtools/client/shared/fluent-l10n/fluent-l10n.js");
+
+// exports a singleton, which will be used across all aboutdebugging modules
+exports.l10n = new FluentL10n();
diff --git a/devtools/client/aboutdebugging/src/modules/moz.build b/devtools/client/aboutdebugging/src/modules/moz.build
new file mode 100644
index 0000000000..909e181714
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/modules/moz.build
@@ -0,0 +1,16 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ "client-wrapper.js",
+ "debug-target-collapsibilities.js",
+ "debug-target-support.js",
+ "extensions-helper.js",
+ "l10n.js",
+ "network-locations.js",
+ "runtime-client-factory.js",
+ "runtime-default-preferences.js",
+ "runtimes-state-helper.js",
+ "usb-runtimes.js",
+)
diff --git a/devtools/client/aboutdebugging/src/modules/network-locations.js b/devtools/client/aboutdebugging/src/modules/network-locations.js
new file mode 100644
index 0000000000..cbae436df7
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/modules/network-locations.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 NETWORK_LOCATIONS_PREF = "devtools.aboutdebugging.network-locations";
+
+/**
+ * This module provides a collection of helper methods to read and update network
+ * locations monitored by about-debugging.
+ */
+
+function addNetworkLocationsObserver(listener) {
+ Services.prefs.addObserver(NETWORK_LOCATIONS_PREF, listener);
+}
+exports.addNetworkLocationsObserver = addNetworkLocationsObserver;
+
+function removeNetworkLocationsObserver(listener) {
+ Services.prefs.removeObserver(NETWORK_LOCATIONS_PREF, listener);
+}
+exports.removeNetworkLocationsObserver = removeNetworkLocationsObserver;
+
+/**
+ * Read the current preference value for aboutdebugging network locations.
+ * Will throw if the value cannot be parsed or is not an array.
+ */
+function _parsePreferenceAsArray() {
+ const pref = Services.prefs.getStringPref(NETWORK_LOCATIONS_PREF, "[]");
+ const parsedValue = JSON.parse(pref);
+ if (!Array.isArray(parsedValue)) {
+ throw new Error("Expected array value in " + NETWORK_LOCATIONS_PREF);
+ }
+ return parsedValue;
+}
+
+function getNetworkLocations() {
+ try {
+ return _parsePreferenceAsArray();
+ } catch (e) {
+ Services.prefs.clearUserPref(NETWORK_LOCATIONS_PREF);
+ return [];
+ }
+}
+exports.getNetworkLocations = getNetworkLocations;
+
+function addNetworkLocation(location) {
+ const locations = getNetworkLocations();
+ const locationsSet = new Set(locations);
+ locationsSet.add(location);
+
+ Services.prefs.setStringPref(
+ NETWORK_LOCATIONS_PREF,
+ JSON.stringify([...locationsSet])
+ );
+}
+exports.addNetworkLocation = addNetworkLocation;
+
+function removeNetworkLocation(location) {
+ const locations = getNetworkLocations();
+ const locationsSet = new Set(locations);
+ locationsSet.delete(location);
+
+ Services.prefs.setStringPref(
+ NETWORK_LOCATIONS_PREF,
+ JSON.stringify([...locationsSet])
+ );
+}
+exports.removeNetworkLocation = removeNetworkLocation;
diff --git a/devtools/client/aboutdebugging/src/modules/runtime-client-factory.js b/devtools/client/aboutdebugging/src/modules/runtime-client-factory.js
new file mode 100644
index 0000000000..e553416c18
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/modules/runtime-client-factory.js
@@ -0,0 +1,68 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ prepareTCPConnection,
+} = require("resource://devtools/client/shared/remote-debugging/adb/commands/index.js");
+const {
+ DevToolsClient,
+} = require("resource://devtools/client/devtools-client.js");
+const {
+ DevToolsServer,
+} = require("resource://devtools/server/devtools-server.js");
+const {
+ ClientWrapper,
+} = require("resource://devtools/client/aboutdebugging/src/modules/client-wrapper.js");
+const {
+ remoteClientManager,
+} = require("resource://devtools/client/shared/remote-debugging/remote-client-manager.js");
+
+const {
+ RUNTIMES,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+async function createLocalClient() {
+ DevToolsServer.init();
+ DevToolsServer.registerAllActors();
+ DevToolsServer.allowChromeProcess = true;
+
+ const client = new DevToolsClient(DevToolsServer.connectPipe());
+ await client.connect();
+ return new ClientWrapper(client);
+}
+
+async function createNetworkClient(host, port) {
+ const transport = await DevToolsClient.socketConnect({ host, port });
+ const client = new DevToolsClient(transport);
+ await client.connect();
+ return new ClientWrapper(client);
+}
+
+async function createUSBClient(deviceId, socketPath) {
+ const port = await prepareTCPConnection(deviceId, socketPath);
+ return createNetworkClient("localhost", port);
+}
+
+async function createClientForRuntime(runtime) {
+ const { extra, id, type } = runtime;
+
+ if (type === RUNTIMES.THIS_FIREFOX) {
+ return createLocalClient();
+ } else if (remoteClientManager.hasClient(id, type)) {
+ const client = remoteClientManager.getClient(id, type);
+ return new ClientWrapper(client);
+ } else if (type === RUNTIMES.NETWORK) {
+ const { host, port } = extra.connectionParameters;
+ return createNetworkClient(host, port);
+ } else if (type === RUNTIMES.USB) {
+ const { deviceId, socketPath } = extra.connectionParameters;
+ return createUSBClient(deviceId, socketPath);
+ }
+
+ return null;
+}
+
+exports.createClientForRuntime = createClientForRuntime;
diff --git a/devtools/client/aboutdebugging/src/modules/runtime-default-preferences.js b/devtools/client/aboutdebugging/src/modules/runtime-default-preferences.js
new file mode 100644
index 0000000000..02c06334f7
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/modules/runtime-default-preferences.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";
+
+/**
+ * This module provides a workaround for remote debugging when a preference is
+ * defined in the firefox preference file (browser/app/profile/firefox.js) but
+ * still read from the server, without any default value.
+ *
+ * This causes the server to crash and can't easily be recovered.
+ *
+ * While we work on better linting to prevent such issues (Bug 1660182), this
+ * module will be able to set default values for all missing preferences.
+ */
+
+const PREFERENCE_TYPES = {
+ BOOL: "BOOL",
+ CHAR: "CHAR",
+ INT: "INT",
+};
+exports.PREFERENCE_TYPES = PREFERENCE_TYPES;
+
+/**
+ * Expected properties for the preference descriptors:
+ * - prefName {String}: the name of the preference.
+ * - defaultValue {String|Bool|Number}: the value to set if the preference is
+ * missing.
+ * - trait {String}: the name of the trait corresponding to this pref on the
+ * PreferenceFront.
+ * - type {String}: the preference type (either BOOL, CHAR or INT).
+ */
+const DEFAULT_PREFERENCES = [];
+exports.DEFAULT_PREFERENCES = DEFAULT_PREFERENCES;
+
+const METHODS = {
+ [PREFERENCE_TYPES.BOOL]: {
+ setPref: "setBoolPref",
+ getPref: "getBoolPref",
+ },
+ [PREFERENCE_TYPES.CHAR]: {
+ setPref: "setCharPref",
+ getPref: "getCharPref",
+ },
+ [PREFERENCE_TYPES.INT]: {
+ setPref: "setIntPref",
+ getPref: "getIntPref",
+ },
+};
+
+/**
+ * Set default values for all the provided preferences on the runtime
+ * corresponding to the provided clientWrapper, if needed.
+ *
+ * Note: prefDescriptors will most likely be DEFAULT_PREFERENCES when
+ * used in production code, but can be parameterized for tests.
+ *
+ * @param {ClientWrapper} clientWrapper
+ * @param {Array} prefDescriptors
+ * Array of preference descriptors, see DEFAULT_PREFERENCES.
+ */
+async function setDefaultPreferencesIfNeeded(clientWrapper, prefDescriptors) {
+ if (!prefDescriptors || prefDescriptors.length === 0) {
+ return;
+ }
+
+ const preferenceFront = await clientWrapper.getFront("preference");
+ const preferenceTraits = await preferenceFront.getTraits();
+
+ // Note: using Promise.all here fails because the request/responses get mixed.
+ for (const prefDescriptor of prefDescriptors) {
+ // If the fix for this preference is already on this server, skip it.
+ if (preferenceTraits[prefDescriptor.trait]) {
+ continue;
+ }
+
+ await setDefaultPreference(preferenceFront, prefDescriptor);
+ }
+}
+exports.setDefaultPreferencesIfNeeded = setDefaultPreferencesIfNeeded;
+
+async function setDefaultPreference(preferenceFront, prefDescriptor) {
+ const { prefName, type, defaultValue } = prefDescriptor;
+
+ if (!Object.values(PREFERENCE_TYPES).includes(type)) {
+ throw new Error(`Unsupported type for setDefaultPreference "${type}"`);
+ }
+
+ const prefMethods = METHODS[type];
+ try {
+ // Try to read the preference only to check if the call is successful.
+ // If not, this means the preference is missing and should be initialized.
+ await preferenceFront[prefMethods.getPref](prefName);
+ } catch (e) {
+ console.warn(
+ `Preference "${prefName}"" is not set on the remote runtime. Setting default value.`
+ );
+ await preferenceFront[prefMethods.setPref](prefName, defaultValue);
+ }
+}
diff --git a/devtools/client/aboutdebugging/src/modules/runtimes-state-helper.js b/devtools/client/aboutdebugging/src/modules/runtimes-state-helper.js
new file mode 100644
index 0000000000..1864ef8341
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/modules/runtimes-state-helper.js
@@ -0,0 +1,37 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+function getCurrentRuntime(runtimesState) {
+ const selectedRuntimeId = runtimesState.selectedRuntimeId;
+ return findRuntimeById(selectedRuntimeId, runtimesState);
+}
+exports.getCurrentRuntime = getCurrentRuntime;
+
+function getCurrentClient(runtimesState) {
+ const runtimeDetails = getCurrentRuntimeDetails(runtimesState);
+ return runtimeDetails ? runtimeDetails.clientWrapper : null;
+}
+exports.getCurrentClient = getCurrentClient;
+
+function findRuntimeById(id, runtimesState) {
+ return getAllRuntimes(runtimesState).find(r => r.id === id);
+}
+exports.findRuntimeById = findRuntimeById;
+
+function getAllRuntimes(runtimesState) {
+ return [
+ ...runtimesState.networkRuntimes,
+ ...runtimesState.thisFirefoxRuntimes,
+ ...runtimesState.usbRuntimes,
+ ];
+}
+exports.getAllRuntimes = getAllRuntimes;
+
+function getCurrentRuntimeDetails(runtimesState) {
+ const runtime = getCurrentRuntime(runtimesState);
+ return runtime ? runtime.runtimeDetails : null;
+}
+exports.getCurrentRuntimeDetails = getCurrentRuntimeDetails;
diff --git a/devtools/client/aboutdebugging/src/modules/usb-runtimes.js b/devtools/client/aboutdebugging/src/modules/usb-runtimes.js
new file mode 100644
index 0000000000..887dc6788e
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/modules/usb-runtimes.js
@@ -0,0 +1,122 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+loader.lazyRequireGetter(
+ this,
+ "adb",
+ "resource://devtools/client/shared/remote-debugging/adb/adb.js",
+ true
+);
+
+/**
+ * Used to represent a regular runtime returned by ADB.
+ */
+class UsbRuntime {
+ constructor(adbRuntime) {
+ this.id = adbRuntime.id;
+ this.deviceId = adbRuntime.deviceId;
+ this.deviceName = adbRuntime.deviceName;
+ this.shortName = adbRuntime.shortName;
+ this.socketPath = adbRuntime.socketPath;
+ this.isFenix = adbRuntime.isFenix;
+ this.isUnavailable = false;
+ this.isUnplugged = false;
+ this.versionName = adbRuntime.versionName;
+ }
+}
+
+/**
+ * Used when a device was detected, meaning USB debugging is enabled on the device, but no
+ * runtime/browser is available for connection.
+ */
+class UnavailableUsbRuntime {
+ constructor(adbDevice) {
+ this.id = adbDevice.id + "|unavailable";
+ this.deviceId = adbDevice.id;
+ this.deviceName = adbDevice.name;
+ this.shortName = "Unavailable runtime";
+ this.socketPath = null;
+ this.isFenix = false;
+ this.isUnavailable = true;
+ this.isUnplugged = false;
+ this.versionName = null;
+ }
+}
+
+/**
+ * Used to represent USB devices that were previously connected but are now missing
+ * (presumably after being unplugged/disconnected from the computer).
+ */
+class UnpluggedUsbRuntime {
+ constructor(deviceId, deviceName) {
+ this.id = deviceId + "|unplugged";
+ this.deviceId = deviceId;
+ this.deviceName = deviceName;
+ this.shortName = "Unplugged runtime";
+ this.socketPath = null;
+ this.isFenix = false;
+ this.isUnavailable = true;
+ this.isUnplugged = true;
+ this.versionName = null;
+ }
+}
+
+/**
+ * Map used to keep track of discovered usb devices. Will be used to create the unplugged
+ * usb runtimes.
+ */
+const devices = new Map();
+
+/**
+ * This module provides a collection of helper methods to detect USB runtimes whom Firefox
+ * is running on.
+ */
+function addUSBRuntimesObserver(listener) {
+ adb.registerListener(listener);
+}
+exports.addUSBRuntimesObserver = addUSBRuntimesObserver;
+
+async function getUSBRuntimes() {
+ // Get the available runtimes
+ const runtimes = adb.getRuntimes().map(r => new UsbRuntime(r));
+
+ // Get devices found by ADB, but without any available runtime.
+ const runtimeDevices = runtimes.map(r => r.deviceId);
+ const unavailableRuntimes = adb
+ .getDevices()
+ .filter(d => !runtimeDevices.includes(d.id))
+ .map(d => new UnavailableUsbRuntime(d));
+
+ // Add all devices to the map detected devices.
+ const allRuntimes = runtimes.concat(unavailableRuntimes);
+ for (const runtime of allRuntimes) {
+ devices.set(runtime.deviceId, runtime.deviceName);
+ }
+
+ // Get devices previously found by ADB but no longer available.
+ const currentDevices = allRuntimes.map(r => r.deviceId);
+ const detectedDevices = [...devices.keys()];
+ const unpluggedDevices = detectedDevices.filter(
+ id => !currentDevices.includes(id)
+ );
+ const unpluggedRuntimes = unpluggedDevices.map(deviceId => {
+ const deviceName = devices.get(deviceId);
+ return new UnpluggedUsbRuntime(deviceId, deviceName);
+ });
+
+ return allRuntimes.concat(unpluggedRuntimes);
+}
+exports.getUSBRuntimes = getUSBRuntimes;
+
+function removeUSBRuntimesObserver(listener) {
+ adb.unregisterListener(listener);
+}
+exports.removeUSBRuntimesObserver = removeUSBRuntimesObserver;
+
+function refreshUSBRuntimes() {
+ return adb.updateRuntimes();
+}
+exports.refreshUSBRuntimes = refreshUSBRuntimes;
diff --git a/devtools/client/aboutdebugging/src/moz.build b/devtools/client/aboutdebugging/src/moz.build
new file mode 100644
index 0000000000..58e6f92857
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/moz.build
@@ -0,0 +1,17 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DIRS += [
+ "actions",
+ "components",
+ "middleware",
+ "modules",
+ "reducers",
+ "types",
+]
+
+DevToolsModules(
+ "constants.js",
+ "create-store.js",
+)
diff --git a/devtools/client/aboutdebugging/src/reducers/debug-targets-state.js b/devtools/client/aboutdebugging/src/reducers/debug-targets-state.js
new file mode 100644
index 0000000000..8ee0473e3c
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/reducers/debug-targets-state.js
@@ -0,0 +1,155 @@
+/* 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 {
+ EXTENSION_BGSCRIPT_STATUS_UPDATED,
+ REQUEST_EXTENSIONS_SUCCESS,
+ REQUEST_PROCESSES_SUCCESS,
+ REQUEST_TABS_SUCCESS,
+ REQUEST_WORKERS_SUCCESS,
+ TEMPORARY_EXTENSION_RELOAD_FAILURE,
+ TEMPORARY_EXTENSION_RELOAD_START,
+ TERMINATE_EXTENSION_BGSCRIPT_FAILURE,
+ TERMINATE_EXTENSION_BGSCRIPT_START,
+ UNWATCH_RUNTIME_SUCCESS,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+function DebugTargetsState() {
+ return {
+ installedExtensions: [],
+ otherWorkers: [],
+ processes: [],
+ serviceWorkers: [],
+ sharedWorkers: [],
+ tabs: [],
+ temporaryExtensions: [],
+ };
+}
+
+function updateExtensionDetails(extensions, id, updatedDetails) {
+ // extensions is meant to be either state.installExtensions or state.temporaryExtensions.
+ return extensions.map(extension => {
+ if (extension.id === id) {
+ extension = Object.assign({}, extension);
+
+ extension.details = Object.assign({}, extension.details, updatedDetails);
+ }
+ return extension;
+ });
+}
+
+function updateTemporaryExtension(state, id, updatedDetails) {
+ return updateExtensionDetails(state.temporaryExtensions, id, updatedDetails);
+}
+
+function updateInstalledExtension(state, id, updatedDetails) {
+ return updateExtensionDetails(state.installedExtensions, id, updatedDetails);
+}
+
+function updateExtension(state, id, updatedDetails) {
+ return {
+ installedExtensions: updateInstalledExtension(state, id, updatedDetails),
+ temporaryExtensions: updateTemporaryExtension(state, id, updatedDetails),
+ };
+}
+
+function debugTargetsReducer(state = DebugTargetsState(), action) {
+ switch (action.type) {
+ case UNWATCH_RUNTIME_SUCCESS: {
+ return DebugTargetsState();
+ }
+ case REQUEST_EXTENSIONS_SUCCESS: {
+ const { installedExtensions, temporaryExtensions } = action;
+ return Object.assign({}, state, {
+ installedExtensions,
+ temporaryExtensions,
+ });
+ }
+ case REQUEST_PROCESSES_SUCCESS: {
+ const { processes } = action;
+ return Object.assign({}, state, { processes });
+ }
+ case REQUEST_TABS_SUCCESS: {
+ const { tabs } = action;
+ return Object.assign({}, state, { tabs });
+ }
+ case REQUEST_WORKERS_SUCCESS: {
+ const { otherWorkers, serviceWorkers, sharedWorkers } = action;
+ return Object.assign({}, state, {
+ otherWorkers,
+ serviceWorkers,
+ sharedWorkers,
+ });
+ }
+ case TEMPORARY_EXTENSION_RELOAD_FAILURE: {
+ const { id, error } = action;
+ const temporaryExtensions = updateTemporaryExtension(state, id, {
+ reloadError: error.message,
+ });
+ return Object.assign({}, state, { temporaryExtensions });
+ }
+ case TEMPORARY_EXTENSION_RELOAD_START: {
+ const { id } = action;
+ const temporaryExtensions = updateTemporaryExtension(state, id, {
+ reloadError: null,
+ });
+ return Object.assign({}, state, { temporaryExtensions });
+ }
+ case TERMINATE_EXTENSION_BGSCRIPT_START: {
+ const { id } = action;
+ const { installedExtensions, temporaryExtensions } = updateExtension(
+ state,
+ id,
+ {
+ // Clear the last error if one was still set.
+ lastTerminateBackgroundScriptError: null,
+ }
+ );
+ return Object.assign({}, state, {
+ installedExtensions,
+ temporaryExtensions,
+ });
+ }
+ case TERMINATE_EXTENSION_BGSCRIPT_FAILURE: {
+ const { id, error } = action;
+ const { installedExtensions, temporaryExtensions } = updateExtension(
+ state,
+ id,
+ {
+ lastTerminateBackgroundScriptError: error.message,
+ }
+ );
+ return Object.assign({}, state, {
+ installedExtensions,
+ temporaryExtensions,
+ });
+ }
+ case EXTENSION_BGSCRIPT_STATUS_UPDATED: {
+ const { id, backgroundScriptStatus } = action;
+ const { installedExtensions, temporaryExtensions } = updateExtension(
+ state,
+ id,
+ {
+ backgroundScriptStatus,
+ // Clear the last error if one was still set.
+ lastTerminateBackgroundScriptError: null,
+ }
+ );
+ return Object.assign({}, state, {
+ installedExtensions,
+ temporaryExtensions,
+ });
+ }
+
+ default:
+ return state;
+ }
+}
+
+module.exports = {
+ DebugTargetsState,
+ debugTargetsReducer,
+};
diff --git a/devtools/client/aboutdebugging/src/reducers/index.js b/devtools/client/aboutdebugging/src/reducers/index.js
new file mode 100644
index 0000000000..8f104a4ff7
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/reducers/index.js
@@ -0,0 +1,24 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+ combineReducers,
+} = require("resource://devtools/client/shared/vendor/redux.js");
+const {
+ debugTargetsReducer,
+} = require("resource://devtools/client/aboutdebugging/src/reducers/debug-targets-state.js");
+const {
+ runtimesReducer,
+} = require("resource://devtools/client/aboutdebugging/src/reducers/runtimes-state.js");
+const {
+ uiReducer,
+} = require("resource://devtools/client/aboutdebugging/src/reducers/ui-state.js");
+
+module.exports = combineReducers({
+ debugTargets: debugTargetsReducer,
+ runtimes: runtimesReducer,
+ ui: uiReducer,
+});
diff --git a/devtools/client/aboutdebugging/src/reducers/moz.build b/devtools/client/aboutdebugging/src/reducers/moz.build
new file mode 100644
index 0000000000..24d3382f5b
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/reducers/moz.build
@@ -0,0 +1,10 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ "debug-targets-state.js",
+ "index.js",
+ "runtimes-state.js",
+ "ui-state.js",
+)
diff --git a/devtools/client/aboutdebugging/src/reducers/runtimes-state.js b/devtools/client/aboutdebugging/src/reducers/runtimes-state.js
new file mode 100644
index 0000000000..5acf35390a
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/reducers/runtimes-state.js
@@ -0,0 +1,178 @@
+/* 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_RUNTIME_CANCEL,
+ CONNECT_RUNTIME_FAILURE,
+ CONNECT_RUNTIME_NOT_RESPONDING,
+ CONNECT_RUNTIME_START,
+ CONNECT_RUNTIME_SUCCESS,
+ DISCONNECT_RUNTIME_SUCCESS,
+ RUNTIMES,
+ UPDATE_CONNECTION_PROMPT_SETTING_SUCCESS,
+ REMOTE_RUNTIMES_UPDATED,
+ SELECTED_RUNTIME_ID_UPDATED,
+ THIS_FIREFOX_RUNTIME_CREATED,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+const {
+ findRuntimeById,
+} = require("resource://devtools/client/aboutdebugging/src/modules/runtimes-state-helper.js");
+
+const {
+ remoteClientManager,
+} = require("resource://devtools/client/shared/remote-debugging/remote-client-manager.js");
+
+// Map between known runtime types and nodes in the runtimes state.
+const TYPE_TO_RUNTIMES_KEY = {
+ [RUNTIMES.THIS_FIREFOX]: "thisFirefoxRuntimes",
+ [RUNTIMES.NETWORK]: "networkRuntimes",
+ [RUNTIMES.USB]: "usbRuntimes",
+};
+
+function RuntimesState() {
+ return {
+ networkRuntimes: [],
+ selectedRuntimeId: null,
+ // "This Firefox" runtimes is an array for consistency, but it should only contain one
+ // runtime. This runtime will be added after initializing the application via
+ // THIS_FIREFOX_RUNTIME_CREATED.
+ thisFirefoxRuntimes: [],
+ usbRuntimes: [],
+ };
+}
+
+/**
+ * Update the runtime matching the provided runtimeId with the content of updatedRuntime,
+ * and return the new state.
+ *
+ * @param {String} runtimeId
+ * The id of the runtime to update
+ * @param {Object} updatedRuntime
+ * Object used to update the runtime matching the idea using Object.assign.
+ * @param {Object} state
+ * Current runtimes state.
+ * @return {Object} The updated state
+ */
+function _updateRuntimeById(runtimeId, updatedRuntime, state) {
+ // Find the array of runtimes that contains the updated runtime.
+ const runtime = findRuntimeById(runtimeId, state);
+ const key = TYPE_TO_RUNTIMES_KEY[runtime.type];
+ const runtimesToUpdate = state[key];
+
+ // Update the runtime with the provided updatedRuntime.
+ const updatedRuntimes = runtimesToUpdate.map(r => {
+ if (r.id === runtimeId) {
+ return Object.assign({}, r, updatedRuntime);
+ }
+ return r;
+ });
+ return Object.assign({}, state, { [key]: updatedRuntimes });
+}
+
+function runtimesReducer(state = RuntimesState(), action) {
+ switch (action.type) {
+ case CONNECT_RUNTIME_START: {
+ const { id } = action;
+ const updatedState = {
+ isConnecting: true,
+ isConnectionFailed: false,
+ isConnectionNotResponding: false,
+ isConnectionTimeout: false,
+ };
+ return _updateRuntimeById(id, updatedState, state);
+ }
+
+ case CONNECT_RUNTIME_NOT_RESPONDING: {
+ const { id } = action;
+ return _updateRuntimeById(id, { isConnectionNotResponding: true }, state);
+ }
+
+ case CONNECT_RUNTIME_CANCEL: {
+ const { id } = action;
+ const updatedState = {
+ isConnecting: false,
+ isConnectionFailed: false,
+ isConnectionNotResponding: false,
+ isConnectionTimeout: true,
+ };
+ return _updateRuntimeById(id, updatedState, state);
+ }
+
+ case CONNECT_RUNTIME_SUCCESS: {
+ const { id, runtimeDetails, type } = action.runtime;
+
+ // Update the remoteClientManager with the connected runtime.
+ const client = runtimeDetails.clientWrapper.client;
+ const runtimeInfo = runtimeDetails.info;
+ remoteClientManager.setClient(id, type, client, runtimeInfo);
+
+ const updatedState = {
+ isConnecting: false,
+ isConnectionFailed: false,
+ isConnectionNotResponding: false,
+ isConnectionTimeout: false,
+ runtimeDetails,
+ };
+ return _updateRuntimeById(id, updatedState, state);
+ }
+
+ case CONNECT_RUNTIME_FAILURE: {
+ const { id } = action;
+ const updatedState = {
+ isConnecting: false,
+ isConnectionFailed: true,
+ isConnectionNotResponding: false,
+ isConnectionTimeout: false,
+ };
+ return _updateRuntimeById(id, updatedState, state);
+ }
+
+ case DISCONNECT_RUNTIME_SUCCESS: {
+ const { id, type } = action.runtime;
+ remoteClientManager.removeClient(id, type);
+ return _updateRuntimeById(id, { runtimeDetails: null }, state);
+ }
+
+ case SELECTED_RUNTIME_ID_UPDATED: {
+ const selectedRuntimeId = action.runtimeId || null;
+ return Object.assign({}, state, { selectedRuntimeId });
+ }
+
+ case UPDATE_CONNECTION_PROMPT_SETTING_SUCCESS: {
+ const { connectionPromptEnabled } = action;
+ const { id: runtimeId } = action.runtime;
+ const runtime = findRuntimeById(runtimeId, state);
+ const runtimeDetails = Object.assign({}, runtime.runtimeDetails, {
+ connectionPromptEnabled,
+ });
+ return _updateRuntimeById(runtimeId, { runtimeDetails }, state);
+ }
+
+ case REMOTE_RUNTIMES_UPDATED: {
+ const { runtimes, runtimeType } = action;
+ const key = TYPE_TO_RUNTIMES_KEY[runtimeType];
+ return Object.assign({}, state, {
+ [key]: runtimes,
+ });
+ }
+
+ case THIS_FIREFOX_RUNTIME_CREATED: {
+ const { runtime } = action;
+ return Object.assign({}, state, {
+ thisFirefoxRuntimes: [runtime],
+ });
+ }
+
+ default:
+ return state;
+ }
+}
+
+module.exports = {
+ RuntimesState,
+ runtimesReducer,
+};
diff --git a/devtools/client/aboutdebugging/src/reducers/ui-state.js b/devtools/client/aboutdebugging/src/reducers/ui-state.js
new file mode 100644
index 0000000000..771358eaac
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/reducers/ui-state.js
@@ -0,0 +1,115 @@
+/* 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 {
+ ADB_ADDON_STATUS_UPDATED,
+ ADB_READY_UPDATED,
+ DEBUG_TARGET_COLLAPSIBILITY_UPDATED,
+ HIDE_PROFILER_DIALOG,
+ NETWORK_LOCATIONS_UPDATE_SUCCESS,
+ PROFILER_PAGE_CONTEXT,
+ SELECT_PAGE_SUCCESS,
+ SHOW_PROFILER_DIALOG,
+ SWITCH_PROFILER_CONTEXT,
+ TEMPORARY_EXTENSION_INSTALL_FAILURE,
+ TEMPORARY_EXTENSION_INSTALL_SUCCESS,
+ USB_RUNTIMES_SCAN_START,
+ USB_RUNTIMES_SCAN_SUCCESS,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+function UiState(
+ locations = [],
+ debugTargetCollapsibilities = {},
+ showHiddenAddons = false
+) {
+ return {
+ adbAddonStatus: null,
+ debugTargetCollapsibilities,
+ isAdbReady: false,
+ isScanningUsb: false,
+ networkLocations: locations,
+ profilerContext: PROFILER_PAGE_CONTEXT.DEVTOOLS_REMOTE,
+ selectedPage: null,
+ showProfilerDialog: false,
+ showHiddenAddons,
+ temporaryInstallError: null,
+ };
+}
+
+function uiReducer(state = UiState(), action) {
+ switch (action.type) {
+ case ADB_ADDON_STATUS_UPDATED: {
+ const { adbAddonStatus } = action;
+ return Object.assign({}, state, { adbAddonStatus });
+ }
+
+ case ADB_READY_UPDATED: {
+ const { isAdbReady } = action;
+ return Object.assign({}, state, { isAdbReady });
+ }
+
+ case DEBUG_TARGET_COLLAPSIBILITY_UPDATED: {
+ const { isCollapsed, key } = action;
+ const debugTargetCollapsibilities = new Map(
+ state.debugTargetCollapsibilities
+ );
+ debugTargetCollapsibilities.set(key, isCollapsed);
+ return Object.assign({}, state, { debugTargetCollapsibilities });
+ }
+
+ case NETWORK_LOCATIONS_UPDATE_SUCCESS: {
+ const { locations } = action;
+ return Object.assign({}, state, { networkLocations: locations });
+ }
+
+ case SELECT_PAGE_SUCCESS: {
+ const { page } = action;
+ return Object.assign({}, state, { selectedPage: page });
+ }
+
+ case SHOW_PROFILER_DIALOG: {
+ return Object.assign({}, state, {
+ showProfilerDialog: true,
+ // Always start in the devtools-remote view.
+ profilerContext: "devtools-remote",
+ });
+ }
+
+ case HIDE_PROFILER_DIALOG: {
+ return Object.assign({}, state, { showProfilerDialog: false });
+ }
+
+ case SWITCH_PROFILER_CONTEXT: {
+ const { profilerContext } = action;
+ return Object.assign({}, state, { profilerContext });
+ }
+
+ case USB_RUNTIMES_SCAN_START: {
+ return Object.assign({}, state, { isScanningUsb: true });
+ }
+
+ case USB_RUNTIMES_SCAN_SUCCESS: {
+ return Object.assign({}, state, { isScanningUsb: false });
+ }
+
+ case TEMPORARY_EXTENSION_INSTALL_SUCCESS: {
+ return Object.assign({}, state, { temporaryInstallError: null });
+ }
+
+ case TEMPORARY_EXTENSION_INSTALL_FAILURE: {
+ const { error } = action;
+ return Object.assign({}, state, { temporaryInstallError: error });
+ }
+
+ default:
+ return state;
+ }
+}
+
+module.exports = {
+ UiState,
+ uiReducer,
+};
diff --git a/devtools/client/aboutdebugging/src/types/debug-target.js b/devtools/client/aboutdebugging/src/types/debug-target.js
new file mode 100644
index 0000000000..c2f419082a
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/types/debug-target.js
@@ -0,0 +1,71 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ DEBUG_TARGETS,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+const extensionTargetDetails = {
+ // actor ID for this extention.
+ actor: PropTypes.string.isRequired,
+ location: PropTypes.string.isRequired,
+ // error message forwarded from the WebExtensions internals if terminating the background script failed.
+ lastTerminateBackgroundScriptError: PropTypes.string,
+ // manifestURL points to the manifest.json file. This URL is only valid when debugging
+ // local extensions so it might be null.
+ manifestURL: PropTypes.string,
+ // error message forwarded from the addon manager during reloading temporary extension.
+ reloadError: PropTypes.string,
+ // unique extension id.
+ uuid: PropTypes.string.isRequired,
+ // warning messages forwarded from the addon manager.
+ warnings: PropTypes.arrayOf(PropTypes.string).isRequired,
+};
+
+const processTargetDetails = {
+ // Description for the process.
+ description: PropTypes.string.isRequired,
+};
+
+const tabTargetDetails = {
+ // the url of the tab.
+ url: PropTypes.string.isRequired,
+};
+
+const workerTargetDetails = {
+ // (service worker specific) one of "LISTENING", "NOT_LISTENING". undefined otherwise.
+ fetch: PropTypes.string,
+ // front for the ServiceWorkerRegistration related to this service worker.
+ registrationFront: PropTypes.object,
+ // (service worker specific) scope of the service worker registration.
+ scope: PropTypes.string,
+ // (service worker specific) one of "RUNNING", "REGISTERING", "STOPPED".
+ status: PropTypes.string,
+};
+
+const debugTarget = {
+ // details property will contain a type-specific object.
+ details: PropTypes.oneOfType([
+ PropTypes.shape(extensionTargetDetails),
+ PropTypes.shape(processTargetDetails),
+ PropTypes.shape(tabTargetDetails),
+ PropTypes.shape(workerTargetDetails),
+ ]).isRequired,
+ // icon to display for the debug target.
+ icon: PropTypes.string.isRequired,
+ // unique id for the target (unique in the scope of the application lifecycle).
+ // - extensions: {String} extension id (for instance "someextension@mozilla.org")
+ // - tabs: {Number} browserId
+ // - workers: {String} id for the WorkerTargetActor corresponding to the worker
+ id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
+ // display name for the debug target.
+ name: PropTypes.string.isRequired,
+ // one of "extension", "tab", "worker", "process".
+ type: PropTypes.oneOf(Object.values(DEBUG_TARGETS)).isRequired,
+};
+
+exports.debugTarget = PropTypes.shape(debugTarget);
diff --git a/devtools/client/aboutdebugging/src/types/index.js b/devtools/client/aboutdebugging/src/types/index.js
new file mode 100644
index 0000000000..288a063be1
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/types/index.js
@@ -0,0 +1,18 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const debugTargetTypes = require("resource://devtools/client/aboutdebugging/src/types/debug-target.js");
+const runtimeTypes = require("resource://devtools/client/aboutdebugging/src/types/runtime.js");
+const uiTypes = require("resource://devtools/client/aboutdebugging/src/types/ui.js");
+
+module.exports = Object.assign(
+ {},
+ {
+ ...debugTargetTypes,
+ ...runtimeTypes,
+ ...uiTypes,
+ }
+);
diff --git a/devtools/client/aboutdebugging/src/types/moz.build b/devtools/client/aboutdebugging/src/types/moz.build
new file mode 100644
index 0000000000..a58a6e0e28
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/types/moz.build
@@ -0,0 +1,10 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+ "debug-target.js",
+ "index.js",
+ "runtime.js",
+ "ui.js",
+)
diff --git a/devtools/client/aboutdebugging/src/types/runtime.js b/devtools/client/aboutdebugging/src/types/runtime.js
new file mode 100644
index 0000000000..cef9ace204
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/types/runtime.js
@@ -0,0 +1,164 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ ClientWrapper,
+} = require("resource://devtools/client/aboutdebugging/src/modules/client-wrapper.js");
+const {
+ COMPATIBILITY_STATUS,
+} = require("resource://devtools/client/shared/remote-debugging/version-checker.js");
+
+const runtimeInfo = {
+ // device name which is running the runtime,
+ // unavailable on this-firefox runtime
+ deviceName: PropTypes.string,
+
+ // icon which represents the kind of runtime
+ icon: PropTypes.string.isRequired,
+
+ // name of runtime such as "Firefox Nightly"
+ name: PropTypes.string.isRequired,
+
+ // operating system on which the runtime runs such as "Android", "Linux"
+ os: PropTypes.string.isRequired,
+
+ // runtime type, for instance "network", "usb" ...
+ type: PropTypes.string.isRequired,
+
+ // version of runtime
+ version: PropTypes.string.isRequired,
+};
+
+const compatibilityReport = {
+ // build ID for the current runtime (date formatted as yyyyMMdd eg "20193101")
+ localID: PropTypes.string.isRequired,
+
+ // "platform" version for the current runtime (eg "67.0a1")
+ localVersion: PropTypes.string.isRequired,
+
+ // minimum "platform" version supported for remote debugging by the current runtime
+ minVersion: PropTypes.string.isRequired,
+
+ // build ID for the target runtime (date formatted as yyyyMMdd eg "20193101")
+ runtimeID: PropTypes.string.isRequired,
+
+ // "platform" version for the target runtime (eg "67.0a1")
+ runtimeVersion: PropTypes.string.isRequired,
+
+ // report result, either COMPATIBLE, TOO_OLD or TOO_RECENT
+ status: PropTypes.oneOf(Object.values(COMPATIBILITY_STATUS)).isRequired,
+};
+exports.compatibilityReport = PropTypes.shape(compatibilityReport);
+
+const runtimeDetails = {
+ // True if this runtime supports debugging service workers.
+ // This might be undefined when connecting to runtimes older than Fx 66
+ canDebugServiceWorkers: PropTypes.bool,
+
+ // ClientWrapper built using a DevToolsClient for the runtime
+ clientWrapper: PropTypes.instanceOf(ClientWrapper).isRequired,
+
+ // compatibility report to check if the target runtime is in range of the backward
+ // compatibility policy for DevTools remote debugging.
+ compatibilityReport: PropTypes.shape(compatibilityReport).isRequired,
+
+ // reflect devtools.debugger.prompt-connection preference of this runtime
+ connectionPromptEnabled: PropTypes.bool.isRequired,
+
+ // runtime information
+ info: PropTypes.shape(runtimeInfo).isRequired,
+
+ // True if service workers should be available in the target runtime. Service workers
+ // can be disabled via preferences or if the runtime runs in fully private browsing
+ // mode.
+ serviceWorkersAvailable: PropTypes.bool.isRequired,
+};
+exports.runtimeDetails = PropTypes.shape(runtimeDetails);
+
+const networkRuntimeConnectionParameter = {
+ // host name of devtools server to connect
+ host: PropTypes.string.isRequired,
+
+ // port number of devtools server to connect
+ port: PropTypes.number.isRequired,
+};
+
+const usbRuntimeConnectionParameter = {
+ // device id
+ deviceId: PropTypes.string.isRequired,
+ // socket path to connect devtools server
+ socketPath: PropTypes.string.isRequired,
+};
+
+const runtimeExtra = {
+ // parameter to connect to devtools server
+ // unavailable on unavailable/unplugged runtimes
+ connectionParameters: PropTypes.oneOfType([
+ PropTypes.shape(networkRuntimeConnectionParameter),
+ PropTypes.shape(usbRuntimeConnectionParameter),
+ ]),
+
+ // device name
+ // unavailable on this-firefox and network-location runtimes
+ deviceName: PropTypes.string,
+
+ // version of the application coming from ADB, only available via USB. Useful for Fenix
+ // runtimes, because the version can't be retrieved from Service.appInfo.
+ adbPackageVersion: PropTypes.string,
+};
+
+const runtime = {
+ // unique id for the runtime
+ id: PropTypes.string.isRequired,
+
+ // object containing non standard properties that depend on the runtime type,
+ // unavailable on this-firefox runtime
+ extra: PropTypes.shape(runtimeExtra),
+
+ // this flag will be true when start to connect to the runtime, will be false after
+ // connected or has failures.
+ isConnecting: PropTypes.bool.isRequired,
+
+ // this flag will be true when the connection failed.
+ isConnectionFailed: PropTypes.bool.isRequired,
+
+ // will be true if connecting to runtime is taking time, will be false after connecting
+ // or failing.
+ isConnectionNotResponding: PropTypes.bool.isRequired,
+
+ // this flag will be true when the connection was timeout.
+ isConnectionTimeout: PropTypes.bool.isRequired,
+
+ // this flag will be true when the detected runtime is Fenix (Firefox Preview).
+ // Fenix need specific logic to get their display name, version and logos.
+ // Discussion ongoing in https://github.com/mozilla-mobile/fenix/issues/2016
+ isFenix: PropTypes.bool.isRequired,
+
+ // unavailable runtimes are placeholders for devices where the runtime has not been
+ // started yet. For instance an ADB device connected without a compatible runtime
+ // running.
+ isUnavailable: PropTypes.bool.isRequired,
+
+ // unplugged runtimes are placeholders for devices that are no longer available. For
+ // instance a USB device that was unplugged from the computer.
+ isUnplugged: PropTypes.bool.isRequired,
+
+ // display name of the runtime
+ name: PropTypes.string.isRequired,
+
+ // available after the connection to the runtime is established
+ // unavailable on disconnected runtimes
+ runtimeDetails: PropTypes.shape(runtimeDetails),
+
+ // runtime type, for instance "network", "usb" ...
+ type: PropTypes.string.isRequired,
+};
+
+/**
+ * Export type of runtime
+ */
+exports.runtime = PropTypes.shape(runtime);
diff --git a/devtools/client/aboutdebugging/src/types/ui.js b/devtools/client/aboutdebugging/src/types/ui.js
new file mode 100644
index 0000000000..68daaaa5d2
--- /dev/null
+++ b/devtools/client/aboutdebugging/src/types/ui.js
@@ -0,0 +1,85 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const {
+ ADB_ADDON_STATES,
+} = require("resource://devtools/client/shared/remote-debugging/adb/adb-addon.js");
+const {
+ DEBUG_TARGET_PANE,
+ PAGE_TYPES,
+} = require("resource://devtools/client/aboutdebugging/src/constants.js");
+
+function makeCollapsibilitiesType(isRequired) {
+ return (props, propName, componentName, _, propFullName) => {
+ if (isRequired && props[propName] === null) {
+ return new Error(
+ `Missing prop ${propFullName} marked as required in ${componentName}`
+ );
+ }
+
+ const error = new Error(
+ `Invalid prop ${propFullName} (${props[propName]}) supplied to ` +
+ `${componentName}. Collapsibilities needs to be a Map<DEBUG_TARGET_PANE, bool>`
+ );
+
+ const map = props[propName];
+
+ // check that the prop is a Map
+ if (!(map instanceof Map)) {
+ return error;
+ }
+
+ // check that the keys refer to debug target panes
+ const areKeysValid = [...map.keys()].every(x =>
+ Object.values(DEBUG_TARGET_PANE).includes(x)
+ );
+ // check that the values are boolean
+ const areValuesValid = [...map.values()].every(x => typeof x === "boolean");
+ // error if values or keys fail their checks
+ if (!areKeysValid || !areValuesValid) {
+ return error;
+ }
+
+ return null;
+ };
+}
+
+function makeLocationType(isRequired) {
+ return (props, propName, componentName, _, propFullName) => {
+ if (isRequired && props[propName] === null) {
+ return new Error(
+ `Missing prop ${propFullName} marked as required in ${componentName}`
+ );
+ }
+
+ // check that location is a string with a semicolon in it
+ if (!/\:/.test(props[propName])) {
+ return new Error(
+ `Invalid prop ${propFullName} (${props[propName]}) supplied to ` +
+ `${componentName}. Location needs to be a string with a host:port format`
+ );
+ }
+
+ return null;
+ };
+}
+
+const collapsibilities = makeCollapsibilitiesType(false);
+collapsibilities.isRequired = makeCollapsibilitiesType(true);
+
+const location = makeLocationType(false);
+location.isRequired = makeLocationType(true);
+
+module.exports = {
+ adbAddonStatus: PropTypes.oneOf(Object.values(ADB_ADDON_STATES)),
+ // a Map<DEBUG_TARGET_PANE, bool>, to flag collapsed/expanded status of the
+ // debug target panes
+ collapsibilities,
+ // a string with "host:port" format, used for network locations
+ location,
+ page: PropTypes.oneOf(Object.values(PAGE_TYPES)),
+};