diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /devtools/client/aboutdebugging/src | |
parent | Initial commit. (diff) | |
download | firefox-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')
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)), +}; |