Bug 1436665 - Expose Net panel API without the UI; r=ochameau draft
authorJan Odvarko <odvarko@gmail.com>
Tue, 17 Apr 2018 14:09:34 +0200
changeset 783593 a12867ede1928ec2592d1516cdf0b95991850413
parent 783258 0ceabd10aac2272e83850e278c7876f32dbae42e
child 783594 d5ac5c6edd0aa8d247a0bc5b114a4e0466217114
push id106728
push userjodvarko@mozilla.com
push dateTue, 17 Apr 2018 13:08:07 +0000
reviewersochameau
bugs1436665
milestone61.0a1
Bug 1436665 - Expose Net panel API without the UI; r=ochameau MozReview-Commit-ID: 31ceGL3zWzl
devtools/client/framework/toolbox.js
devtools/client/netmonitor/initializer.js
devtools/client/netmonitor/panel.js
devtools/client/netmonitor/src/api.js
devtools/client/netmonitor/src/app.js
devtools/client/netmonitor/src/connector/chrome-connector.js
devtools/client/netmonitor/src/connector/firefox-connector.js
devtools/client/netmonitor/src/connector/firefox-data-provider.js
devtools/client/netmonitor/src/connector/index.js
devtools/client/netmonitor/src/har/har-exporter.js
devtools/client/netmonitor/src/moz.build
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -69,16 +69,18 @@ loader.lazyRequireGetter(this, "HUDServi
 loader.lazyRequireGetter(this, "viewSource",
   "devtools/client/shared/view-source");
 loader.lazyRequireGetter(this, "StyleSheetsFront",
   "devtools/shared/fronts/stylesheets", true);
 loader.lazyRequireGetter(this, "buildHarLog",
   "devtools/client/netmonitor/src/har/har-builder-utils", true);
 loader.lazyRequireGetter(this, "getKnownDeviceFront",
   "devtools/shared/fronts/device", true);
+loader.lazyRequireGetter(this, "NetMonitorAPI",
+  "devtools/client/netmonitor/src/api", true);
 
 loader.lazyGetter(this, "domNodeConstants", () => {
   return require("devtools/shared/dom-node-constants");
 });
 
 loader.lazyGetter(this, "registerHarOverlay", () => {
   return require("devtools/client/netmonitor/src/har/toolbox-overlay").register;
 });
@@ -111,24 +113,22 @@ function Toolbox(target, selectedTool, h
 
   this._toolPanels = new Map();
   this._inspectorExtensionSidebars = new Map();
   this._telemetry = new Telemetry();
 
   this._initInspector = null;
   this._inspector = null;
   this._styleSheets = null;
+  this._netMonitorAPI = null;
 
   // Map of frames (id => frame-info) and currently selected frame id.
   this.frameMap = new Map();
   this.selectedFrameId = null;
 
-  // List of listeners for `devtools.network.onRequestFinished` WebExt API
-  this._requestFinishedListeners = new Set();
-
   this._toolRegistered = this._toolRegistered.bind(this);
   this._toolUnregistered = this._toolUnregistered.bind(this);
   this._onWillNavigate = this._onWillNavigate.bind(this);
   this._refreshHostTitle = this._refreshHostTitle.bind(this);
   this.toggleNoAutohide = this.toggleNoAutohide.bind(this);
   this.showFramesMenu = this.showFramesMenu.bind(this);
   this.handleKeyDownOnFramesButton = this.handleKeyDownOnFramesButton.bind(this);
   this.showFramesMenuOnKeyDown = this.showFramesMenuOnKeyDown.bind(this);
@@ -2668,17 +2668,16 @@ Toolbox.prototype = {
                                   this._applyServiceWorkersTestingSettings);
 
     this._lastFocusedElement = null;
 
     if (this._sourceMapURLService) {
       this._sourceMapURLService.destroy();
       this._sourceMapURLService = null;
     }
-
     if (this._sourceMapService) {
       this._sourceMapService.stopSourceMapWorker();
       this._sourceMapService = null;
     }
 
     if (this.webconsolePanel) {
       this._saveSplitConsoleHeight();
       this.webconsolePanel.removeEventListener("resize",
@@ -2767,16 +2766,21 @@ Toolbox.prototype = {
     this._telemetry.destroy();
 
     // Finish all outstanding tasks (which means finish destroying panels and
     // then destroying the host, successfully or not) before destroying the
     // target.
     deferred.resolve(settleAll(outstanding)
         .catch(console.error)
         .then(() => {
+          let api = this._netMonitorAPI;
+          this._netMonitorAPI = null;
+          return api ? api.destroy() : null;
+        }, console.error)
+        .then(() => {
           this._removeHostListeners();
 
           // `location` may already be 'invalid' if the toolbox document is
           // already in process of destruction. Otherwise if it is still
           // around, ensure releasing toolbox document and triggering cleanup
           // thanks to unload event. We do that precisely here, before
           // nullifying the target as various cleanup code depends on the
           // target attribute to be still
@@ -3026,84 +3030,95 @@ Toolbox.prototype = {
    */
   viewSource: function(sourceURL, sourceLine) {
     return viewSource.viewSource(this, sourceURL, sourceLine);
   },
 
   // Support for WebExtensions API (`devtools.network.*`)
 
   /**
+   * Return Netmonitor API object. This object offers Network monitor
+   * public API that can be consumed by other panels or WE API.
+   */
+  getNetMonitorAPI: async function() {
+    let netPanel = this.getPanel("netmonitor");
+
+    // Return Net panel if it exists.
+    if (netPanel) {
+      return netPanel.panelWin.Netmonitor.api;
+    }
+
+    if (this._netMonitorAPI) {
+      return this._netMonitorAPI;
+    }
+
+    // Create and initialize Network monitor API object.
+    // This object is only connected to the backend - not to the UI.
+    this._netMonitorAPI = new NetMonitorAPI();
+    await this._netMonitorAPI.connect(this);
+
+    return this._netMonitorAPI;
+  },
+
+  /**
    * Returns data (HAR) collected by the Network panel.
    */
   getHARFromNetMonitor: async function() {
-    let netPanel = this.getPanel("netmonitor");
-
-    // The panel doesn't have to exist (it must be selected
-    // by the user at least once to be created).
-    // Return default empty HAR log in such case.
-    if (!netPanel) {
-      let har = await buildHarLog(Services.appinfo);
-      return har.log;
-    }
-
-    // Use Netmonitor object to get the current HAR log.
-    let har = await netPanel.panelWin.Netmonitor.getHar();
+    let netMonitor = await this.getNetMonitorAPI();
+    let har = await netMonitor.getHar();
+
+    // Return default empty HAR file if needed.
+    har = har || buildHarLog(Services.appinfo);
 
     // Return the log directly to be compatible with
     // Chrome WebExtension API.
     return har.log;
   },
 
   /**
    * Add listener for `onRequestFinished` events.
    *
    * @param {Object} listener
    *        The listener to be called it's expected to be
    *        a function that takes ({harEntry, requestId})
    *        as first argument.
    */
-  addRequestFinishedListener: function(listener) {
-    // Log console message informing the extension developer
-    // that the Network panel needs to be selected at least
-    // once in order to receive `onRequestFinished` events.
-    let message = "The Network panel needs to be selected at least" +
-      " once in order to receive 'onRequestFinished' events.";
-    this.target.logWarningInPage(message, "har");
-
-    // Add the listener into internal list.
-    this._requestFinishedListeners.add(listener);
+  addRequestFinishedListener: async function(listener) {
+    let netMonitor = await this.getNetMonitorAPI();
+    netMonitor.addRequestFinishedListener(listener);
   },
 
-  removeRequestFinishedListener: function(listener) {
-    this._requestFinishedListeners.delete(listener);
-  },
-
-  getRequestFinishedListeners: function() {
-    return this._requestFinishedListeners;
+  removeRequestFinishedListener: async function(listener) {
+    let netMonitor = await this.getNetMonitorAPI();
+    netMonitor.removeRequestFinishedListener(listener);
+
+    // Destroy Network monitor API object if the following is true:
+    // 1) there is no listener
+    // 2) the Net panel doesn't exist/use the API object (if the panel
+    //    exists it's also responsible for destroying it,
+    //    see `NetMonitorPanel.open` for more details)
+    let netPanel = this.getPanel("netmonitor");
+    let hasListeners = netMonitor.hasRequestFinishedListeners();
+    if (this._netMonitorAPI && !hasListeners && !netPanel) {
+      this._netMonitorAPI.destroy();
+      this._netMonitorAPI = null;
+    }
   },
 
   /**
    * Used to lazily fetch HTTP response content within
    * `onRequestFinished` event listener.
    *
    * @param {String} requestId
    *        Id of the request for which the response content
    *        should be fetched.
    */
-  fetchResponseContent: function(requestId) {
-    let netPanel = this.getPanel("netmonitor");
-
-    // The panel doesn't have to exist (it must be selected
-    // by the user at least once to be created).
-    // Return undefined content in such case.
-    if (!netPanel) {
-      return Promise.resolve({content: {}});
-    }
-
-    return netPanel.panelWin.Netmonitor.fetchResponseContent(requestId);
+  fetchResponseContent: async function(requestId) {
+    let netMonitor = await this.getNetMonitorAPI();
+    return netMonitor.fetchResponseContent(requestId);
   },
 
   // Support management of installed WebExtensions that provide a devtools_page.
 
   /**
    * List the subset of the active WebExtensions which have a devtools_page (used by
    * toolbox-options.js to create the list of the tools provided by the enabled
    * WebExtensions).
--- a/devtools/client/netmonitor/initializer.js
+++ b/devtools/client/netmonitor/initializer.js
@@ -1,205 +1,72 @@
 /* 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/. */
+/* exported initialize */
 
 "use strict";
 
 /**
  * This script is the entry point of Network monitor panel.
  * See README.md for more information.
  */
 const { BrowserLoader } = ChromeUtils.import(
   "resource://devtools/client/shared/browser-loader.js", {});
 
 const require = window.windowRequire = BrowserLoader({
   baseURI: "resource://devtools/client/netmonitor/",
   window,
 }).require;
 
+const { NetMonitorAPI } = require("./src/api");
+const { NetMonitorApp } = require("./src/app");
 const EventEmitter = require("devtools/shared/event-emitter");
-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 { Connector } = require("./src/connector/index");
-const { configureStore } = require("./src/create-store");
-const App = createFactory(require("./src/components/App"));
-const { EVENTS } = require("./src/constants");
-const {
-  getDisplayedRequestById,
-  getSortedRequests
-} = require("./src/selectors/index");
 
 // Inject EventEmitter into global window.
 EventEmitter.decorate(window);
 
-// Configure store/state object.
-let connector = new Connector();
-const store = configureStore(connector);
-const actions = bindActionCreators(require("./src/actions/index"), store.dispatch);
+/**
+ * This is the initialization point for the Network monitor.
+ *
+ * @param {Object} api Allows reusing existing API object.
+ */
+function initialize(api) {
+  const app = new NetMonitorApp(api);
 
-// Inject to global window for testing
-window.store = store;
-window.connector = connector;
-window.actions = actions;
+  // Inject to global window for testing
+  window.Netmonitor = app;
+  window.store = app.api.store;
+  window.connector = app.api.connector;
+  window.actions = app.api.actions;
+
+  return app;
+}
 
 /**
- * Global Netmonitor object in this panel. This object can be consumed
- * by other panels (e.g. Console is using inspectRequest), by the
- * Launchpad (bootstrap), WebExtension API (getHAR), etc.
+ * The following code is used to open Network monitor in a tab.
+ * Like the Launchpad, but without Launchpad.
+ *
+ * For example:
+ * chrome://devtools/content/netmonitor/index.html?type=process
+ * loads the netmonitor for the parent process, exactly like the
+ * one in the browser toolbox
+ *
+ * It's also possible to connect to a tab.
+ * 1) go in about:debugging
+ * 2) In menu Tabs, click on a Debug button for particular tab
+ *
+ * This  will open an about:devtools-toolbox url, from which you can
+ * take type and id query parameters and reuse them for the chrome url
+ * of the netmonitor
+ *
+ * chrome://devtools/content/netmonitor/index.html?type=tab&id=1234 URLs
+ * where 1234 is the tab id, you can retrieve from about:debugging#tabs links.
+ * Simply copy the id from about:devtools-toolbox?type=tab&id=1234 URLs.
  */
-window.Netmonitor = {
-  bootstrap({ toolbox, panel }) {
-    this.mount = document.querySelector("#mount");
-    this.toolbox = toolbox;
-
-    const connection = {
-      tabConnection: {
-        tabTarget: toolbox.target,
-      },
-      toolbox,
-      panel,
-    };
-
-    const openLink = (link) => {
-      let parentDoc = toolbox.doc;
-      let iframe = parentDoc.getElementById("toolbox-panel-iframe-netmonitor");
-      let top = iframe.ownerDocument.defaultView.top;
-      top.openWebLinkIn(link, "tab");
-    };
-
-    const openSplitConsole = (err) => {
-      toolbox.openSplitConsole().then(() => {
-        toolbox.target.logErrorInPage(err, "har");
-      });
-    };
-
-    this.onRequestAdded = this.onRequestAdded.bind(this);
-    window.on(EVENTS.REQUEST_ADDED, this.onRequestAdded);
-
-    // Render the root Application component.
-    const sourceMapService = toolbox.sourceMapURLService;
-    const app = App({
-      actions,
-      connector,
-      openLink,
-      openSplitConsole,
-      sourceMapService
-    });
-    render(Provider({ store }, app), this.mount);
-
-    // Connect to the Firefox backend by default.
-    return connector.connectFirefox(connection, actions, store.getState);
-  },
-
-  destroy() {
-    unmountComponentAtNode(this.mount);
-    window.off(EVENTS.REQUEST_ADDED, this.onRequestAdded);
-    return connector.disconnect();
-  },
-
-  // Support for WebExtensions API
-
-  /**
-   * Support for `devtools.network.getHAR` (get collected data as HAR)
-   */
-  getHar() {
-    let { HarExporter } = require("devtools/client/netmonitor/src/har/har-exporter");
-    let state = store.getState();
-
-    let options = {
-      connector,
-      items: getSortedRequests(state),
-      // Always generate HAR log even if there are no requests.
-      forceExport: true,
-    };
-
-    return HarExporter.getHar(options);
-  },
-
-  /**
-   * Support for `devtools.network.onRequestFinished`. A hook for
-   * every finished HTTP request used by WebExtensions API.
-   */
-  onRequestAdded(requestId) {
-    let listeners = this.toolbox.getRequestFinishedListeners();
-    if (!listeners.size) {
-      return;
-    }
-
-    let { HarExporter } = require("devtools/client/netmonitor/src/har/har-exporter");
-    let options = {
-      connector,
-      includeResponseBodies: false,
-      items: [getDisplayedRequestById(store.getState(), requestId)],
-    };
-
-    // Build HAR for specified request only.
-    HarExporter.getHar(options).then(har => {
-      let harEntry = har.log.entries[0];
-      delete harEntry.pageref;
-      listeners.forEach(listener => listener({
-        harEntry,
-        requestId,
-      }));
-    });
-  },
-
-  /**
-   * Support for `Request.getContent` WebExt API (lazy loading response body)
-   */
-  fetchResponseContent(requestId) {
-    return connector.requestData(requestId, "responseContent");
-  },
-
-  /**
-   * Selects the specified request in the waterfall and opens the details view.
-   * This is a firefox toolbox specific API, which providing an ability to inspect
-   * a network request directly from other internal toolbox panel.
-   *
-   * @param {string} requestId The actor ID of the request to inspect.
-   * @return {object} A promise resolved once the task finishes.
-   */
-  inspectRequest(requestId) {
-    // Look for the request in the existing ones or wait for it to appear, if
-    // the network monitor is still loading.
-    return new Promise((resolve) => {
-      let request = null;
-      let inspector = () => {
-        request = getDisplayedRequestById(store.getState(), requestId);
-        if (!request) {
-          // Reset filters so that the request is visible.
-          actions.toggleRequestFilterType("all");
-          request = getDisplayedRequestById(store.getState(), requestId);
-        }
-
-        // If the request was found, select it. Otherwise this function will be
-        // called again once new requests arrive.
-        if (request) {
-          window.off(EVENTS.REQUEST_ADDED, inspector);
-          actions.selectRequest(request.id);
-          resolve();
-        }
-      };
-
-      inspector();
-
-      if (!request) {
-        window.on(EVENTS.REQUEST_ADDED, inspector);
-      }
-    });
-  }
-};
-
-// Implement support for:
-// chrome://devtools/content/netmonitor/index.html?type=tab&id=1234 URLs
-// where 1234 is the tab id, you can retrieve from about:debugging#tabs links.
-// Simply copy the id from about:devtools-toolbox?type=tab&id=1234 URLs.
 
 // URL constructor doesn't support chrome: scheme
 let href = window.location.href.replace(/chrome:/, "http://");
 let url = new window.URL(href);
 
 // If query parameters are given in a chrome tab, the inspector
 // is running in standalone.
 if (window.location.protocol === "chrome:" && url.search.length > 1) {
@@ -217,14 +84,20 @@ if (window.location.protocol === "chrome
       // Create a fake toolbox object
       let toolbox = {
         target,
         viewSourceInDebugger() {
           throw new Error("toolbox.viewSourceInDebugger is not implement from a tab");
         }
       };
 
-      window.Netmonitor.bootstrap({ toolbox });
+      let api = new NetMonitorAPI();
+      await api.connect(toolbox);
+      let app = window.initialize(api);
+      app.bootstrap({
+        toolbox,
+        document: window.document,
+      });
     } catch (err) {
       window.alert("Unable to start the network monitor:" + err);
     }
   })();
 }
--- a/devtools/client/netmonitor/panel.js
+++ b/devtools/client/netmonitor/panel.js
@@ -9,22 +9,32 @@ function NetMonitorPanel(iframeWindow, t
   this.toolbox = toolbox;
 }
 
 NetMonitorPanel.prototype = {
   async open() {
     if (!this.toolbox.target.isRemote) {
       await this.toolbox.target.makeRemote();
     }
-    await this.panelWin.Netmonitor.bootstrap({
+
+    // Reuse an existing Network monitor API object if available.
+    // It could have been created for WE API before Net panel opens.
+    let api = await this.toolbox.getNetMonitorAPI();
+    let app = this.panelWin.initialize(api);
+
+    // Connect the application object to the UI.
+    await app.bootstrap({
       toolbox: this.toolbox,
-      panel: this,
+      document: this.panelWin.document,
     });
+
+    // Ready to go!
     this.emit("ready");
     this.isReady = true;
+
     return this;
   },
 
   async destroy() {
     await this.panelWin.Netmonitor.destroy();
     this.emit("destroyed");
     return this;
   },
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/api.js
@@ -0,0 +1,197 @@
+/* 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 EventEmitter = require("devtools/shared/event-emitter");
+
+const { bindActionCreators } = require("devtools/client/shared/vendor/redux");
+const { Connector } = require("./connector/index");
+const { configureStore } = require("./create-store");
+const { EVENTS } = require("./constants");
+const Actions = require("./actions/index");
+
+const {
+  getDisplayedRequestById,
+  getSortedRequests
+} = require("./selectors/index");
+
+/**
+ * API object for NetMonitor panel (like a facade). This object can be
+ * consumed by other panels, WebExtension API, etc.
+ *
+ * This object doesn't depend on the panel UI and can be created
+ * and used even if the Network panel UI doesn't exist.
+ */
+function NetMonitorAPI() {
+  EventEmitter.decorate(this);
+
+  // Connector to the backend.
+  this.connector = new Connector();
+
+  // Configure store/state object.
+  this.store = configureStore(this.connector);
+
+  // List of listeners for `devtools.network.onRequestFinished` WebExt API
+  this._requestFinishedListeners = new Set();
+
+  // Bind event handlers
+  this.onRequestAdded = this.onRequestAdded.bind(this);
+  this.actions = bindActionCreators(Actions, this.store.dispatch);
+}
+
+NetMonitorAPI.prototype = {
+  async connect(toolbox) {
+    // Bail out if already connected.
+    if (this.toolbox) {
+      return;
+    }
+
+    this.toolbox = toolbox;
+
+    // Register listener for new requests (utilized by WebExtension API).
+    this.on(EVENTS.REQUEST_ADDED, this.onRequestAdded);
+
+    // Initialize connection to the backend. Pass `this` as the owner,
+    // so this object can receive all emitted events.
+    const connection = {
+      tabConnection: {
+        tabTarget: toolbox.target,
+      },
+      toolbox,
+      owner: this,
+    };
+
+    await this.connectBackend(this.connector, connection, this.actions,
+      this.store.getState);
+  },
+
+  /**
+   * Clean up (unmount from DOM, remove listeners, disconnect).
+   */
+  async destroy() {
+    this.off(EVENTS.REQUEST_ADDED, this.onRequestAdded);
+
+    await this.connector.disconnect();
+
+    if (this.harExportConnector) {
+      await this.harExportConnector.disconnect();
+    }
+  },
+
+  /**
+   * Connect to the Firefox backend by default.
+   *
+   * As soon as connections to different back-ends is supported
+   * this function should be responsible for picking the right API.
+   */
+  async connectBackend(connector, connection, actions, getState) {
+    // The connection might happen during Toolbox initialization
+    // so make sure the target is ready.
+    await connection.tabConnection.tabTarget.makeRemote();
+    return connector.connectFirefox(connection, actions, getState);
+  },
+
+  // HAR
+
+  /**
+   * Support for `devtools.network.getHAR` (get collected data as HAR)
+   */
+  async getHar() {
+    let { HarExporter } = require("devtools/client/netmonitor/src/har/har-exporter");
+    let state = this.store.getState();
+
+    let options = {
+      connector: this.connector,
+      items: getSortedRequests(state),
+    };
+
+    return HarExporter.getHar(options);
+  },
+
+  /**
+   * Support for `devtools.network.onRequestFinished`. A hook for
+   * every finished HTTP request used by WebExtensions API.
+   */
+  async onRequestAdded(requestId) {
+    if (!this._requestFinishedListeners.size) {
+      return;
+    }
+
+    let { HarExporter } = require("devtools/client/netmonitor/src/har/har-exporter");
+
+    let connector = await this.getHarExportConnector();
+    let request = getDisplayedRequestById(this.store.getState(), requestId);
+    if (!request) {
+      console.error("HAR: request not found " + requestId);
+      return;
+    }
+
+    let options = {
+      connector,
+      includeResponseBodies: false,
+      items: [request],
+    };
+
+    let har = await HarExporter.getHar(options);
+
+    // There is page so remove the page reference.
+    let harEntry = har.log.entries[0];
+    delete harEntry.pageref;
+
+    this._requestFinishedListeners.forEach(listener => listener({
+      harEntry,
+      requestId,
+    }));
+  },
+
+  /**
+   * Support for `Request.getContent` WebExt API (lazy loading response body)
+   */
+  async fetchResponseContent(requestId) {
+    return this.connector.requestData(requestId, "responseContent");
+  },
+
+  /**
+   * Add listener for `onRequestFinished` events.
+   *
+   * @param {Object} listener
+   *        The listener to be called it's expected to be
+   *        a function that takes ({harEntry, requestId})
+   *        as first argument.
+   */
+  addRequestFinishedListener: function(listener) {
+    this._requestFinishedListeners.add(listener);
+  },
+
+  removeRequestFinishedListener: function(listener) {
+    this._requestFinishedListeners.delete(listener);
+  },
+
+  hasRequestFinishedListeners: function() {
+    return this._requestFinishedListeners.size > 0;
+  },
+
+  /**
+   * Separate connector for HAR export.
+   */
+  async getHarExportConnector() {
+    if (this.harExportConnector) {
+      return this.harExportConnector;
+    }
+
+    const connection = {
+      tabConnection: {
+        tabTarget: this.toolbox.target,
+      },
+      toolbox: this.toolbox,
+    };
+
+    this.harExportConnector = new Connector();
+    await this.connectBackend(this.harExportConnector, connection);
+    return this.harExportConnector;
+  },
+};
+
+exports.NetMonitorAPI = NetMonitorAPI;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/app.js
@@ -0,0 +1,124 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { createFactory } = 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 App = createFactory(require("./components/App"));
+const { EVENTS } = require("./constants");
+
+const {
+  getDisplayedRequestById,
+} = require("./selectors/index");
+
+/**
+ * Global App object for Network panel. This object depends
+ * on the UI and can't be created independently.
+ *
+ * This object can be consumed by other panels (e.g. Console
+ * is using inspectRequest), by the Launchpad (bootstrap), etc.
+ *
+ * @param {Object} api An existing API object to be reused.
+ */
+function NetMonitorApp(api) {
+  this.api = api;
+}
+
+NetMonitorApp.prototype = {
+  async bootstrap({ toolbox, document }) {
+    // Get the root element for mounting.
+    this.mount = document.querySelector("#mount");
+
+    const openLink = (link) => {
+      let parentDoc = toolbox.doc;
+      let iframe = parentDoc.getElementById("toolbox-panel-iframe-netmonitor");
+      let top = iframe.ownerDocument.defaultView.top;
+      top.openUILinkIn(link, "tab");
+    };
+
+    const openSplitConsole = (err) => {
+      toolbox.openSplitConsole().then(() => {
+        toolbox.target.logErrorInPage(err, "har");
+      });
+    };
+
+    let {
+      actions,
+      connector,
+      store,
+    } = this.api;
+
+    const sourceMapService = toolbox.sourceMapURLService;
+    const app = App({
+      actions,
+      connector,
+      openLink,
+      openSplitConsole,
+      sourceMapService
+    });
+
+    // Render the root Application component.
+    render(Provider({ store: store }, app), this.mount);
+  },
+
+  /**
+   * Clean up (unmount from DOM, remove listeners, disconnect).
+   */
+  async destroy() {
+    unmountComponentAtNode(this.mount);
+
+    // Make sure to destroy the API object. It's usually destroyed
+    // in the Toolbox destroy method, but we need it here for case
+    // where the Network panel is initialized without the toolbox
+    // and running in a tab (see initialize.js for details).
+    await this.api.destroy();
+  },
+
+  /**
+   * Selects the specified request in the waterfall and opens the details view.
+   * This is a firefox toolbox specific API, which providing an ability to inspect
+   * a network request directly from other internal toolbox panel.
+   *
+   * @param {string} requestId The actor ID of the request to inspect.
+   * @return {object} A promise resolved once the task finishes.
+   */
+  async inspectRequest(requestId) {
+    let {
+      actions,
+      store,
+    } = this.api;
+
+    // Look for the request in the existing ones or wait for it to appear,
+    // if the network monitor is still loading.
+    return new Promise((resolve) => {
+      let request = null;
+      let inspector = () => {
+        request = getDisplayedRequestById(store.getState(), requestId);
+        if (!request) {
+          // Reset filters so that the request is visible.
+          actions.toggleRequestFilterType("all");
+          request = getDisplayedRequestById(store.getState(), requestId);
+        }
+
+        // If the request was found, select it. Otherwise this function will be
+        // called again once new requests arrive.
+        if (request) {
+          this.api.off(EVENTS.REQUEST_ADDED, inspector);
+          actions.selectRequest(request.id);
+          resolve();
+        }
+      };
+
+      inspector();
+
+      if (!request) {
+        this.api.on(EVENTS.REQUEST_ADDED, inspector);
+      }
+    });
+  }
+};
+
+exports.NetMonitorApp = NetMonitorApp;
--- a/devtools/client/netmonitor/src/connector/chrome-connector.js
+++ b/devtools/client/netmonitor/src/connector/chrome-connector.js
@@ -93,9 +93,9 @@ class ChromeConnector {
     // TODO : implement.
   }
 
   viewSourceInDebugger() {
     // TODO : implement.
   }
 }
 
-module.exports = new ChromeConnector();
+module.exports = ChromeConnector;
--- a/devtools/client/netmonitor/src/connector/firefox-connector.js
+++ b/devtools/client/netmonitor/src/connector/firefox-connector.js
@@ -30,56 +30,72 @@ class FirefoxConnector {
     this.requestData = this.requestData.bind(this);
     this.getTimingMarker = this.getTimingMarker.bind(this);
 
     // Internals
     this.getLongString = this.getLongString.bind(this);
     this.getNetworkRequest = this.getNetworkRequest.bind(this);
   }
 
+  /**
+   * Connect to the backend.
+   *
+   * @param {Object} connection object with e.g. reference to the Toolbox.
+   * @param {Object} actions (optional) is used to fire Redux actions to update store.
+   * @param {Object} getState (optional) is used to get access to the state.
+   */
   async connect(connection, actions, getState) {
     this.actions = actions;
     this.getState = getState;
     this.tabTarget = connection.tabConnection.tabTarget;
     this.toolbox = connection.toolbox;
-    this.panel = connection.panel;
+
+    // The owner object (NetMonitorAPI) received all events.
+    this.owner = connection.owner;
 
     this.webConsoleClient = this.tabTarget.activeConsole;
 
     this.dataProvider = new FirefoxDataProvider({
       webConsoleClient: this.webConsoleClient,
       actions: this.actions,
+      owner: this.owner,
     });
 
     await this.addListeners();
 
     // Listener for `will-navigate` event is (un)registered outside
     // of the `addListeners` and `removeListeners` methods since
     // these are used to pause/resume the connector.
     // Paused network panel should be automatically resumed when page
     // reload, so `will-navigate` listener needs to be there all the time.
-    this.tabTarget.on("will-navigate", this.willNavigate);
-    this.tabTarget.on("navigate", this.navigate);
+    if (this.tabTarget) {
+      this.tabTarget.on("will-navigate", this.willNavigate);
+      this.tabTarget.on("navigate", this.navigate);
+    }
 
-    this.displayCachedEvents();
+    // Displaying cache events is only intended for the UI panel.
+    if (this.actions) {
+      this.displayCachedEvents();
+    }
   }
 
   async disconnect() {
-    this.actions.batchReset();
+    if (this.actions) {
+      this.actions.batchReset();
+    }
 
     await this.removeListeners();
 
     if (this.tabTarget) {
       this.tabTarget.off("will-navigate");
       this.tabTarget = null;
     }
 
     this.webConsoleClient = null;
     this.dataProvider = null;
-    this.panel = null;
   }
 
   async pause() {
     await this.removeListeners();
   }
 
   async resume() {
     await this.addListeners();
@@ -127,53 +143,58 @@ class FirefoxConnector {
     }
   }
 
   enableActions(enable) {
     this.dataProvider.enableActions(enable);
   }
 
   willNavigate() {
-    if (!Services.prefs.getBoolPref("devtools.netmonitor.persistlog")) {
-      this.actions.batchReset();
-      this.actions.clearRequests();
-    } else {
-      // If the log is persistent, just clear all accumulated timing markers.
-      this.actions.clearTimingMarkers();
+    if (this.actions) {
+      if (!Services.prefs.getBoolPref("devtools.netmonitor.persistlog")) {
+        this.actions.batchReset();
+        this.actions.clearRequests();
+      } else {
+        // If the log is persistent, just clear all accumulated timing markers.
+        this.actions.clearTimingMarkers();
+      }
     }
 
     // Resume is done automatically on page reload/navigation.
-    let state = this.getState();
-    if (!state.requests.recording) {
-      this.actions.toggleRecording();
+    if (this.actions && this.getState) {
+      let state = this.getState();
+      if (!state.requests.recording) {
+        this.actions.toggleRecording();
+      }
     }
   }
 
   navigate() {
     if (this.dataProvider.isPayloadQueueEmpty()) {
       this.onReloaded();
       return;
     }
     let listener = () => {
       if (this.dataProvider && !this.dataProvider.isPayloadQueueEmpty()) {
         return;
       }
-      window.off(EVENTS.PAYLOAD_READY, listener);
+      this.owner.off(EVENTS.PAYLOAD_READY, listener);
       // Netmonitor may already be destroyed,
       // so do not try to notify the listeners
       if (this.dataProvider) {
         this.onReloaded();
       }
     };
-    window.on(EVENTS.PAYLOAD_READY, listener);
+    this.owner.on(EVENTS.PAYLOAD_READY, listener);
   }
 
   onReloaded() {
-    if (this.panel) {
-      this.panel.emit("reloaded");
+    let panel = this.toolbox.getPanel("netmonitor");
+    if (panel) {
+      panel.emit("reloaded");
     }
   }
 
   /**
    * Display any network events already in the cache.
    */
   displayCachedEvents() {
     for (let networkInfo of this.webConsoleClient.getNetworkEvents()) {
@@ -199,30 +220,37 @@ class FirefoxConnector {
   onDocLoadingMarker(marker) {
     // Translate marker into event similar to newer "docEvent" event sent by the console
     // actor
     let event = {
       name: marker.name == "document::DOMContentLoaded" ?
             "dom-interactive" : "dom-complete",
       time: marker.unixTime / 1000
     };
-    this.actions.addTimingMarker(event);
-    window.emit(EVENTS.TIMELINE_EVENT, event);
+
+    if (this.actions) {
+      this.actions.addTimingMarker(event);
+    }
+
+    this.emit(EVENTS.TIMELINE_EVENT, event);
   }
 
   /**
    * The "DOMContentLoaded" and "Load" events sent by the console actor.
    *
    * Only used by FF60+.
    *
    * @param {object} marker
    */
   onDocEvent(event) {
-    this.actions.addTimingMarker(event);
-    window.emit(EVENTS.TIMELINE_EVENT, event);
+    if (this.actions) {
+      this.actions.addTimingMarker(event);
+    }
+
+    this.emit(EVENTS.TIMELINE_EVENT, event);
   }
 
   /**
    * Send a HTTP request data payload
    *
    * @param {object} data data payload would like to sent to backend
    * @param {function} callback callback will be invoked after the request finished
    */
@@ -366,14 +394,32 @@ class FirefoxConnector {
    * @param {object} request network request instance
    * @param {string} type NetworkEventUpdate type
    */
   requestData(request, type) {
     return this.dataProvider.requestData(request, type);
   }
 
   getTimingMarker(name) {
+    if (!this.getState) {
+      return -1;
+    }
+
     let state = this.getState();
     return getDisplayedTimingMarker(state, name);
   }
+
+  /**
+   * Fire events for the owner object.
+   */
+  emit(type, data) {
+    if (this.owner) {
+      this.owner.emit(type, data);
+    }
+
+    // Consumed mainly by tests.
+    if (typeof window != "undefined") {
+      window.emit(type, data);
+    }
+  }
 }
 
-module.exports = new FirefoxConnector();
+module.exports = FirefoxConnector;
--- a/devtools/client/netmonitor/src/connector/firefox-data-provider.js
+++ b/devtools/client/netmonitor/src/connector/firefox-data-provider.js
@@ -13,21 +13,29 @@ const { fetchHeaders } = require("../uti
  * This object is responsible for fetching additional HTTP
  * data from the backend over RDP protocol.
  *
  * The object also keeps track of RDP requests in-progress,
  * so it's possible to determine whether all has been fetched
  * or not.
  */
 class FirefoxDataProvider {
-  constructor({webConsoleClient, actions}) {
+  /**
+   * Constructor for data provider
+   *
+   * @param {Object} webConcoleClient represents the client object for Console actor.
+   * @param {Object} actions set of actions fired during data fetching process
+   * @params {Object} owner all events are fired on this object
+   */
+  constructor({webConsoleClient, actions, owner}) {
     // Options
     this.webConsoleClient = webConsoleClient;
-    this.actions = actions;
+    this.actions = actions || {};
     this.actionsEnabled = true;
+    this.owner = owner;
 
     // Internal properties
     this.payloadQueue = new Map();
 
     // Map[key string => Promise] used by `requestData` to prevent requesting the same
     // request data twice.
     this.lazyRequestData = new Map();
 
@@ -79,17 +87,17 @@ class FirefoxDataProvider {
         // FF59+ supports fetching the traces lazily via requestData.
         stacktrace: cause.stacktrace,
 
         fromCache,
         fromServiceWorker,
       }, true);
     }
 
-    emit(EVENTS.REQUEST_ADDED, id);
+    this.emit(EVENTS.REQUEST_ADDED, id);
   }
 
   /**
    * Update a network request if it already exists in application state.
    *
    * @param {string} id request id
    * @param {object} data data payload will be updated to application state
    */
@@ -309,17 +317,17 @@ class FirefoxDataProvider {
       fromCache,
       fromServiceWorker,
       isXHR,
       method,
       startedDateTime,
       url,
     });
 
-    emit(EVENTS.NETWORK_EVENT, actor);
+    this.emit(EVENTS.NETWORK_EVENT, actor);
   }
 
   /**
    * The "networkEventUpdate" message type handler.
    *
    * @param {object} packet the message received from the server.
    * @param {object} networkInfo the network request information.
    */
@@ -336,17 +344,17 @@ class FirefoxDataProvider {
         this.pushRequestToQueue(actor, {
           httpVersion: networkInfo.response.httpVersion,
           remoteAddress: networkInfo.response.remoteAddress,
           remotePort: networkInfo.response.remotePort,
           status: networkInfo.response.status,
           statusText: networkInfo.response.statusText,
           headersSize: networkInfo.response.headersSize
         });
-        emit(EVENTS.STARTED_RECEIVING_RESPONSE, actor);
+        this.emit(EVENTS.STARTED_RECEIVING_RESPONSE, actor);
         break;
       case "responseContent":
         this.pushRequestToQueue(actor, {
           contentSize: networkInfo.response.bodySize,
           transferredSize: networkInfo.response.transferredSize,
           mimeType: networkInfo.response.content.mimeType,
         });
         break;
@@ -362,17 +370,17 @@ class FirefoxDataProvider {
     }
 
     // This available field helps knowing when/if updateType property is arrived
     // and can be requested via `requestData`
     this.pushRequestToQueue(actor, { [`${updateType}Available`]: true });
 
     this.onPayloadDataReceived(actor);
 
-    emit(EVENTS.NETWORK_EVENT_UPDATED, actor);
+    this.emit(EVENTS.NETWORK_EVENT_UPDATED, actor);
   }
 
   /**
    * Notify actions when messages from onNetworkEventUpdate are done, networkEventUpdate
    * messages contain initial network info for each updateType and then we can invoke
    * requestData to fetch its corresponded data lazily.
    * Once all updateTypes of networkEventUpdate message are arrived, we flush merged
    * request payload from pending queue and then update component.
@@ -388,17 +396,17 @@ class FirefoxDataProvider {
     this.payloadQueue.delete(actor);
 
     if (this.actionsEnabled && this.actions.updateRequest) {
       await this.actions.updateRequest(actor, payload, true);
     }
 
     // This event is fired only once per request, once all the properties are fetched
     // from `onNetworkEventUpdate`. There should be no more RDP requests after this.
-    emit(EVENTS.PAYLOAD_READY, actor);
+    this.emit(EVENTS.PAYLOAD_READY, actor);
   }
 
   /**
    * Public connector API to lazily request HTTP details from the backend.
    *
    * The method focus on:
    * - calling the right actor method,
    * - emitting an event to tell we start fetching some request data,
@@ -458,17 +466,17 @@ class FirefoxDataProvider {
     // Calculate real name of the client getter.
     let clientMethodName = `get${method.charAt(0).toUpperCase()}${method.slice(1)}`;
     // The name of the callback that processes request response
     let callbackMethodName = `on${method.charAt(0).toUpperCase()}${method.slice(1)}`;
     // And the event to fire before updating this data
     let updatingEventName = `UPDATING_${method.replace(/([A-Z])/g, "_$1").toUpperCase()}`;
 
     // Emit event that tell we just start fetching some data
-    emit(EVENTS[updatingEventName], actor);
+    this.emit(EVENTS[updatingEventName], actor);
 
     let response = await new Promise((resolve, reject) => {
       // Do a RDP request to fetch data from the actor.
       if (typeof this.webConsoleClient[clientMethodName] === "function") {
         // Make sure we fetch the real actor data instead of cloned actor
         // e.g. CustomRequestPanel will clone a request with additional '-clone' actor id
         this.webConsoleClient[clientMethodName](actor.replace("-clone", ""), (res) => {
           if (res.error) {
@@ -495,131 +503,136 @@ class FirefoxDataProvider {
    * Handles additional information received for a "requestHeaders" packet.
    *
    * @param {object} response the message received from the server.
    */
   async onRequestHeaders(response) {
     let payload = await this.updateRequest(response.from, {
       requestHeaders: response
     });
-    emit(EVENTS.RECEIVED_REQUEST_HEADERS, response.from);
+    this.emit(EVENTS.RECEIVED_REQUEST_HEADERS, response.from);
     return payload.requestHeaders;
   }
 
   /**
    * Handles additional information received for a "responseHeaders" packet.
    *
    * @param {object} response the message received from the server.
    */
   async onResponseHeaders(response) {
     let payload = await this.updateRequest(response.from, {
       responseHeaders: response
     });
-    emit(EVENTS.RECEIVED_RESPONSE_HEADERS, response.from);
+    this.emit(EVENTS.RECEIVED_RESPONSE_HEADERS, response.from);
     return payload.responseHeaders;
   }
 
   /**
    * Handles additional information received for a "requestCookies" packet.
    *
    * @param {object} response the message received from the server.
    */
   async onRequestCookies(response) {
     let payload = await this.updateRequest(response.from, {
       requestCookies: response
     });
-    emit(EVENTS.RECEIVED_REQUEST_COOKIES, response.from);
+    this.emit(EVENTS.RECEIVED_REQUEST_COOKIES, response.from);
     return payload.requestCookies;
   }
 
   /**
    * Handles additional information received for a "requestPostData" packet.
    *
    * @param {object} response the message received from the server.
    */
   async onRequestPostData(response) {
     let payload = await this.updateRequest(response.from, {
       requestPostData: response
     });
-    emit(EVENTS.RECEIVED_REQUEST_POST_DATA, response.from);
+    this.emit(EVENTS.RECEIVED_REQUEST_POST_DATA, response.from);
     return payload.requestPostData;
   }
 
   /**
    * Handles additional information received for a "securityInfo" packet.
    *
    * @param {object} response the message received from the server.
    */
   async onSecurityInfo(response) {
     let payload = await this.updateRequest(response.from, {
       securityInfo: response.securityInfo
     });
-    emit(EVENTS.RECEIVED_SECURITY_INFO, response.from);
+    this.emit(EVENTS.RECEIVED_SECURITY_INFO, response.from);
     return payload.securityInfo;
   }
 
   /**
    * Handles additional information received for a "responseCookies" packet.
    *
    * @param {object} response the message received from the server.
    */
   async onResponseCookies(response) {
     let payload = await this.updateRequest(response.from, {
       responseCookies: response
     });
-    emit(EVENTS.RECEIVED_RESPONSE_COOKIES, response.from);
+    this.emit(EVENTS.RECEIVED_RESPONSE_COOKIES, response.from);
     return payload.responseCookies;
   }
 
   /**
    * Handles additional information received via "getResponseContent" request.
    *
    * @param {object} response the message received from the server.
    */
   async onResponseContent(response) {
     let payload = await this.updateRequest(response.from, {
       // We have to ensure passing mimeType as fetchResponseContent needs it from
       // updateRequest. It will convert the LongString in `response.content.text` to a
       // string.
       mimeType: response.content.mimeType,
       responseContent: response,
     });
-    emit(EVENTS.RECEIVED_RESPONSE_CONTENT, response.from);
+    this.emit(EVENTS.RECEIVED_RESPONSE_CONTENT, response.from);
     return payload.responseContent;
   }
 
   /**
    * Handles additional information received for a "eventTimings" packet.
    *
    * @param {object} response the message received from the server.
    */
   async onEventTimings(response) {
     let payload = await this.updateRequest(response.from, {
       eventTimings: response
     });
-    emit(EVENTS.RECEIVED_EVENT_TIMINGS, response.from);
+    this.emit(EVENTS.RECEIVED_EVENT_TIMINGS, response.from);
     return payload.eventTimings;
   }
 
   /**
    * Handles information received for a "stackTrace" packet.
    *
    * @param {object} response the message received from the server.
    */
   async onStackTrace(response) {
     let payload = await this.updateRequest(response.from, {
       stacktrace: response.stacktrace
     });
-    emit(EVENTS.RECEIVED_EVENT_STACKTRACE, response.from);
+    this.emit(EVENTS.RECEIVED_EVENT_STACKTRACE, response.from);
     return payload.stacktrace;
   }
-}
 
-/**
- * Guard 'emit' to avoid exception in non-window environment.
- */
-function emit(type, data) {
-  if (typeof window != "undefined") {
-    window.emit(type, data);
+  /**
+   * Fire events for the owner object.
+   */
+  emit(type, data) {
+    if (this.owner) {
+      this.owner.emit(type, data);
+    }
+
+    // Consumed mainly by tests.
+    if (typeof window != "undefined") {
+      window.emit(type, data);
+    }
   }
 }
 
 module.exports = FirefoxDataProvider;
--- a/devtools/client/netmonitor/src/connector/index.js
+++ b/devtools/client/netmonitor/src/connector/index.js
@@ -49,22 +49,24 @@ class Connector {
     }
   }
 
   disconnect() {
     this.connector && this.connector.disconnect();
   }
 
   connectChrome(connection, actions, getState) {
-    this.connector = require("./chrome-connector");
+    let ChromeConnector = require("./chrome-connector");
+    this.connector = new ChromeConnector();
     return this.connector.connect(connection, actions, getState);
   }
 
   connectFirefox(connection, actions, getState) {
-    this.connector = require("./firefox-connector");
+    let FirefoxConnector = require("./firefox-connector");
+    this.connector = new FirefoxConnector();
     return this.connector.connect(connection, actions, getState);
   }
 
   pause() {
     return this.connector.pause();
   }
 
   resume() {
--- a/devtools/client/netmonitor/src/har/har-exporter.js
+++ b/devtools/client/netmonitor/src/har/har-exporter.js
@@ -121,17 +121,19 @@ const HarExporter = {
 
   /**
    * Get HAR data as JSON object.
    *
    * @param Object options
    *        Configuration object, see save() for detailed description.
    */
   getHar: function(options) {
-    return this.fetchHarData(options).then(JSON.parse);
+    return this.fetchHarData(options).then(data => {
+      return data ? JSON.parse(data) : null;
+    });
   },
 
   // Helpers
 
   fetchHarData: function(options) {
     // Generate page ID
     options.id = options.id || uid++;
 
--- a/devtools/client/netmonitor/src/moz.build
+++ b/devtools/client/netmonitor/src/moz.build
@@ -10,11 +10,13 @@ DIRS += [
     'middleware',
     'reducers',
     'selectors',
     'utils',
     'widgets',
 ]
 
 DevToolsModules(
+    'api.js',
+    'app.js',
     'constants.js',
     'create-store.js',
 )