summaryrefslogtreecommitdiffstats
path: root/devtools/client/aboutdebugging/components
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/aboutdebugging/components')
-rw-r--r--devtools/client/aboutdebugging/components/aboutdebugging.js111
-rw-r--r--devtools/client/aboutdebugging/components/addons/controls.js97
-rw-r--r--devtools/client/aboutdebugging/components/addons/install-error.js26
-rw-r--r--devtools/client/aboutdebugging/components/addons/moz.build10
-rw-r--r--devtools/client/aboutdebugging/components/addons/panel.js146
-rw-r--r--devtools/client/aboutdebugging/components/addons/target.js84
-rw-r--r--devtools/client/aboutdebugging/components/moz.build17
-rw-r--r--devtools/client/aboutdebugging/components/panel-header.js24
-rw-r--r--devtools/client/aboutdebugging/components/panel-menu-entry.js48
-rw-r--r--devtools/client/aboutdebugging/components/panel-menu.js41
-rw-r--r--devtools/client/aboutdebugging/components/tabs/moz.build8
-rw-r--r--devtools/client/aboutdebugging/components/tabs/panel.js98
-rw-r--r--devtools/client/aboutdebugging/components/tabs/target.js53
-rw-r--r--devtools/client/aboutdebugging/components/target-list.js56
-rw-r--r--devtools/client/aboutdebugging/components/workers/moz.build9
-rw-r--r--devtools/client/aboutdebugging/components/workers/panel.js193
-rw-r--r--devtools/client/aboutdebugging/components/workers/service-worker-target.js231
-rw-r--r--devtools/client/aboutdebugging/components/workers/target.js57
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"))
+ );
+ }
+});