diff options
Diffstat (limited to 'devtools/client/aboutdebugging/components')
18 files changed, 1309 insertions, 0 deletions
diff --git a/devtools/client/aboutdebugging/components/aboutdebugging.js b/devtools/client/aboutdebugging/components/aboutdebugging.js new file mode 100644 index 000000000..601574dcb --- /dev/null +++ b/devtools/client/aboutdebugging/components/aboutdebugging.js @@ -0,0 +1,111 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* eslint-env browser */ + +"use strict"; + +const { createFactory, createClass, DOM: dom, PropTypes } = + require("devtools/client/shared/vendor/react"); +const Services = require("Services"); + +const PanelMenu = createFactory(require("./panel-menu")); + +loader.lazyGetter(this, "AddonsPanel", + () => createFactory(require("./addons/panel"))); +loader.lazyGetter(this, "TabsPanel", + () => createFactory(require("./tabs/panel"))); +loader.lazyGetter(this, "WorkersPanel", + () => createFactory(require("./workers/panel"))); + +loader.lazyRequireGetter(this, "DebuggerClient", + "devtools/shared/client/main", true); +loader.lazyRequireGetter(this, "Telemetry", + "devtools/client/shared/telemetry"); + +const Strings = Services.strings.createBundle( + "chrome://devtools/locale/aboutdebugging.properties"); + +const panels = [{ + id: "addons", + name: Strings.GetStringFromName("addons"), + icon: "chrome://devtools/skin/images/debugging-addons.svg", + component: AddonsPanel +}, { + id: "tabs", + name: Strings.GetStringFromName("tabs"), + icon: "chrome://devtools/skin/images/debugging-tabs.svg", + component: TabsPanel +}, { + id: "workers", + name: Strings.GetStringFromName("workers"), + icon: "chrome://devtools/skin/images/debugging-workers.svg", + component: WorkersPanel +}]; + +const defaultPanelId = "addons"; + +module.exports = createClass({ + displayName: "AboutDebuggingApp", + + propTypes: { + client: PropTypes.instanceOf(DebuggerClient).isRequired, + telemetry: PropTypes.instanceOf(Telemetry).isRequired + }, + + getInitialState() { + return { + selectedPanelId: defaultPanelId + }; + }, + + componentDidMount() { + window.addEventListener("hashchange", this.onHashChange); + this.onHashChange(); + this.props.telemetry.toolOpened("aboutdebugging"); + }, + + componentWillUnmount() { + window.removeEventListener("hashchange", this.onHashChange); + this.props.telemetry.toolClosed("aboutdebugging"); + this.props.telemetry.destroy(); + }, + + onHashChange() { + this.setState({ + selectedPanelId: window.location.hash.substr(1) || defaultPanelId + }); + }, + + selectPanel(panelId) { + window.location.hash = "#" + panelId; + }, + + render() { + let { client } = this.props; + let { selectedPanelId } = this.state; + let selectPanel = this.selectPanel; + let selectedPanel = panels.find(p => p.id == selectedPanelId); + let panel; + + if (selectedPanel) { + panel = selectedPanel.component({ client, id: selectedPanel.id }); + } else { + panel = ( + dom.div({ className: "error-page" }, + dom.h1({ className: "header-name" }, + Strings.GetStringFromName("pageNotFound") + ), + dom.h4({ className: "error-page-details" }, + Strings.formatStringFromName("doesNotExist", [selectedPanelId], 1)) + ) + ); + } + + return dom.div({ className: "app" }, + PanelMenu({ panels, selectedPanelId, selectPanel }), + dom.div({ className: "main-content" }, panel) + ); + } +}); diff --git a/devtools/client/aboutdebugging/components/addons/controls.js b/devtools/client/aboutdebugging/components/addons/controls.js new file mode 100644 index 000000000..7f985528c --- /dev/null +++ b/devtools/client/aboutdebugging/components/addons/controls.js @@ -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/. */ + +/* eslint-env browser */ +/* globals AddonManager */ + +"use strict"; + +loader.lazyImporter(this, "AddonManager", + "resource://gre/modules/AddonManager.jsm"); + +const { Cc, Ci } = require("chrome"); +const { createFactory, createClass, DOM: dom, PropTypes } = + require("devtools/client/shared/vendor/react"); +const Services = require("Services"); +const AddonsInstallError = createFactory(require("./install-error")); + +const Strings = Services.strings.createBundle( + "chrome://devtools/locale/aboutdebugging.properties"); + +const MORE_INFO_URL = "https://developer.mozilla.org/docs/Tools" + + "/about:debugging#Enabling_add-on_debugging"; + +module.exports = createClass({ + displayName: "AddonsControls", + + propTypes: { + debugDisabled: PropTypes.bool + }, + + getInitialState() { + return { + installError: null, + }; + }, + + onEnableAddonDebuggingChange(event) { + let enabled = event.target.checked; + Services.prefs.setBoolPref("devtools.chrome.enabled", enabled); + Services.prefs.setBoolPref("devtools.debugger.remote-enabled", enabled); + }, + + loadAddonFromFile() { + this.setState({ installError: null }); + let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + fp.init(window, + Strings.GetStringFromName("selectAddonFromFile2"), + Ci.nsIFilePicker.modeOpen); + let res = fp.show(); + if (res == Ci.nsIFilePicker.returnCancel || !fp.file) { + return; + } + let file = fp.file; + // AddonManager.installTemporaryAddon accepts either + // addon directory or final xpi file. + if (!file.isDirectory() && !file.leafName.endsWith(".xpi")) { + file = file.parent; + } + + AddonManager.installTemporaryAddon(file) + .catch(e => { + console.error(e); + this.setState({ installError: e.message }); + }); + }, + + render() { + let { debugDisabled } = this.props; + + return dom.div({ className: "addons-top" }, + dom.div({ className: "addons-controls" }, + dom.div({ className: "addons-options toggle-container-with-text" }, + dom.input({ + id: "enable-addon-debugging", + type: "checkbox", + checked: !debugDisabled, + onChange: this.onEnableAddonDebuggingChange, + }), + dom.label({ + className: "addons-debugging-label", + htmlFor: "enable-addon-debugging", + title: Strings.GetStringFromName("addonDebugging.tooltip") + }, Strings.GetStringFromName("addonDebugging.label")), + "(", + dom.a({ href: MORE_INFO_URL, target: "_blank" }, + Strings.GetStringFromName("moreInfo")), + ")" + ), + dom.button({ + id: "load-addon-from-file", + onClick: this.loadAddonFromFile, + }, Strings.GetStringFromName("loadTemporaryAddon")) + ), + AddonsInstallError({ error: this.state.installError })); + } +}); diff --git a/devtools/client/aboutdebugging/components/addons/install-error.js b/devtools/client/aboutdebugging/components/addons/install-error.js new file mode 100644 index 000000000..aea1c4f09 --- /dev/null +++ b/devtools/client/aboutdebugging/components/addons/install-error.js @@ -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/. */ + +/* eslint-env browser */ +"use strict"; + +const { createClass, DOM: dom, PropTypes } = require("devtools/client/shared/vendor/react"); + +module.exports = createClass({ + displayName: "AddonsInstallError", + + propTypes: { + error: PropTypes.string + }, + + render() { + if (!this.props.error) { + return null; + } + let text = `There was an error during installation: ${this.props.error}`; + return dom.div({ className: "addons-install-error" }, + dom.div({ className: "warning" }), + dom.span({}, text)); + } +}); diff --git a/devtools/client/aboutdebugging/components/addons/moz.build b/devtools/client/aboutdebugging/components/addons/moz.build new file mode 100644 index 000000000..378554f78 --- /dev/null +++ b/devtools/client/aboutdebugging/components/addons/moz.build @@ -0,0 +1,10 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + 'controls.js', + 'install-error.js', + 'panel.js', + 'target.js', +) diff --git a/devtools/client/aboutdebugging/components/addons/panel.js b/devtools/client/aboutdebugging/components/addons/panel.js new file mode 100644 index 000000000..425a10a8d --- /dev/null +++ b/devtools/client/aboutdebugging/components/addons/panel.js @@ -0,0 +1,146 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { AddonManager } = require("resource://gre/modules/AddonManager.jsm"); +const { createFactory, createClass, DOM: dom, PropTypes } = + require("devtools/client/shared/vendor/react"); +const Services = require("Services"); + +const AddonsControls = createFactory(require("./controls")); +const AddonTarget = createFactory(require("./target")); +const PanelHeader = createFactory(require("../panel-header")); +const TargetList = createFactory(require("../target-list")); + +loader.lazyRequireGetter(this, "DebuggerClient", + "devtools/shared/client/main", true); + +const Strings = Services.strings.createBundle( + "chrome://devtools/locale/aboutdebugging.properties"); + +const ExtensionIcon = "chrome://mozapps/skin/extensions/extensionGeneric.svg"; +const CHROME_ENABLED_PREF = "devtools.chrome.enabled"; +const REMOTE_ENABLED_PREF = "devtools.debugger.remote-enabled"; + +module.exports = createClass({ + displayName: "AddonsPanel", + + propTypes: { + client: PropTypes.instanceOf(DebuggerClient).isRequired, + id: PropTypes.string.isRequired + }, + + getInitialState() { + return { + extensions: [], + debugDisabled: false, + }; + }, + + componentDidMount() { + AddonManager.addAddonListener(this); + + Services.prefs.addObserver(CHROME_ENABLED_PREF, + this.updateDebugStatus, false); + Services.prefs.addObserver(REMOTE_ENABLED_PREF, + this.updateDebugStatus, false); + + this.updateDebugStatus(); + this.updateAddonsList(); + }, + + componentWillUnmount() { + AddonManager.removeAddonListener(this); + Services.prefs.removeObserver(CHROME_ENABLED_PREF, + this.updateDebugStatus); + Services.prefs.removeObserver(REMOTE_ENABLED_PREF, + this.updateDebugStatus); + }, + + updateDebugStatus() { + let debugDisabled = + !Services.prefs.getBoolPref(CHROME_ENABLED_PREF) || + !Services.prefs.getBoolPref(REMOTE_ENABLED_PREF); + + this.setState({ debugDisabled }); + }, + + updateAddonsList() { + this.props.client.listAddons() + .then(({addons}) => { + let extensions = addons.filter(addon => addon.debuggable).map(addon => { + return { + name: addon.name, + icon: addon.iconURL || ExtensionIcon, + addonID: addon.id, + addonActor: addon.actor, + temporarilyInstalled: addon.temporarilyInstalled + }; + }); + + this.setState({ extensions }); + }, error => { + throw new Error("Client error while listing addons: " + error); + }); + }, + + /** + * Mandatory callback as AddonManager listener. + */ + onInstalled() { + this.updateAddonsList(); + }, + + /** + * Mandatory callback as AddonManager listener. + */ + onUninstalled() { + this.updateAddonsList(); + }, + + /** + * Mandatory callback as AddonManager listener. + */ + onEnabled() { + this.updateAddonsList(); + }, + + /** + * Mandatory callback as AddonManager listener. + */ + onDisabled() { + this.updateAddonsList(); + }, + + render() { + let { client, id } = this.props; + let { debugDisabled, extensions: targets } = this.state; + let name = Strings.GetStringFromName("extensions"); + let targetClass = AddonTarget; + + return dom.div({ + id: id + "-panel", + className: "panel", + role: "tabpanel", + "aria-labelledby": id + "-header" + }, + PanelHeader({ + id: id + "-header", + name: Strings.GetStringFromName("addons") + }), + AddonsControls({ debugDisabled }), + dom.div({ id: "addons" }, + TargetList({ + id: "extensions", + name, + targets, + client, + debugDisabled, + targetClass, + sort: true + }) + )); + } +}); diff --git a/devtools/client/aboutdebugging/components/addons/target.js b/devtools/client/aboutdebugging/components/addons/target.js new file mode 100644 index 000000000..c21499650 --- /dev/null +++ b/devtools/client/aboutdebugging/components/addons/target.js @@ -0,0 +1,84 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* eslint-env browser */ + +"use strict"; + +const { createClass, DOM: dom, PropTypes } = + require("devtools/client/shared/vendor/react"); +const { debugAddon } = require("../../modules/addon"); +const Services = require("Services"); + +loader.lazyImporter(this, "BrowserToolboxProcess", + "resource://devtools/client/framework/ToolboxProcess.jsm"); + +loader.lazyRequireGetter(this, "DebuggerClient", + "devtools/shared/client/main", true); + +const Strings = Services.strings.createBundle( + "chrome://devtools/locale/aboutdebugging.properties"); + +module.exports = createClass({ + displayName: "AddonTarget", + + propTypes: { + client: PropTypes.instanceOf(DebuggerClient).isRequired, + debugDisabled: PropTypes.bool, + target: PropTypes.shape({ + addonActor: PropTypes.string.isRequired, + addonID: PropTypes.string.isRequired, + icon: PropTypes.string, + name: PropTypes.string.isRequired, + temporarilyInstalled: PropTypes.bool + }).isRequired + }, + + debug() { + let { target } = this.props; + debugAddon(target.addonID); + }, + + reload() { + let { client, target } = this.props; + // This function sometimes returns a partial promise that only + // implements then(). + client.request({ + to: target.addonActor, + type: "reload" + }).then(() => {}, error => { + throw new Error( + "Error reloading addon " + target.addonID + ": " + error); + }); + }, + + render() { + let { target, debugDisabled } = this.props; + // Only temporarily installed add-ons can be reloaded. + const canBeReloaded = target.temporarilyInstalled; + + return dom.li({ className: "target-container" }, + dom.img({ + className: "target-icon", + role: "presentation", + src: target.icon + }), + dom.div({ className: "target" }, + dom.div({ className: "target-name", title: target.name }, target.name) + ), + dom.button({ + className: "debug-button", + onClick: this.debug, + disabled: debugDisabled, + }, Strings.GetStringFromName("debug")), + dom.button({ + className: "reload-button", + onClick: this.reload, + disabled: !canBeReloaded, + title: !canBeReloaded ? + Strings.GetStringFromName("reloadDisabledTooltip") : "" + }, Strings.GetStringFromName("reload")) + ); + } +}); diff --git a/devtools/client/aboutdebugging/components/moz.build b/devtools/client/aboutdebugging/components/moz.build new file mode 100644 index 000000000..829979dcc --- /dev/null +++ b/devtools/client/aboutdebugging/components/moz.build @@ -0,0 +1,17 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIRS += [ + 'addons', + 'tabs', + 'workers', +] + +DevToolsModules( + 'aboutdebugging.js', + 'panel-header.js', + 'panel-menu-entry.js', + 'panel-menu.js', + 'target-list.js', +) diff --git a/devtools/client/aboutdebugging/components/panel-header.js b/devtools/client/aboutdebugging/components/panel-header.js new file mode 100644 index 000000000..5629018f7 --- /dev/null +++ b/devtools/client/aboutdebugging/components/panel-header.js @@ -0,0 +1,24 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { createClass, DOM: dom, PropTypes } = + require("devtools/client/shared/vendor/react"); + +module.exports = createClass({ + displayName: "PanelHeader", + + propTypes: { + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired + }, + + render() { + let { name, id } = this.props; + + return dom.div({ className: "header" }, + dom.h1({ id, className: "header-name" }, name)); + }, +}); diff --git a/devtools/client/aboutdebugging/components/panel-menu-entry.js b/devtools/client/aboutdebugging/components/panel-menu-entry.js new file mode 100644 index 000000000..1af02d435 --- /dev/null +++ b/devtools/client/aboutdebugging/components/panel-menu-entry.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 { createClass, DOM: dom, PropTypes } = + require("devtools/client/shared/vendor/react"); + +module.exports = createClass({ + displayName: "PanelMenuEntry", + + propTypes: { + icon: PropTypes.string.isRequired, + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + selected: PropTypes.bool, + selectPanel: PropTypes.func.isRequired + }, + + onClick() { + this.props.selectPanel(this.props.id); + }, + + onKeyDown(event) { + if ([" ", "Enter"].includes(event.key)) { + this.props.selectPanel(this.props.id); + } + }, + + render() { + let { id, name, icon, selected } = this.props; + + // Here .category, .category-icon, .category-name classnames are used to + // apply common styles defined. + let className = "category" + (selected ? " selected" : ""); + return dom.div({ + "aria-selected": selected, + "aria-controls": id + "-panel", + className, + onClick: this.onClick, + onKeyDown: this.onKeyDown, + tabIndex: "0", + role: "tab" }, + dom.img({ className: "category-icon", src: icon, role: "presentation" }), + dom.div({ className: "category-name" }, name)); + } +}); diff --git a/devtools/client/aboutdebugging/components/panel-menu.js b/devtools/client/aboutdebugging/components/panel-menu.js new file mode 100644 index 000000000..b24493d78 --- /dev/null +++ b/devtools/client/aboutdebugging/components/panel-menu.js @@ -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/. */ + +"use strict"; + +const { createClass, createFactory, DOM: dom, PropTypes } = + require("devtools/client/shared/vendor/react"); +const PanelMenuEntry = createFactory(require("./panel-menu-entry")); + +module.exports = createClass({ + displayName: "PanelMenu", + + propTypes: { + panels: PropTypes.arrayOf(PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + icon: PropTypes.string.isRequired, + component: PropTypes.func.isRequired + })).isRequired, + selectPanel: PropTypes.func.isRequired, + selectedPanelId: PropTypes.string + }, + + render() { + let { panels, selectedPanelId, selectPanel } = this.props; + let panelLinks = panels.map(({ id, name, icon }) => { + let selected = id == selectedPanelId; + return PanelMenuEntry({ + id, + name, + icon, + selected, + selectPanel + }); + }); + + // "categories" id used for styling purposes + return dom.div({ id: "categories", role: "tablist" }, panelLinks); + }, +}); diff --git a/devtools/client/aboutdebugging/components/tabs/moz.build b/devtools/client/aboutdebugging/components/tabs/moz.build new file mode 100644 index 000000000..ee6a89e37 --- /dev/null +++ b/devtools/client/aboutdebugging/components/tabs/moz.build @@ -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/. + +DevToolsModules( + 'panel.js', + 'target.js', +) diff --git a/devtools/client/aboutdebugging/components/tabs/panel.js b/devtools/client/aboutdebugging/components/tabs/panel.js new file mode 100644 index 000000000..e280ce7f1 --- /dev/null +++ b/devtools/client/aboutdebugging/components/tabs/panel.js @@ -0,0 +1,98 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* eslint-env browser */ + +"use strict"; + +const { createClass, createFactory, DOM: dom, PropTypes } = + require("devtools/client/shared/vendor/react"); +const Services = require("Services"); + +const PanelHeader = createFactory(require("../panel-header")); +const TargetList = createFactory(require("../target-list")); +const TabTarget = createFactory(require("./target")); + +loader.lazyRequireGetter(this, "DebuggerClient", + "devtools/shared/client/main", true); + +const Strings = Services.strings.createBundle( + "chrome://devtools/locale/aboutdebugging.properties"); + +module.exports = createClass({ + displayName: "TabsPanel", + + propTypes: { + client: PropTypes.instanceOf(DebuggerClient).isRequired, + id: PropTypes.string.isRequired + }, + + getInitialState() { + return { + tabs: [] + }; + }, + + componentDidMount() { + let { client } = this.props; + client.addListener("tabListChanged", this.update); + this.update(); + }, + + componentWillUnmount() { + let { client } = this.props; + client.removeListener("tabListChanged", this.update); + }, + + update() { + this.props.client.mainRoot.listTabs().then(({ tabs }) => { + // Filter out closed tabs (represented as `null`). + tabs = tabs.filter(tab => !!tab); + tabs.forEach(tab => { + // FIXME Also try to fetch low-res favicon. But we should use actor + // support for this to get the high-res one (bug 1061654). + let url = new URL(tab.url); + if (url.protocol.startsWith("http")) { + let prePath = url.origin; + let idx = url.pathname.lastIndexOf("/"); + if (idx === -1) { + prePath += url.pathname; + } else { + prePath += url.pathname.substr(0, idx); + } + tab.icon = prePath + "/favicon.ico"; + } else { + tab.icon = "chrome://devtools/skin/images/globe.svg"; + } + }); + this.setState({ tabs }); + }); + }, + + render() { + let { client, id } = this.props; + let { tabs } = this.state; + + return dom.div({ + id: id + "-panel", + className: "panel", + role: "tabpanel", + "aria-labelledby": id + "-header" + }, + PanelHeader({ + id: id + "-header", + name: Strings.GetStringFromName("tabs") + }), + dom.div({}, + TargetList({ + client, + id: "tabs", + name: Strings.GetStringFromName("tabs"), + sort: false, + targetClass: TabTarget, + targets: tabs + }) + )); + } +}); diff --git a/devtools/client/aboutdebugging/components/tabs/target.js b/devtools/client/aboutdebugging/components/tabs/target.js new file mode 100644 index 000000000..d946f8f61 --- /dev/null +++ b/devtools/client/aboutdebugging/components/tabs/target.js @@ -0,0 +1,53 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* eslint-env browser */ + +"use strict"; + +const { createClass, DOM: dom, PropTypes } = + require("devtools/client/shared/vendor/react"); +const Services = require("Services"); + +const Strings = Services.strings.createBundle( + "chrome://devtools/locale/aboutdebugging.properties"); + +module.exports = createClass({ + displayName: "TabTarget", + + propTypes: { + target: PropTypes.shape({ + icon: PropTypes.string, + outerWindowID: PropTypes.number.isRequired, + title: PropTypes.string, + url: PropTypes.string.isRequired + }).isRequired + }, + + debug() { + let { target } = this.props; + window.open("about:devtools-toolbox?type=tab&id=" + target.outerWindowID); + }, + + render() { + let { target } = this.props; + + return dom.div({ className: "target-container" }, + dom.img({ + className: "target-icon", + role: "presentation", + src: target.icon + }), + dom.div({ className: "target" }, + // If the title is empty, display the url instead. + dom.div({ className: "target-name", title: target.url }, + target.title || target.url) + ), + dom.button({ + className: "debug-button", + onClick: this.debug, + }, Strings.GetStringFromName("debug")) + ); + } +}); diff --git a/devtools/client/aboutdebugging/components/target-list.js b/devtools/client/aboutdebugging/components/target-list.js new file mode 100644 index 000000000..e2d5669e7 --- /dev/null +++ b/devtools/client/aboutdebugging/components/target-list.js @@ -0,0 +1,56 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 { createClass, DOM: dom, PropTypes } = + require("devtools/client/shared/vendor/react"); +const Services = require("Services"); + +loader.lazyRequireGetter(this, "DebuggerClient", + "devtools/shared/client/main", true); + +const Strings = Services.strings.createBundle( + "chrome://devtools/locale/aboutdebugging.properties"); + +const LocaleCompare = (a, b) => { + return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); +}; + +module.exports = createClass({ + displayName: "TargetList", + + propTypes: { + client: PropTypes.instanceOf(DebuggerClient).isRequired, + debugDisabled: PropTypes.bool, + error: PropTypes.node, + id: PropTypes.string.isRequired, + name: PropTypes.string, + sort: PropTypes.bool, + targetClass: PropTypes.func.isRequired, + targets: PropTypes.arrayOf(PropTypes.object).isRequired + }, + + render() { + let { client, debugDisabled, error, targetClass, targets, sort } = this.props; + if (sort) { + targets = targets.sort(LocaleCompare); + } + targets = targets.map(target => { + return targetClass({ client, target, debugDisabled }); + }); + + let content = ""; + if (error) { + content = error; + } else if (targets.length > 0) { + content = dom.ul({ className: "target-list" }, targets); + } else { + content = dom.p(null, Strings.GetStringFromName("nothing")); + } + + return dom.div({ id: this.props.id, className: "targets" }, + dom.h2(null, this.props.name), content); + }, +}); diff --git a/devtools/client/aboutdebugging/components/workers/moz.build b/devtools/client/aboutdebugging/components/workers/moz.build new file mode 100644 index 000000000..ff33a5b28 --- /dev/null +++ b/devtools/client/aboutdebugging/components/workers/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( + 'panel.js', + 'service-worker-target.js', + 'target.js', +) diff --git a/devtools/client/aboutdebugging/components/workers/panel.js b/devtools/client/aboutdebugging/components/workers/panel.js new file mode 100644 index 000000000..b1bab2b99 --- /dev/null +++ b/devtools/client/aboutdebugging/components/workers/panel.js @@ -0,0 +1,193 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +/* globals window */ +"use strict"; + +loader.lazyImporter(this, "PrivateBrowsingUtils", + "resource://gre/modules/PrivateBrowsingUtils.jsm"); + +const { Ci } = require("chrome"); +const { createClass, createFactory, DOM: dom, PropTypes } = + require("devtools/client/shared/vendor/react"); +const { getWorkerForms } = require("../../modules/worker"); +const Services = require("Services"); + +const PanelHeader = createFactory(require("../panel-header")); +const TargetList = createFactory(require("../target-list")); +const WorkerTarget = createFactory(require("./target")); +const ServiceWorkerTarget = createFactory(require("./service-worker-target")); + +loader.lazyImporter(this, "PrivateBrowsingUtils", + "resource://gre/modules/PrivateBrowsingUtils.jsm"); + +loader.lazyRequireGetter(this, "DebuggerClient", + "devtools/shared/client/main", true); + +const Strings = Services.strings.createBundle( + "chrome://devtools/locale/aboutdebugging.properties"); + +const WorkerIcon = "chrome://devtools/skin/images/debugging-workers.svg"; +const MORE_INFO_URL = "https://developer.mozilla.org/en-US/docs/Tools/about%3Adebugging"; + +module.exports = createClass({ + displayName: "WorkersPanel", + + propTypes: { + client: PropTypes.instanceOf(DebuggerClient).isRequired, + id: PropTypes.string.isRequired + }, + + getInitialState() { + return { + workers: { + service: [], + shared: [], + other: [] + } + }; + }, + + componentDidMount() { + let client = this.props.client; + client.addListener("workerListChanged", this.update); + client.addListener("serviceWorkerRegistrationListChanged", this.update); + client.addListener("processListChanged", this.update); + client.addListener("registration-changed", this.update); + + this.update(); + }, + + componentWillUnmount() { + let client = this.props.client; + client.removeListener("processListChanged", this.update); + client.removeListener("serviceWorkerRegistrationListChanged", this.update); + client.removeListener("workerListChanged", this.update); + client.removeListener("registration-changed", this.update); + }, + + update() { + let workers = this.getInitialState().workers; + + getWorkerForms(this.props.client).then(forms => { + forms.registrations.forEach(form => { + workers.service.push({ + icon: WorkerIcon, + name: form.url, + url: form.url, + scope: form.scope, + registrationActor: form.actor, + active: form.active + }); + }); + + forms.workers.forEach(form => { + let worker = { + icon: WorkerIcon, + name: form.url, + url: form.url, + workerActor: form.actor + }; + switch (form.type) { + case Ci.nsIWorkerDebugger.TYPE_SERVICE: + let registration = this.getRegistrationForWorker(form, workers.service); + if (registration) { + // XXX: Race, sometimes a ServiceWorkerRegistrationInfo doesn't + // have a scriptSpec, but its associated WorkerDebugger does. + if (!registration.url) { + registration.name = registration.url = form.url; + } + registration.workerActor = form.actor; + } else { + // If a service worker registration could not be found, this means we are in + // e10s, and registrations are not forwarded to other processes until they + // reach the activated state. Augment the worker as a registration worker to + // display it in aboutdebugging. + worker.scope = form.scope; + worker.active = false; + workers.service.push(worker); + } + break; + case Ci.nsIWorkerDebugger.TYPE_SHARED: + workers.shared.push(worker); + break; + default: + workers.other.push(worker); + } + }); + + // XXX: Filter out the service worker registrations for which we couldn't + // find the scriptSpec. + workers.service = workers.service.filter(reg => !!reg.url); + + this.setState({ workers }); + }); + }, + + getRegistrationForWorker(form, registrations) { + for (let registration of registrations) { + if (registration.scope === form.scope) { + return registration; + } + } + return null; + }, + + render() { + let { client, id } = this.props; + let { workers } = this.state; + + let isWindowPrivate = PrivateBrowsingUtils.isContentWindowPrivate(window); + let isPrivateBrowsingMode = PrivateBrowsingUtils.permanentPrivateBrowsing; + let isServiceWorkerDisabled = !Services.prefs + .getBoolPref("dom.serviceWorkers.enabled"); + let errorMsg = isWindowPrivate || isPrivateBrowsingMode || + isServiceWorkerDisabled ? + dom.p({ className: "service-worker-disabled" }, + dom.div({ className: "warning" }), + Strings.GetStringFromName("configurationIsNotCompatible"), + " (", + dom.a({ href: MORE_INFO_URL, target: "_blank" }, + Strings.GetStringFromName("moreInfo")), + ")" + ) : ""; + + return dom.div({ + id: id + "-panel", + className: "panel", + role: "tabpanel", + "aria-labelledby": id + "-header" + }, + PanelHeader({ + id: id + "-header", + name: Strings.GetStringFromName("workers") + }), + dom.div({ id: "workers", className: "inverted-icons" }, + TargetList({ + client, + error: errorMsg, + id: "service-workers", + name: Strings.GetStringFromName("serviceWorkers"), + sort: true, + targetClass: ServiceWorkerTarget, + targets: workers.service + }), + TargetList({ + client, + id: "shared-workers", + name: Strings.GetStringFromName("sharedWorkers"), + sort: true, + targetClass: WorkerTarget, + targets: workers.shared + }), + TargetList({ + client, + id: "other-workers", + name: Strings.GetStringFromName("otherWorkers"), + sort: true, + targetClass: WorkerTarget, + targets: workers.other + }) + )); + } +}); diff --git a/devtools/client/aboutdebugging/components/workers/service-worker-target.js b/devtools/client/aboutdebugging/components/workers/service-worker-target.js new file mode 100644 index 000000000..d46f6f20f --- /dev/null +++ b/devtools/client/aboutdebugging/components/workers/service-worker-target.js @@ -0,0 +1,231 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* eslint-env browser */ + +"use strict"; + +const { createClass, DOM: dom, PropTypes } = + require("devtools/client/shared/vendor/react"); +const { debugWorker } = require("../../modules/worker"); +const Services = require("Services"); + +loader.lazyRequireGetter(this, "DebuggerClient", + "devtools/shared/client/main", true); + +const Strings = Services.strings.createBundle( + "chrome://devtools/locale/aboutdebugging.properties"); + +module.exports = createClass({ + displayName: "ServiceWorkerTarget", + + propTypes: { + client: PropTypes.instanceOf(DebuggerClient).isRequired, + debugDisabled: PropTypes.bool, + target: PropTypes.shape({ + active: PropTypes.bool, + icon: PropTypes.string, + name: PropTypes.string.isRequired, + url: PropTypes.string, + scope: PropTypes.string.isRequired, + // registrationActor can be missing in e10s. + registrationActor: PropTypes.string, + workerActor: PropTypes.string + }).isRequired + }, + + getInitialState() { + return { + pushSubscription: null + }; + }, + + componentDidMount() { + let { client } = this.props; + client.addListener("push-subscription-modified", this.onPushSubscriptionModified); + this.updatePushSubscription(); + }, + + componentDidUpdate(oldProps, oldState) { + let wasActive = oldProps.target.active; + if (!wasActive && this.isActive()) { + // While the service worker isn't active, any calls to `updatePushSubscription` + // won't succeed. If we just became active, make sure we didn't miss a push + // subscription change by updating it now. + this.updatePushSubscription(); + } + }, + + componentWillUnmount() { + let { client } = this.props; + client.removeListener("push-subscription-modified", this.onPushSubscriptionModified); + }, + + debug() { + if (!this.isRunning()) { + // If the worker is not running, we can't debug it. + return; + } + + let { client, target } = this.props; + debugWorker(client, target.workerActor); + }, + + push() { + if (!this.isActive() || !this.isRunning()) { + // If the worker is not running, we can't push to it. + // If the worker is not active, the registration might be unavailable and the + // push will not succeed. + return; + } + + let { client, target } = this.props; + client.request({ + to: target.workerActor, + type: "push" + }); + }, + + start() { + if (!this.isActive() || this.isRunning()) { + // If the worker is not active or if it is already running, we can't start it. + return; + } + + let { client, target } = this.props; + client.request({ + to: target.registrationActor, + type: "start" + }); + }, + + unregister() { + let { client, target } = this.props; + client.request({ + to: target.registrationActor, + type: "unregister" + }); + }, + + onPushSubscriptionModified(type, data) { + let { target } = this.props; + if (data.from === target.registrationActor) { + this.updatePushSubscription(); + } + }, + + updatePushSubscription() { + if (!this.props.target.registrationActor) { + // A valid registrationActor is needed to retrieve the push subscription. + return; + } + + let { client, target } = this.props; + client.request({ + to: target.registrationActor, + type: "getPushSubscription" + }, ({ subscription }) => { + this.setState({ pushSubscription: subscription }); + }); + }, + + isRunning() { + // We know the target is running if it has a worker actor. + return !!this.props.target.workerActor; + }, + + isActive() { + return this.props.target.active; + }, + + getServiceWorkerStatus() { + if (this.isActive() && this.isRunning()) { + return "running"; + } else if (this.isActive()) { + return "stopped"; + } + // We cannot get service worker registrations unless the registration is in + // ACTIVE state. Unable to know the actual state ("installing", "waiting"), we + // display a custom state "registering" for now. See Bug 1153292. + return "registering"; + }, + + renderButtons() { + let pushButton = dom.button({ + className: "push-button", + onClick: this.push + }, Strings.GetStringFromName("push")); + + let debugButton = dom.button({ + className: "debug-button", + onClick: this.debug, + disabled: this.props.debugDisabled + }, Strings.GetStringFromName("debug")); + + let startButton = dom.button({ + className: "start-button", + onClick: this.start, + }, Strings.GetStringFromName("start")); + + if (this.isRunning()) { + if (this.isActive()) { + return [pushButton, debugButton]; + } + // Only debug button is available if the service worker is not active. + return debugButton; + } + return startButton; + }, + + renderUnregisterLink() { + if (!this.isActive()) { + // If not active, there might be no registrationActor available. + return null; + } + + return dom.a({ + onClick: this.unregister, + className: "unregister-link" + }, Strings.GetStringFromName("unregister")); + }, + + render() { + let { target } = this.props; + let { pushSubscription } = this.state; + let status = this.getServiceWorkerStatus(); + + return dom.div({ className: "target-container" }, + dom.img({ + className: "target-icon", + role: "presentation", + src: target.icon + }), + dom.span({ className: `target-status target-status-${status}` }, + Strings.GetStringFromName(status)), + dom.div({ className: "target" }, + dom.div({ className: "target-name", title: target.name }, target.name), + dom.ul({ className: "target-details" }, + (pushSubscription ? + dom.li({ className: "target-detail" }, + dom.strong(null, Strings.GetStringFromName("pushService")), + dom.span({ + className: "service-worker-push-url", + title: pushSubscription.endpoint + }, pushSubscription.endpoint)) : + null + ), + dom.li({ className: "target-detail" }, + dom.strong(null, Strings.GetStringFromName("scope")), + dom.span({ + className: "service-worker-scope", + title: target.scope + }, target.scope), + this.renderUnregisterLink() + ) + ) + ), + this.renderButtons() + ); + } +}); diff --git a/devtools/client/aboutdebugging/components/workers/target.js b/devtools/client/aboutdebugging/components/workers/target.js new file mode 100644 index 000000000..c1f6420ac --- /dev/null +++ b/devtools/client/aboutdebugging/components/workers/target.js @@ -0,0 +1,57 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* eslint-env browser */ + +"use strict"; + +const { createClass, DOM: dom, PropTypes } = + require("devtools/client/shared/vendor/react"); +const { debugWorker } = require("../../modules/worker"); +const Services = require("Services"); + +loader.lazyRequireGetter(this, "DebuggerClient", + "devtools/shared/client/main", true); + +const Strings = Services.strings.createBundle( + "chrome://devtools/locale/aboutdebugging.properties"); + +module.exports = createClass({ + displayName: "WorkerTarget", + + propTypes: { + client: PropTypes.instanceOf(DebuggerClient).isRequired, + debugDisabled: PropTypes.bool, + target: PropTypes.shape({ + icon: PropTypes.string, + name: PropTypes.string.isRequired, + workerActor: PropTypes.string + }).isRequired + }, + + debug() { + let { client, target } = this.props; + debugWorker(client, target.workerActor); + }, + + render() { + let { target, debugDisabled } = this.props; + + return dom.li({ className: "target-container" }, + dom.img({ + className: "target-icon", + role: "presentation", + src: target.icon + }), + dom.div({ className: "target" }, + dom.div({ className: "target-name", title: target.name }, target.name) + ), + dom.button({ + className: "debug-button", + onClick: this.debug, + disabled: debugDisabled + }, Strings.GetStringFromName("debug")) + ); + } +}); |