new file mode 100644
--- /dev/null
+++ b/devtools/client/application/application.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/. */
+
+@import "resource://devtools/client/application/src/components/App.css";
+@import "resource://devtools/client/application/src/components/Worker.css";
+@import "resource://devtools/client/application/src/components/WorkerList.css";
+
+* {
+ box-sizing: border-box;
+}
+
+html,
+body,
+#mount {
+ height: 100%;
+}
+
+ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
\ No newline at end of file
--- a/devtools/client/application/index.html
+++ b/devtools/client/application/index.html
@@ -1,13 +1,14 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<!DOCTYPE html>
<html dir="">
<head>
+ <link rel="stylesheet" type="text/css" href="resource://devtools/client/application/application.css" />
</head>
<body class="theme-body" role="application">
<div id="mount"></div>
<script src="chrome://devtools/content/shared/theme-switching.js"></script>
<script src="initializer.js"></script>
</body>
</html>
--- a/devtools/client/application/initializer.js
+++ b/devtools/client/application/initializer.js
@@ -1,36 +1,74 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.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 { BrowserLoader } = ChromeUtils.import("resource://devtools/client/shared/browser-loader.js", {});
-const require = window.windowRequire = BrowserLoader({
+const require = BrowserLoader({
baseURI: "resource://devtools/client/application/",
window,
}).require;
const { createFactory } = require("devtools/client/shared/vendor/react");
const { render, unmountComponentAtNode } = require("devtools/client/shared/vendor/react-dom");
+const Provider = createFactory(require("devtools/client/shared/vendor/react-redux").Provider);
+const { bindActionCreators } = require("devtools/client/shared/vendor/redux");
+
+const { configureStore } = require("./src/create-store");
+const actions = require("./src/actions/index");
const App = createFactory(require("./src/components/App"));
/**
* Global Application object in this panel. This object is expected by panel.js and is
* called to start the UI for the panel.
*/
window.Application = {
- bootstrap({ toolbox, panel }) {
+ async bootstrap({ toolbox, panel }) {
+ this.updateWorkers = this.updateWorkers.bind(this);
+
this.mount = document.querySelector("#mount");
+ this.toolbox = toolbox;
+ this.client = toolbox.target.client;
+
+ this.store = configureStore();
+ this.actions = bindActionCreators(actions, this.store.dispatch);
+
+ const serviceContainer = {
+ openAboutDebugging() {
+ let win = toolbox.doc.defaultView.top;
+ win.openUILinkIn("about:debugging#workers", "tab", { relatedToCurrent: true });
+ }
+ };
// Render the root Application component.
- const app = App();
+ const app = App({ client: this.client, serviceContainer });
+ render(Provider({ store: this.store }, app), this.mount);
- render(app, this.mount);
+ this.client.addListener("workerListChanged", this.updateWorkers);
+ this.client.addListener("serviceWorkerRegistrationListChanged", this.updateWorkers);
+ this.client.addListener("registration-changed", this.updateWorkers);
+ this.client.addListener("processListChanged", this.updateWorkers);
+
+ await this.updateWorkers();
+ },
+
+ async updateWorkers() {
+ let { service } = await this.client.mainRoot.listAllWorkers();
+ this.actions.updateWorkers(service);
},
destroy() {
+ this.client.removeListener("workerListChanged", this.updateWorkers);
+ this.client.removeListener("serviceWorkerRegistrationListChanged",
+ this.updateWorkers);
+ this.client.removeListener("registration-changed", this.updateWorkers);
+ this.client.removeListener("processListChanged", this.updateWorkers);
+
unmountComponentAtNode(this.mount);
this.mount = null;
+ this.toolbox = null;
+ this.client = null;
},
};
--- a/devtools/client/application/moz.build
+++ b/devtools/client/application/moz.build
@@ -2,10 +2,11 @@
# License, v. 2.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 += [
'src',
]
DevToolsModules(
+ 'application.css',
'panel.js'
)
--- a/devtools/client/application/panel.js
+++ b/devtools/client/application/panel.js
@@ -22,17 +22,17 @@ class ApplicationPanel {
this.toolbox = toolbox;
}
async open() {
if (!this.toolbox.target.isRemote) {
await this.toolbox.target.makeRemote();
}
- this.panelWin.Application.bootstrap({
+ await this.panelWin.Application.bootstrap({
toolbox: this.toolbox,
panel: this,
});
this.emit("ready");
this.isReady = true;
return this;
}
new file mode 100644
--- /dev/null
+++ b/devtools/client/application/src/actions/index.js
@@ -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/. */
+
+"use strict";
+
+const workers = require("./workers");
+
+Object.assign(exports,
+ workers,
+);
new file mode 100644
--- /dev/null
+++ b/devtools/client/application/src/actions/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(
+ 'index.js',
+ 'workers.js',
+)
new file mode 100644
--- /dev/null
+++ b/devtools/client/application/src/actions/workers.js
@@ -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/. */
+
+"use strict";
+
+const {
+ UPDATE_WORKERS,
+} = require("../constants");
+
+function updateWorkers(workers) {
+ return {
+ type: UPDATE_WORKERS,
+ workers
+ };
+}
+
+module.exports = {
+ updateWorkers,
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/application/src/components/App.css
@@ -0,0 +1,44 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 the application panel is
+ *
+ * +---------------------------------------------+
+ * | (header) "Service workers" |
+ * +---------------------------------------------+
+ * | Service worker 1 |
+ * | (...) |
+ * | Service worker N (see Worker.css) |
+ * +---------------------------------------------+
+ * | Link to about:debugging |
+ * +---------------------------------------------+
+ */
+.application {
+ height: 100%;
+ padding: 0 0 0 20px;
+ overflow: auto;
+ display: flex;
+ flex-direction: column;
+}
+
+h1 {
+ font-size: 22px;
+ font-weight: normal;
+}
+
+a,
+a:hover,
+a:visited {
+ color: var(--blue-60) !important;
+ margin: 0 10px;
+ cursor: pointer;
+}
+
+a.disabled,
+a.disabled:hover,
+a.disabled:visited {
+ color: var(--grey-30) !important;
+ cursor: default;
+}
--- a/devtools/client/application/src/components/App.js
+++ b/devtools/client/application/src/components/App.js
@@ -1,18 +1,38 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.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 { Component } = require("devtools/client/shared/vendor/react");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const { createFactory, Component } = require("devtools/client/shared/vendor/react");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
const { div } = require("devtools/client/shared/vendor/react-dom-factories");
+const WorkerList = createFactory(require("./WorkerList"));
+
+/**
+ * This is the main component for the application panel.
+ */
class App extends Component {
+ static get propTypes() {
+ return {
+ client: PropTypes.object.isRequired,
+ workers: PropTypes.object.isRequired,
+ serviceContainer: PropTypes.object.isRequired,
+ };
+ }
+
render() {
- return (
- div({className: "application"}, "application panel content")
- );
+ let { workers, client, serviceContainer } = this.props;
+
+ return div({className: "application"},
+ WorkerList({ workers, client, serviceContainer }));
}
}
-module.exports = App;
+// Exports
+
+module.exports = connect(
+ (state) => ({ workers: state.workers.list }),
+)(App);
new file mode 100644
--- /dev/null
+++ b/devtools/client/application/src/components/Worker.css
@@ -0,0 +1,47 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 a service worker item is
+ *
+ * +----------------------------+----------------+
+ * | Service worker scope | Unregister btn |
+ * +---+----------+-------------+----------------|
+ * | | "Source" | script name | debug link |
+ * | |----------+-------------+----------------|
+ * | | "Status" | status | start link |
+ * +---+----------+-------------+----------------|
+ */
+.service-worker-container {
+ margin-bottom: 20px;
+ width: 100%;
+ max-width: 600px;
+ position: relative;
+ line-height: 1.5;
+ font-size: 13px;
+}
+
+.service-worker-container > .service-worker-scope {
+ padding-inline-start: 30px;
+}
+
+.service-worker-container > :not(.service-worker-scope) {
+ padding-inline-start: 70px;
+}
+
+.service-worker-scope {
+ font-weight: bold;
+}
+
+.service-worker-meta-name {
+ color: var(--grey-50);
+ min-width: 50px;
+ margin-inline-end: 10px;
+ display: inline-block;
+}
+
+.unregister-button {
+ position: absolute;
+ right: 0;
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/application/src/components/Worker.js
@@ -0,0 +1,157 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 { Component } = require("devtools/client/shared/vendor/react");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const { a, button, div, li, span } = require("devtools/client/shared/vendor/react-dom-factories");
+const Services = require("Services");
+
+loader.lazyRequireGetter(this, "DebuggerClient",
+ "devtools/shared/client/debugger-client", true);
+loader.lazyRequireGetter(this, "gDevToolsBrowser",
+ "devtools/client/framework/devtools-browser", true);
+
+const Strings = Services.strings.createBundle(
+ "chrome://devtools/locale/aboutdebugging.properties");
+
+/**
+ * This component is dedicated to display a worker, more accurately a service worker, in
+ * the list of workers displayed in the application panel. It displays information about
+ * the worker as well as action links and buttons to interact with the worker (e.g. debug,
+ * unregister, update etc...).
+ */
+class Worker extends Component {
+ static get propTypes() {
+ return {
+ client: PropTypes.instanceOf(DebuggerClient).isRequired,
+ debugDisabled: PropTypes.bool,
+ worker: PropTypes.shape({
+ active: PropTypes.bool,
+ name: PropTypes.string.isRequired,
+ scope: PropTypes.string.isRequired,
+ // registrationActor can be missing in e10s.
+ registrationActor: PropTypes.string,
+ workerActor: PropTypes.string
+ }).isRequired
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.debug = this.debug.bind(this);
+ this.start = this.start.bind(this);
+ this.unregister = this.unregister.bind(this);
+ }
+
+ debug() {
+ if (!this.isRunning()) {
+ console.log("Service workers cannot be debugged if they are not running");
+ return;
+ }
+
+ let { client, worker } = this.props;
+ gDevToolsBrowser.openWorkerToolbox(client, worker.workerActor);
+ }
+
+ start() {
+ if (!this.isActive() || this.isRunning()) {
+ console.log("Running or inactive service workers cannot be started");
+ return;
+ }
+
+ let { client, worker } = this.props;
+ client.request({
+ to: worker.registrationActor,
+ type: "start"
+ });
+ }
+
+ unregister() {
+ let { client, worker } = this.props;
+ client.request({
+ to: worker.registrationActor,
+ type: "unregister"
+ });
+ }
+
+ isRunning() {
+ // We know the worker is running if it has a worker actor.
+ return !!this.props.worker.workerActor;
+ }
+
+ isActive() {
+ return this.props.worker.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";
+ }
+
+ formatScope(scope) {
+ let [, remainder] = scope.split("://");
+ return remainder || scope;
+ }
+
+ formatSource(source) {
+ let parts = source.split("/");
+ return parts[parts.length - 1];
+ }
+
+ render() {
+ let { worker } = this.props;
+ let status = this.getServiceWorkerStatus();
+
+ const unregisterButton = this.isActive() ?
+ button({
+ onClick: this.unregister,
+ className: "devtools-button unregister-button",
+ "data-standalone": true
+ },
+ Strings.GetStringFromName("unregister"))
+ : null;
+
+ const debugLinkDisabled = this.isRunning() ? "" : "disabled";
+ const debugLink = a({
+ onClick: this.isRunning() ? this.debug : null,
+ title: this.isRunning() ? null : "Only running service workers can be debugged",
+ className: `${debugLinkDisabled} debug-link`
+ },
+ Strings.GetStringFromName("debug"));
+
+ const startLink = !this.isRunning() ?
+ a({ onClick: this.start, className: "start-link" },
+ Strings.GetStringFromName("start"))
+ : null;
+
+ return li({ className: "service-worker-container" },
+ div(
+ { className: "service-worker-scope" },
+ span({ title: worker.scope }, this.formatScope(worker.scope)),
+ unregisterButton),
+ div(
+ { className: "service-worker-source" },
+ span({ className: "service-worker-meta-name" }, "Source"),
+ span({ title: worker.scope }, this.formatSource(worker.url)),
+ debugLink),
+ div(
+ { className: `service-worker-status service-worker-status-${status}` },
+ span({ className: "service-worker-meta-name" }, "Status"),
+ Strings.GetStringFromName(status).toLowerCase(),
+ startLink)
+ );
+ }
+}
+
+module.exports = Worker;
new file mode 100644
--- /dev/null
+++ b/devtools/client/application/src/components/WorkerList.css
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+.application-aboutdebugging-plug {
+ text-align: right;
+ padding: 5px 0;
+}
+
+.application-workers-container {
+ flex-grow: 1;
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/application/src/components/WorkerList.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 PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const { createFactory, Component } = require("devtools/client/shared/vendor/react");
+const { a, div, h1, ul, li } = require("devtools/client/shared/vendor/react-dom-factories");
+const Worker = createFactory(require("./Worker"));
+
+/**
+ * This component handles the list of service workers displayed in the application panel
+ * and also displays a suggestion to use about debugging for debugging other service
+ * workers.
+ */
+class WorkerList extends Component {
+ static get propTypes() {
+ return {
+ client: PropTypes.object.isRequired,
+ workers: PropTypes.object.isRequired,
+ serviceContainer: PropTypes.object.isRequired,
+ };
+ }
+
+ render() {
+ const { workers, client, serviceContainer } = this.props;
+ const { openAboutDebugging } = serviceContainer;
+
+ return [
+ ul({ className: "application-workers-container" },
+ li({},
+ h1({ className: "application-title" }, "Service Workers")
+ ),
+ workers.map(worker => Worker({
+ client,
+ debugDisabled: false,
+ worker,
+ }))
+ ),
+ div({ className: "application-aboutdebugging-plug" },
+ "See about:debugging for Service Workers from other domains",
+ a({ onClick: () => openAboutDebugging() }, "Open about:debugging")
+ )
+ ];
+ }
+}
+
+// Exports
+
+module.exports = WorkerList;
--- a/devtools/client/application/src/components/moz.build
+++ b/devtools/client/application/src/components/moz.build
@@ -1,7 +1,12 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
DevToolsModules(
+ 'App.css',
'App.js',
+ 'Worker.css',
+ 'Worker.js',
+ 'WorkerList.css',
+ 'WorkerList.js',
)
new file mode 100644
--- /dev/null
+++ b/devtools/client/application/src/constants.js
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const actionTypes = {
+ UPDATE_WORKERS: "UPDATE_WORKERS",
+};
+
+// flatten constants
+module.exports = Object.assign({}, actionTypes);
new file mode 100644
--- /dev/null
+++ b/devtools/client/application/src/create-store.js
@@ -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/. */
+
+"use strict";
+
+const { createStore } = require("devtools/client/shared/vendor/redux");
+
+// Reducers
+const rootReducer = require("./reducers/index");
+const { WorkersState } = require("./reducers/workers-state");
+
+function configureStore() {
+ // Prepare initial state.
+ const initialState = {
+ workers: new WorkersState(),
+ };
+
+ return createStore(rootReducer, initialState);
+}
+
+exports.configureStore = configureStore;
--- a/devtools/client/application/src/moz.build
+++ b/devtools/client/application/src/moz.build
@@ -1,7 +1,14 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
DIRS += [
+ 'actions',
'components',
-]
\ No newline at end of file
+ 'reducers',
+]
+
+DevToolsModules(
+ 'constants.js',
+ 'create-store.js',
+)
new file mode 100644
--- /dev/null
+++ b/devtools/client/application/src/reducers/index.js
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { combineReducers } = require("devtools/client/shared/vendor/redux");
+const { workersReducer } = require("./workers-state");
+
+module.exports = combineReducers({
+ workers: workersReducer,
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/application/src/reducers/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(
+ 'index.js',
+ 'workers-state.js',
+)
new file mode 100644
--- /dev/null
+++ b/devtools/client/application/src/reducers/workers-state.js
@@ -0,0 +1,33 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 {
+ UPDATE_WORKERS,
+} = require("../constants");
+
+function WorkersState() {
+ return {
+ // Array of all service workers
+ list: [],
+ };
+}
+
+function workersReducer(state = WorkersState(), action) {
+ switch (action.type) {
+ case UPDATE_WORKERS: {
+ let { workers } = action;
+ return { list: workers };
+ }
+
+ default:
+ return state;
+ }
+}
+
+module.exports = {
+ WorkersState,
+ workersReducer,
+};