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