Bug 1350215 - Merge shared/components to components r?honza draft
authorRicky Chien <ricky060709@gmail.com>
Sun, 26 Mar 2017 19:11:01 +0800
changeset 552836 b22d8522b41ea4592fa419045e6e021e3749ff7a
parent 552835 e7545408e3ffb1d7beb62b625fb94759035ebbe7
child 552837 da406f7b7484ee0d211447f0d49f715afc726ec5
push id51484
push userbmo:rchien@mozilla.com
push dateWed, 29 Mar 2017 03:22:04 +0000
reviewershonza
bugs1350215
milestone55.0a1
Bug 1350215 - Merge shared/components to components r?honza MozReview-Commit-ID: 9BQBRYCfdsu
devtools/client/netmonitor/components/App.js
devtools/client/netmonitor/components/CookiesPanel.js
devtools/client/netmonitor/components/CustomRequestPanel.js
devtools/client/netmonitor/components/Editor.js
devtools/client/netmonitor/components/HeadersPanel.js
devtools/client/netmonitor/components/MDNLink.js
devtools/client/netmonitor/components/MonitorPanel.js
devtools/client/netmonitor/components/NetworkDetailsPanel.js
devtools/client/netmonitor/components/ParamsPanel.js
devtools/client/netmonitor/components/PreviewPanel.js
devtools/client/netmonitor/components/PropertiesView.js
devtools/client/netmonitor/components/RequestList.js
devtools/client/netmonitor/components/RequestListContent.js
devtools/client/netmonitor/components/RequestListEmptyNotice.js
devtools/client/netmonitor/components/RequestListHeader.js
devtools/client/netmonitor/components/RequestListItem.js
devtools/client/netmonitor/components/ResponsePanel.js
devtools/client/netmonitor/components/SecurityPanel.js
devtools/client/netmonitor/components/StatisticsPanel.js
devtools/client/netmonitor/components/TabboxPanel.js
devtools/client/netmonitor/components/TimingsPanel.js
devtools/client/netmonitor/components/monitor-panel.js
devtools/client/netmonitor/components/moz.build
devtools/client/netmonitor/components/network-monitor.js
devtools/client/netmonitor/components/request-list-content.js
devtools/client/netmonitor/components/request-list-empty.js
devtools/client/netmonitor/components/request-list-header.js
devtools/client/netmonitor/components/request-list-item.js
devtools/client/netmonitor/components/request-list.js
devtools/client/netmonitor/components/statistics-panel.js
devtools/client/netmonitor/components/toolbar.js
devtools/client/netmonitor/moz.build
devtools/client/netmonitor/netmonitor.js
devtools/client/netmonitor/shared/components/cookies-panel.js
devtools/client/netmonitor/shared/components/custom-request-panel.js
devtools/client/netmonitor/shared/components/editor.js
devtools/client/netmonitor/shared/components/headers-panel.js
devtools/client/netmonitor/shared/components/mdn-link.js
devtools/client/netmonitor/shared/components/moz.build
devtools/client/netmonitor/shared/components/network-details-panel.js
devtools/client/netmonitor/shared/components/params-panel.js
devtools/client/netmonitor/shared/components/preview-panel.js
devtools/client/netmonitor/shared/components/properties-view.js
devtools/client/netmonitor/shared/components/response-panel.js
devtools/client/netmonitor/shared/components/security-panel.js
devtools/client/netmonitor/shared/components/tabbox-panel.js
devtools/client/netmonitor/shared/components/timings-panel.js
devtools/client/netmonitor/shared/moz.build
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/App.js
@@ -0,0 +1,40 @@
+/* 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,
+  DOM,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+
+// Components
+const MonitorPanel = createFactory(require("./MonitorPanel"));
+const StatisticsPanel = createFactory(require("./StatisticsPanel"));
+
+const { div } = DOM;
+
+/*
+ * App component
+ * The top level component for representing main panel
+ */
+function App({ statisticsOpen }) {
+  return (
+    div({ className: "network-monitor" },
+      !statisticsOpen ? MonitorPanel() : StatisticsPanel()
+    )
+  );
+}
+
+App.displayName = "App";
+
+App.propTypes = {
+  statisticsOpen: PropTypes.bool.isRequired,
+};
+
+module.exports = connect(
+  (state) => ({ statisticsOpen: state.ui.statisticsOpen }),
+)(App);
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/CookiesPanel.js
@@ -0,0 +1,99 @@
+/* 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,
+  DOM,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { L10N } = require("../utils/l10n");
+
+// Component
+const PropertiesView = createFactory(require("./PropertiesView"));
+
+const { div } = DOM;
+
+const COOKIES_EMPTY_TEXT = L10N.getStr("cookiesEmptyText");
+const COOKIES_FILTER_TEXT = L10N.getStr("cookiesFilterText");
+const REQUEST_COOKIES = L10N.getStr("requestCookies");
+const RESPONSE_COOKIES = L10N.getStr("responseCookies");
+const SECTION_NAMES = [
+  RESPONSE_COOKIES,
+  REQUEST_COOKIES,
+];
+
+/*
+ * Cookies panel component
+ * This tab lists full details of any cookies sent with the request or response
+ */
+function CookiesPanel({
+  request,
+}) {
+  let {
+    requestCookies = { cookies: [] },
+    responseCookies = { cookies: [] },
+  } = request;
+
+  requestCookies = requestCookies.cookies || requestCookies;
+  responseCookies = responseCookies.cookies || responseCookies;
+
+  if (!requestCookies.length && !responseCookies.length) {
+    return div({ className: "empty-notice" },
+      COOKIES_EMPTY_TEXT
+    );
+  }
+
+  let object = {};
+
+  if (responseCookies.length) {
+    object[RESPONSE_COOKIES] = getProperties(responseCookies);
+  }
+
+  if (requestCookies.length) {
+    object[REQUEST_COOKIES] = getProperties(requestCookies);
+  }
+
+  return (
+    div({ className: "panel-container" },
+      PropertiesView({
+        object,
+        filterPlaceHolder: COOKIES_FILTER_TEXT,
+        sectionNames: SECTION_NAMES,
+      })
+    )
+  );
+}
+
+CookiesPanel.displayName = "CookiesPanel";
+
+CookiesPanel.propTypes = {
+  request: PropTypes.object.isRequired,
+};
+
+/**
+ * Mapping array to dict for TreeView usage.
+ * Since TreeView only support Object(dict) format.
+ *
+ * @param {Object[]} arr - key-value pair array like cookies or params
+ * @returns {Object}
+ */
+function getProperties(arr) {
+  return arr.reduce((map, obj) => {
+    // Generally cookies object contains only name and value properties and can
+    // be rendered as name: value pair.
+    // When there are more properties in cookies object such as extra or path,
+    // We will pass the object to display these extra information
+    if (Object.keys(obj).length > 2) {
+      map[obj.name] = Object.assign({}, obj);
+      delete map[obj.name].name;
+    } else {
+      map[obj.name] = obj.value;
+    }
+    return map;
+  }, {});
+}
+
+module.exports = CookiesPanel;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/CustomRequestPanel.js
@@ -0,0 +1,257 @@
+/* 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 {
+  DOM,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+const { L10N } = require("../utils/l10n");
+const Actions = require("../actions/index");
+const { getSelectedRequest } = require("../selectors/index");
+const {
+  getUrlQuery,
+  parseQueryString,
+  writeHeaderText,
+} = require("../utils/request-utils");
+
+const {
+  button,
+  div,
+  input,
+  textarea,
+} = DOM;
+
+const CUSTOM_CANCEL = L10N.getStr("netmonitor.custom.cancel");
+const CUSTOM_HEADERS = L10N.getStr("netmonitor.custom.headers");
+const CUSTOM_NEW_REQUEST = L10N.getStr("netmonitor.custom.newRequest");
+const CUSTOM_POSTDATA = L10N.getStr("netmonitor.custom.postData");
+const CUSTOM_QUERY = L10N.getStr("netmonitor.custom.query");
+const CUSTOM_SEND = L10N.getStr("netmonitor.custom.send");
+
+function CustomRequestPanel({
+  removeSelectedCustomRequest,
+  request = {},
+  sendCustomRequest,
+  updateRequest,
+}) {
+  let {
+    method,
+    customQueryValue,
+    requestHeaders,
+    requestPostData,
+    url,
+  } = request;
+
+  let headers = "";
+  if (requestHeaders) {
+    headers = requestHeaders.customHeadersValue ?
+      requestHeaders.customHeadersValue : writeHeaderText(requestHeaders.headers);
+  }
+  let queryArray = url ? parseQueryString(getUrlQuery(url)) : [];
+  let params = customQueryValue;
+  if (!params) {
+    params = queryArray ?
+      queryArray.map(({ name, value }) => name + "=" + value).join("\n") : "";
+  }
+  let postData = requestPostData && requestPostData.postData.text ?
+    requestPostData.postData.text : "";
+
+  return (
+    div({ className: "custom-request-panel" },
+      div({ className: "tabpanel-summary-container custom-request" },
+        div({ className: "custom-request-label custom-header" },
+          CUSTOM_NEW_REQUEST
+        ),
+        button({
+          className: "devtools-button",
+          id: "custom-request-send-button",
+          onClick: sendCustomRequest,
+        },
+          CUSTOM_SEND
+        ),
+        button({
+          className: "devtools-button",
+          id: "custom-request-close-button",
+          onClick: removeSelectedCustomRequest,
+        },
+          CUSTOM_CANCEL
+        ),
+      ),
+      div({
+        className: "tabpanel-summary-container custom-method-and-url",
+        id: "custom-method-and-url",
+      },
+        input({
+          className: "custom-method-value",
+          id: "custom-method-value",
+          onChange: (evt) => updateCustomRequestFields(evt, request, updateRequest),
+          value: method || "GET",
+        }),
+        input({
+          className: "custom-url-value",
+          id: "custom-url-value",
+          onChange: (evt) => updateCustomRequestFields(evt, request, updateRequest),
+          value: url || "http://",
+        }),
+      ),
+      // Hide query field when there is no params
+      params ? div({
+        className: "tabpanel-summary-container custom-section",
+        id: "custom-query",
+      },
+        div({ className: "custom-request-label" }, CUSTOM_QUERY),
+        textarea({
+          className: "tabpanel-summary-input",
+          id: "custom-query-value",
+          onChange: (evt) => updateCustomRequestFields(evt, request, updateRequest),
+          rows: 4,
+          value: params,
+          wrap: "off",
+        })
+      ) : null,
+      div({
+        id: "custom-headers",
+        className: "tabpanel-summary-container custom-section",
+      },
+        div({ className: "custom-request-label" }, CUSTOM_HEADERS),
+        textarea({
+          className: "tabpanel-summary-input",
+          id: "custom-headers-value",
+          onChange: (evt) => updateCustomRequestFields(evt, request, updateRequest),
+          rows: 8,
+          value: headers,
+          wrap: "off",
+        })
+      ),
+      div({
+        id: "custom-postdata",
+        className: "tabpanel-summary-container custom-section",
+      },
+        div({ className: "custom-request-label" }, CUSTOM_POSTDATA),
+        textarea({
+          className: "tabpanel-summary-input",
+          id: "custom-postdata-value",
+          onChange: (evt) => updateCustomRequestFields(evt, request, updateRequest),
+          rows: 6,
+          value: postData,
+          wrap: "off",
+        })
+      ),
+    )
+  );
+}
+
+CustomRequestPanel.displayName = "CustomRequestPanel";
+
+CustomRequestPanel.propTypes = {
+  removeSelectedCustomRequest: PropTypes.func.isRequired,
+  request: PropTypes.object,
+  sendCustomRequest: PropTypes.func.isRequired,
+  updateRequest: PropTypes.func.isRequired,
+};
+
+/**
+ * Parse a text representation of a name[divider]value list with
+ * the given name regex and divider character.
+ *
+ * @param {string} text - Text of list
+ * @return {array} array of headers info {name, value}
+ */
+function parseRequestText(text, namereg, divider) {
+  let regex = new RegExp(`(${namereg})\\${divider}\\s*(.+)`);
+  let pairs = [];
+
+  for (let line of text.split("\n")) {
+    let matches = regex.exec(line);
+    if (matches) {
+      let [, name, value] = matches;
+      pairs.push({ name, value });
+    }
+  }
+  return pairs;
+}
+
+/**
+ * Update Custom Request Fields
+ *
+ * @param {Object} evt click event
+ * @param {Object} request current request
+ * @param {updateRequest} updateRequest action
+ */
+function updateCustomRequestFields(evt, request, updateRequest) {
+  const val = evt.target.value;
+  let data;
+  switch (evt.target.id) {
+    case "custom-headers-value":
+      let customHeadersValue = val || "";
+      // Parse text representation of multiple HTTP headers
+      let headersArray = parseRequestText(customHeadersValue, "\\S+?", ":");
+      // Remove temp customHeadersValue while query string is parsable
+      if (customHeadersValue === "" ||
+          headersArray.length === customHeadersValue.split("\n").length) {
+        customHeadersValue = null;
+      }
+      data = {
+        requestHeaders: {
+          customHeadersValue,
+          headers: headersArray,
+        },
+      };
+      break;
+    case "custom-method-value":
+      data = { method: val.trim() };
+      break;
+    case "custom-postdata-value":
+      data = {
+        requestPostData: {
+          postData: { text: val },
+        }
+      };
+      break;
+    case "custom-query-value":
+      let customQueryValue = val || "";
+      // Parse readable text list of a query string
+      let queryArray = customQueryValue ?
+        parseRequestText(customQueryValue, ".+?", "=") : [];
+      // Write out a list of query params into a query string
+      let queryString = queryArray.map(
+        ({ name, value }) => name + "=" + value).join("&");
+      let url = queryString ? [request.url.split("?")[0], queryString].join("?") :
+        request.url.split("?")[0];
+      // Remove temp customQueryValue while query string is parsable
+      if (customQueryValue === "" ||
+          queryArray.length === customQueryValue.split("\n").length) {
+        customQueryValue = null;
+      }
+      data = {
+        customQueryValue,
+        url,
+      };
+      break;
+    case "custom-url-value":
+      data = {
+        customQueryValue: null,
+        url: val
+      };
+      break;
+    default:
+      break;
+  }
+  if (data) {
+    // All updateRequest batch mode should be disabled to make UI editing in sync
+    updateRequest(request.id, data, false);
+  }
+}
+
+module.exports = connect(
+  (state) => ({ request: getSelectedRequest(state) }),
+  (dispatch) => ({
+    removeSelectedCustomRequest: () => dispatch(Actions.removeSelectedCustomRequest()),
+    sendCustomRequest: () => dispatch(Actions.sendCustomRequest()),
+    updateRequest: (id, data, batch) => dispatch(Actions.updateRequest(id, data, batch)),
+  })
+)(CustomRequestPanel);
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/Editor.js
@@ -0,0 +1,107 @@
+/* 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-disable react/prop-types */
+
+"use strict";
+
+const {
+  createClass,
+  DOM,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const SourceEditor = require("devtools/client/sourceeditor/editor");
+
+const { div } = DOM;
+const SYNTAX_HIGHLIGHT_MAX_SIZE = 102400;
+
+/**
+ * CodeMirror editor as a React component
+ */
+const Editor = createClass({
+  displayName: "Editor",
+
+  propTypes: {
+    // Source editor syntax hightligh mode, which is a mime type defined in CodeMirror
+    mode: PropTypes.string,
+    // Source editor is displayed if set to true
+    open: PropTypes.bool,
+    // Source editor content
+    text: PropTypes.string,
+  },
+
+  getDefaultProps() {
+    return {
+      mode: null,
+      open: true,
+      text: "",
+    };
+  },
+
+  componentDidMount() {
+    const { mode, text } = this.props;
+
+    this.editor = new SourceEditor({
+      lineNumbers: true,
+      mode: text.length < SYNTAX_HIGHLIGHT_MAX_SIZE ? mode : null,
+      readOnly: true,
+      value: text,
+    });
+
+    this.deferEditor = this.editor.appendTo(this.refs.editorElement);
+  },
+
+  componentDidUpdate(prevProps) {
+    const { mode, open, text } = this.props;
+
+    if (!open) {
+      return;
+    }
+
+    if (prevProps.mode !== mode && text.length < SYNTAX_HIGHLIGHT_MAX_SIZE) {
+      this.deferEditor.then(() => {
+        this.editor.setMode(mode);
+      });
+    }
+
+    if (prevProps.text !== text) {
+      this.deferEditor.then(() => {
+        // FIXME: Workaround for browser_net_accessibility test to
+        // make sure editor node exists while setting editor text.
+        // deferEditor workaround should be removed in bug 1308442
+        if (this.refs.editorElement) {
+          this.editor.setText(text);
+        }
+      });
+    }
+  },
+
+  componentWillUnmount() {
+    this.deferEditor.then(() => {
+      this.editor.destroy();
+      this.editor = null;
+    });
+    this.deferEditor = null;
+  },
+
+  render() {
+    const { open } = this.props;
+
+    return (
+      div({ className: "editor-container devtools-monospace" },
+        div({
+          ref: "editorElement",
+          className: "editor-mount devtools-monospace",
+          // Using visibility instead of display property to avoid breaking
+          // CodeMirror indentation
+          style: { visibility: open ? "visible" : "hidden" },
+        }),
+      )
+    );
+  }
+});
+
+module.exports = Editor;
+
+/* eslint-enable react/prop-types */
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/HeadersPanel.js
@@ -0,0 +1,260 @@
+/* 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,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { getFormattedSize } = require("../utils/format-utils");
+const { L10N } = require("../utils/l10n");
+const {
+  getHeadersURL,
+  getHTTPStatusCodeURL,
+} = require("../utils/mdn-utils");
+const { writeHeaderText } = require("../utils/request-utils");
+
+// Components
+const { REPS, MODE } = require("devtools/client/shared/components/reps/reps");
+const MDNLink = createFactory(require("./MDNLink"));
+const PropertiesView = createFactory(require("./PropertiesView"));
+
+const Rep = createFactory(REPS.Rep);
+const { button, div, input, textarea } = DOM;
+
+const EDIT_AND_RESEND = L10N.getStr("netmonitor.summary.editAndResend");
+const RAW_HEADERS = L10N.getStr("netmonitor.summary.rawHeaders");
+const RAW_HEADERS_REQUEST = L10N.getStr("netmonitor.summary.rawHeaders.requestHeaders");
+const RAW_HEADERS_RESPONSE = L10N.getStr("netmonitor.summary.rawHeaders.responseHeaders");
+const HEADERS_EMPTY_TEXT = L10N.getStr("headersEmptyText");
+const HEADERS_FILTER_TEXT = L10N.getStr("headersFilterText");
+const REQUEST_HEADERS = L10N.getStr("requestHeaders");
+const REQUEST_HEADERS_FROM_UPLOAD = L10N.getStr("requestHeadersFromUpload");
+const RESPONSE_HEADERS = L10N.getStr("responseHeaders");
+const SUMMARY_ADDRESS = L10N.getStr("netmonitor.summary.address");
+const SUMMARY_METHOD = L10N.getStr("netmonitor.summary.method");
+const SUMMARY_URL = L10N.getStr("netmonitor.summary.url");
+const SUMMARY_STATUS = L10N.getStr("netmonitor.summary.status");
+const SUMMARY_VERSION = L10N.getStr("netmonitor.summary.version");
+
+/*
+ * Headers panel component
+ * Lists basic information about the request
+ */
+const HeadersPanel = createClass({
+  displayName: "HeadersPanel",
+
+  propTypes: {
+    cloneSelectedRequest: PropTypes.func.isRequired,
+    request: PropTypes.object.isRequired,
+    renderValue: PropTypes.func
+  },
+
+  getInitialState() {
+    return {
+      rawHeadersOpened: false,
+    };
+  },
+
+  getProperties(headers, title) {
+    if (headers && headers.headers.length) {
+      return {
+        [`${title} (${getFormattedSize(headers.headersSize, 3)})`]:
+          headers.headers.reduce((acc, { name, value }) =>
+            name ? Object.assign(acc, { [name]: value }) : acc
+          , {})
+      };
+    }
+
+    return null;
+  },
+
+  toggleRawHeaders() {
+    this.setState({
+      rawHeadersOpened: !this.state.rawHeadersOpened,
+    });
+  },
+
+  renderSummary(label, value) {
+    return (
+      div({ className: "tabpanel-summary-container headers-summary" },
+        div({
+          className: "tabpanel-summary-label headers-summary-label",
+        }, label),
+        input({
+          className: "tabpanel-summary-value textbox-input devtools-monospace",
+          readOnly: true,
+          value,
+        }),
+      )
+    );
+  },
+
+  renderValue(props) {
+    const member = props.member;
+    const value = props.value;
+
+    if (typeof value !== "string") {
+      return null;
+    }
+
+    let headerDocURL = getHeadersURL(member.name);
+
+    return (
+      div({ className: "treeValueCellDivider" },
+        Rep(Object.assign(props, {
+          // FIXME: A workaround for the issue in StringRep
+          // Force StringRep to crop the text everytime
+          member: Object.assign({}, member, { open: false }),
+          mode: MODE.TINY,
+          cropLimit: 60,
+        })),
+        headerDocURL ? MDNLink({
+          url: headerDocURL,
+        }) : null
+      )
+    );
+  },
+
+  render() {
+    const {
+      cloneSelectedRequest,
+      request: {
+        fromCache,
+        fromServiceWorker,
+        httpVersion,
+        method,
+        remoteAddress,
+        remotePort,
+        requestHeaders,
+        requestHeadersFromUploadStream: uploadHeaders,
+        responseHeaders,
+        status,
+        statusText,
+        urlDetails,
+      },
+    } = this.props;
+
+    if ((!requestHeaders || !requestHeaders.headers.length) &&
+        (!uploadHeaders || !uploadHeaders.headers.length) &&
+        (!responseHeaders || !responseHeaders.headers.length)) {
+      return div({ className: "empty-notice" },
+        HEADERS_EMPTY_TEXT
+      );
+    }
+
+    let object = Object.assign({},
+      this.getProperties(responseHeaders, RESPONSE_HEADERS),
+      this.getProperties(requestHeaders, REQUEST_HEADERS),
+      this.getProperties(uploadHeaders, REQUEST_HEADERS_FROM_UPLOAD),
+    );
+
+    let summaryUrl = urlDetails.unicodeUrl ?
+      this.renderSummary(SUMMARY_URL, urlDetails.unicodeUrl) : null;
+
+    let summaryMethod = method ?
+      this.renderSummary(SUMMARY_METHOD, method) : null;
+
+    let summaryAddress = remoteAddress ?
+      this.renderSummary(SUMMARY_ADDRESS,
+        remotePort ? `${remoteAddress}:${remotePort}` : remoteAddress) : null;
+
+    let summaryStatus;
+
+    if (status) {
+      let code;
+      if (fromCache) {
+        code = "cached";
+      } else if (fromServiceWorker) {
+        code = "service worker";
+      } else {
+        code = status;
+      }
+
+      let statusCodeDocURL = getHTTPStatusCodeURL(code);
+      let inputWidth = status.length + statusText.length + 1;
+
+      summaryStatus = (
+        div({ className: "tabpanel-summary-container headers-summary" },
+          div({
+            className: "tabpanel-summary-label headers-summary-label",
+          }, SUMMARY_STATUS),
+          div({
+            className: "requests-list-status-icon",
+            "data-code": code,
+          }),
+          input({
+            className: "tabpanel-summary-value textbox-input devtools-monospace"
+              + " status-text",
+            readOnly: true,
+            value: `${status} ${statusText}`,
+            size: `${inputWidth}`,
+          }),
+          statusCodeDocURL ? MDNLink({
+            url: statusCodeDocURL,
+          }) : null,
+          window.NetMonitorController.supportsCustomRequest && button({
+            className: "devtools-button",
+            onClick: cloneSelectedRequest,
+          }, EDIT_AND_RESEND),
+          button({
+            className: "devtools-button",
+            onClick: this.toggleRawHeaders,
+          }, RAW_HEADERS),
+        )
+      );
+    }
+
+    let summaryVersion = httpVersion ?
+      this.renderSummary(SUMMARY_VERSION, httpVersion) : null;
+
+    let summaryRawHeaders;
+    if (this.state.rawHeadersOpened) {
+      summaryRawHeaders = (
+        div({ className: "tabpanel-summary-container headers-summary" },
+          div({ className: "raw-headers-container" },
+            div({ className: "raw-headers" },
+              div({ className: "tabpanel-summary-label" }, RAW_HEADERS_REQUEST),
+              textarea({
+                value: writeHeaderText(requestHeaders.headers),
+                readOnly: true,
+              }),
+            ),
+            div({ className: "raw-headers" },
+              div({ className: "tabpanel-summary-label" }, RAW_HEADERS_RESPONSE),
+              textarea({
+                value: writeHeaderText(responseHeaders.headers),
+                readOnly: true,
+              }),
+            ),
+          )
+        )
+      );
+    }
+
+    return (
+      div({ className: "panel-container" },
+        div({ className: "headers-overview" },
+          summaryUrl,
+          summaryMethod,
+          summaryAddress,
+          summaryStatus,
+          summaryVersion,
+          summaryRawHeaders,
+        ),
+        PropertiesView({
+          object,
+          filterPlaceHolder: HEADERS_FILTER_TEXT,
+          sectionNames: Object.keys(object),
+          renderValue: this.renderValue,
+        }),
+      )
+    );
+  }
+});
+
+module.exports = HeadersPanel;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/MDNLink.js
@@ -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/. */
+
+"use strict";
+
+const Services = require("Services");
+const {
+  DOM,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { gDevTools } = require("devtools/client/framework/devtools");
+const { L10N } = require("../utils/l10n");
+
+const { a } = DOM;
+
+const LEARN_MORE = L10N.getStr("netmonitor.headers.learnMore");
+
+function MDNLink({ url }) {
+  return (
+    a({
+      className: "learn-more-link",
+      title: url,
+      onClick: (e) => onLearnMoreClick(e, url),
+    }, `[${LEARN_MORE}]`)
+  );
+}
+
+MDNLink.displayName = "MDNLink";
+
+MDNLink.propTypes = {
+  url: PropTypes.string.isRequired,
+};
+
+function onLearnMoreClick(e, url) {
+  e.stopPropagation();
+  e.preventDefault();
+
+  let win = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
+  if (e.button === 1) {
+    win.openUILinkIn(url, "tabshifted");
+  } else {
+    win.openUILinkIn(url, "tab");
+  }
+}
+
+module.exports = MDNLink;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/MonitorPanel.js
@@ -0,0 +1,135 @@
+/* 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,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+const { findDOMNode } = require("devtools/client/shared/vendor/react-dom");
+const Actions = require("../actions/index");
+const { getLongString } = require("../utils/client");
+const { Prefs } = require("../utils/prefs");
+const { getFormDataSections } = require("../utils/request-utils");
+const { getSelectedRequest } = require("../selectors/index");
+
+// Components
+const SplitBox = createFactory(require("devtools/client/shared/components/splitter/split-box"));
+const NetworkDetailsPanel = createFactory(require("./NetworkDetailsPanel"));
+const RequestList = createFactory(require("./RequestList"));
+const Toolbar = createFactory(require("./Toolbar"));
+
+const { div } = DOM;
+const MediaQueryList = window.matchMedia("(min-width: 700px)");
+
+/*
+ * Monitor panel component
+ * The main panel for displaying various network request information
+ */
+const MonitorPanel = createClass({
+  displayName: "MonitorPanel",
+
+  propTypes: {
+    isEmpty: PropTypes.bool.isRequired,
+    networkDetailsOpen: PropTypes.bool.isRequired,
+    openNetworkDetails: PropTypes.func.isRequired,
+    request: PropTypes.object,
+    updateRequest: PropTypes.func.isRequired,
+  },
+
+  getInitialState() {
+    return {
+      isVerticalSpliter: MediaQueryList.matches,
+    };
+  },
+
+  componentDidMount() {
+    MediaQueryList.addListener(this.onLayoutChange);
+  },
+
+  componentWillReceiveProps(nextProps) {
+    let {
+      request = {},
+      updateRequest,
+    } = nextProps;
+    let {
+      formDataSections,
+      requestHeaders,
+      requestHeadersFromUploadStream,
+      requestPostData,
+    } = request;
+
+    if (!formDataSections && requestHeaders &&
+        requestHeadersFromUploadStream && requestPostData) {
+      getFormDataSections(
+        requestHeaders,
+        requestHeadersFromUploadStream,
+        requestPostData,
+        getLongString,
+      ).then((newFormDataSections) => {
+        updateRequest(
+          request.id,
+          { formDataSections: newFormDataSections },
+          true,
+        );
+      });
+    }
+  },
+
+  componentWillUnmount() {
+    MediaQueryList.removeListener(this.onLayoutChange);
+
+    let { clientWidth, clientHeight } = findDOMNode(this.refs.endPanel) || {};
+
+    if (this.state.isVerticalSpliter && clientWidth) {
+      Prefs.networkDetailsWidth = clientWidth;
+    }
+    if (!this.state.isVerticalSpliter && clientHeight) {
+      Prefs.networkDetailsHeight = clientHeight;
+    }
+  },
+
+  onLayoutChange() {
+    this.setState({
+      isVerticalSpliter: MediaQueryList.matches,
+    });
+  },
+
+  render() {
+    let { isEmpty, networkDetailsOpen } = this.props;
+    return (
+      div({ className: "monitor-panel" },
+        Toolbar(),
+        SplitBox({
+          className: "devtools-responsive-container",
+          initialWidth: `${Prefs.networkDetailsWidth}px`,
+          initialHeight: `${Prefs.networkDetailsHeight}px`,
+          minSize: "50px",
+          maxSize: "80%",
+          splitterSize: "1px",
+          startPanel: RequestList({ isEmpty }),
+          endPanel: networkDetailsOpen && NetworkDetailsPanel({ ref: "endPanel" }),
+          endPanelControl: true,
+          vert: this.state.isVerticalSpliter,
+        }),
+      )
+    );
+  }
+});
+
+module.exports = connect(
+  (state) => ({
+    isEmpty: state.requests.requests.isEmpty(),
+    networkDetailsOpen: state.ui.networkDetailsOpen,
+    request: getSelectedRequest(state),
+  }),
+  (dispatch) => ({
+    openNetworkDetails: (open) => dispatch(Actions.openNetworkDetails(open)),
+    updateRequest: (id, data, batch) => dispatch(Actions.updateRequest(id, data, batch)),
+  }),
+)(MonitorPanel);
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/NetworkDetailsPanel.js
@@ -0,0 +1,70 @@
+/* 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,
+  DOM,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+const Actions = require("../actions/index");
+const { getSelectedRequest } = require("../selectors/index");
+
+// Components
+const CustomRequestPanel = createFactory(require("./CustomRequestPanel"));
+const TabboxPanel = createFactory(require("./TabboxPanel"));
+
+const { div } = DOM;
+
+/*
+ * Network details panel component
+ */
+function NetworkDetailsPanel({
+  activeTabId,
+  cloneSelectedRequest,
+  request,
+  selectTab,
+}) {
+  if (!request) {
+    return null;
+  }
+
+  return (
+    div({ className: "network-details-panel" },
+      !request.isCustom ?
+        TabboxPanel({
+          activeTabId,
+          request,
+          selectTab,
+        }) :
+        CustomRequestPanel({
+          cloneSelectedRequest,
+          request,
+        })
+    )
+  );
+}
+
+NetworkDetailsPanel.displayName = "NetworkDetailsPanel";
+
+NetworkDetailsPanel.propTypes = {
+  activeTabId: PropTypes.string,
+  cloneSelectedRequest: PropTypes.func.isRequired,
+  open: PropTypes.bool,
+  request: PropTypes.object,
+  selectTab: PropTypes.func.isRequired,
+};
+
+module.exports = connect(
+  (state) => ({
+    activeTabId: state.ui.detailsPanelSelectedTab,
+    request: getSelectedRequest(state),
+  }),
+  (dispatch) => ({
+    cloneSelectedRequest: () => dispatch(Actions.cloneSelectedRequest()),
+    selectTab: (tabId) => dispatch(Actions.selectDetailsPanelTab(tabId)),
+  }),
+)(NetworkDetailsPanel);
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/ParamsPanel.js
@@ -0,0 +1,130 @@
+/* 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,
+  DOM,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { L10N } = require("../utils/l10n");
+const { getUrlQuery, parseQueryString } = require("../utils/request-utils");
+
+// Components
+const PropertiesView = createFactory(require("./PropertiesView"));
+
+const { div } = DOM;
+
+const JSON_SCOPE_NAME = L10N.getStr("jsonScopeName");
+const PARAMS_EMPTY_TEXT = L10N.getStr("paramsEmptyText");
+const PARAMS_FILTER_TEXT = L10N.getStr("paramsFilterText");
+const PARAMS_FORM_DATA = L10N.getStr("paramsFormData");
+const PARAMS_POST_PAYLOAD = L10N.getStr("paramsPostPayload");
+const PARAMS_QUERY_STRING = L10N.getStr("paramsQueryString");
+const SECTION_NAMES = [
+  JSON_SCOPE_NAME,
+  PARAMS_FORM_DATA,
+  PARAMS_POST_PAYLOAD,
+  PARAMS_QUERY_STRING,
+];
+
+/*
+ * Params panel component
+ * Displays the GET parameters and POST data of a request
+ */
+function ParamsPanel({ request }) {
+  let {
+    formDataSections,
+    mimeType,
+    requestPostData,
+    url,
+  } = request;
+  let postData = requestPostData ? requestPostData.postData.text : null;
+  let query = getUrlQuery(url);
+
+  if (!formDataSections && !postData && !query) {
+    return div({ className: "empty-notice" },
+      PARAMS_EMPTY_TEXT
+    );
+  }
+
+  let object = {};
+  let json;
+
+  // Query String section
+  if (query) {
+    object[PARAMS_QUERY_STRING] = getProperties(parseQueryString(query));
+  }
+
+  // Form Data section
+  if (formDataSections && formDataSections.length > 0) {
+    let sections = formDataSections.filter((str) => /\S/.test(str)).join("&");
+    object[PARAMS_FORM_DATA] = getProperties(parseQueryString(sections));
+  }
+
+  // Request payload section
+  if (formDataSections && formDataSections.length === 0 && postData) {
+    try {
+      json = JSON.parse(postData);
+    } catch (error) {
+      // Continue regardless of parsing error
+    }
+
+    if (json) {
+      object[JSON_SCOPE_NAME] = json;
+    } else {
+      object[PARAMS_POST_PAYLOAD] = {
+        EDITOR_CONFIG: {
+          text: postData,
+          mode: mimeType.replace(/;.+/, ""),
+        },
+      };
+    }
+  } else {
+    postData = "";
+  }
+
+  return (
+    div({ className: "panel-container" },
+      PropertiesView({
+        object,
+        filterPlaceHolder: PARAMS_FILTER_TEXT,
+        sectionNames: SECTION_NAMES,
+      })
+    )
+  );
+}
+
+ParamsPanel.displayName = "ParamsPanel";
+
+ParamsPanel.propTypes = {
+  request: PropTypes.object.isRequired,
+};
+
+/**
+ * Mapping array to dict for TreeView usage.
+ * Since TreeView only support Object(dict) format.
+ * This function also deal with duplicate key case
+ * (for multiple selection and query params with same keys)
+ *
+ * @param {Object[]} arr - key-value pair array like query or form params
+ * @returns {Object} Rep compatible object
+ */
+function getProperties(arr) {
+  return arr.reduce((map, obj) => {
+    let value = map[obj.name];
+    if (value) {
+      if (typeof value !== "object") {
+        map[obj.name] = [value];
+      }
+      map[obj.name].push(obj.value);
+    } else {
+      map[obj.name] = obj.value;
+    }
+    return map;
+  }, {});
+}
+
+module.exports = ParamsPanel;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/PreviewPanel.js
@@ -0,0 +1,35 @@
+/* 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 { DOM, PropTypes } = require("devtools/client/shared/vendor/react");
+
+const { div, iframe } = DOM;
+
+/*
+ * Preview panel component
+ * Display HTML content within a sandbox enabled iframe
+ */
+function PreviewPanel({ request }) {
+  const htmlBody = request.responseContent ?
+    request.responseContent.content.text : "";
+
+  return (
+    div({ className: "panel-container" },
+      iframe({
+        sandbox: "",
+        srcDoc: typeof htmlBody === "string" ? htmlBody : "",
+      })
+    )
+  );
+}
+
+PreviewPanel.displayName = "PreviewPanel";
+
+PreviewPanel.propTypes = {
+  request: PropTypes.object.isRequired,
+};
+
+module.exports = PreviewPanel;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/PropertiesView.js
@@ -0,0 +1,218 @@
+/* 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-disable react/prop-types */
+
+"use strict";
+
+const {
+  createClass,
+  createFactory,
+  DOM,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+
+const { REPS, MODE } = require("devtools/client/shared/components/reps/reps");
+const Rep = createFactory(REPS.Rep);
+
+const { FILTER_SEARCH_DELAY } = require("../constants");
+
+// Components
+const SearchBox = createFactory(require("devtools/client/shared/components/search-box"));
+const TreeView = createFactory(require("devtools/client/shared/components/tree/tree-view"));
+const TreeRow = createFactory(require("devtools/client/shared/components/tree/tree-row"));
+const Editor = createFactory(require("./Editor"));
+
+const { div, tr, td } = DOM;
+const AUTO_EXPAND_MAX_LEVEL = 7;
+const AUTO_EXPAND_MAX_NODES = 50;
+const EDITOR_CONFIG_ID = "EDITOR_CONFIG";
+
+/*
+ * Properties View component
+ * A scrollable tree view component which provides some useful features for
+ * representing object properties.
+ *
+ * Search filter - Set enableFilter to enable / disable SearchBox feature.
+ * Tree view - Default enabled.
+ * Source editor - Enable by specifying object level 1 property name to EDITOR_CONFIG_ID.
+ * Rep - Default enabled.
+ */
+const PropertiesView = createClass({
+  displayName: "PropertiesView",
+
+  propTypes: {
+    object: PropTypes.object,
+    enableInput: PropTypes.bool,
+    expandableStrings: PropTypes.bool,
+    filterPlaceHolder: PropTypes.string,
+    sectionNames: PropTypes.array,
+  },
+
+  getDefaultProps() {
+    return {
+      enableInput: true,
+      enableFilter: true,
+      expandableStrings: false,
+      filterPlaceHolder: "",
+      sectionNames: [],
+    };
+  },
+
+  getInitialState() {
+    return {
+      filterText: "",
+    };
+  },
+
+  getRowClass(object, sectionNames) {
+    return sectionNames.includes(object.name) ? "tree-section" : "";
+  },
+
+  onFilter(object, whiteList) {
+    let { name, value } = object;
+    let filterText = this.state.filterText;
+
+    if (!filterText || whiteList.includes(name)) {
+      return true;
+    }
+
+    let jsonString = JSON.stringify({ [name]: value }).toLowerCase();
+    return jsonString.includes(filterText.toLowerCase());
+  },
+
+  renderRowWithEditor(props) {
+    const { level, name, value, path } = props.member;
+
+    // Display source editor when specifying to EDITOR_CONFIG_ID along with config
+    if (level === 1 && name === EDITOR_CONFIG_ID) {
+      return (
+        tr({ className: "editor-row-container" },
+          td({ colSpan: 2 },
+            Editor(value)
+          )
+        )
+      );
+    }
+
+    // Skip for editor config
+    if (level >= 1 && path.includes(EDITOR_CONFIG_ID)) {
+      return null;
+    }
+
+    return TreeRow(props);
+  },
+
+  renderValueWithRep(props) {
+    const { member } = props;
+
+    // Hide strings with following conditions
+    // 1. this row is a togglable section and content is object ('cause it shouldn't hide
+    //    when string or number)
+    // 2. the `value` object has a `value` property, only happened in Cookies panel
+    // Put 2 here to not dup this method
+    if (member.level === 0 && member.type === "object" ||
+      (typeof member.value === "object" && member.value && member.value.value)) {
+      return null;
+    }
+
+    return Rep(Object.assign(props, {
+      // FIXME: A workaround for the issue in StringRep
+      // Force StringRep to crop the text everytime
+      member: Object.assign({}, member, { open: false }),
+      mode: MODE.TINY,
+      cropLimit: 60,
+    }));
+  },
+
+  shouldRenderSearchBox(object) {
+    return this.props.enableFilter && object && Object.keys(object)
+      .filter((section) => !object[section][EDITOR_CONFIG_ID]).length > 0;
+  },
+
+  updateFilterText(filterText) {
+    this.setState({
+      filterText,
+    });
+  },
+
+  getExpandedNodes: function (object, path = "", level = 0) {
+    if (typeof object != "object") {
+      return null;
+    }
+
+    if (level > AUTO_EXPAND_MAX_LEVEL) {
+      return null;
+    }
+
+    let expandedNodes = new Set();
+    for (let prop in object) {
+      if (expandedNodes.size > AUTO_EXPAND_MAX_NODES) {
+        // If we reached the limit of expandable nodes, bail out to avoid performance
+        // issues.
+        break;
+      }
+
+      let nodePath = path + "/" + prop;
+      expandedNodes.add(nodePath);
+
+      let nodes = this.getExpandedNodes(object[prop], nodePath, level + 1);
+      if (nodes) {
+        let newSize = expandedNodes.size + nodes.size;
+        if (newSize < AUTO_EXPAND_MAX_NODES) {
+          // Avoid having a subtree half expanded.
+          expandedNodes = new Set([...expandedNodes, ...nodes]);
+        }
+      }
+    }
+    return expandedNodes;
+  },
+
+  render() {
+    const {
+      decorator,
+      enableInput,
+      expandableStrings,
+      filterPlaceHolder,
+      object,
+      renderRow,
+      renderValue,
+      sectionNames,
+    } = this.props;
+
+    return (
+      div({ className: "properties-view" },
+        this.shouldRenderSearchBox(object) &&
+          div({ className: "searchbox-section" },
+            SearchBox({
+              delay: FILTER_SEARCH_DELAY,
+              type: "filter",
+              onChange: this.updateFilterText,
+              placeholder: filterPlaceHolder,
+            }),
+          ),
+        div({ className: "tree-container" },
+          TreeView({
+            object,
+            columns: [{
+              id: "value",
+              width: "100%",
+            }],
+            decorator: decorator || {
+              getRowClass: (rowObject) => this.getRowClass(rowObject, sectionNames),
+            },
+            enableInput,
+            expandableStrings,
+            expandedNodes: this.getExpandedNodes(object),
+            onFilter: (props) => this.onFilter(props, sectionNames),
+            renderRow: renderRow || this.renderRowWithEditor,
+            renderValue: renderValue || this.renderValueWithRep,
+          }),
+        ),
+      )
+    );
+  }
+});
+
+module.exports = PropertiesView;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/RequestList.js
@@ -0,0 +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 {
+  createFactory,
+  DOM,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+
+// Components
+const RequestListContent = createFactory(require("./RequestListContent"));
+const RequestListEmptyNotice = createFactory(require("./RequestListEmptyNotice"));
+const RequestListHeader = createFactory(require("./RequestListHeader"));
+
+const { div } = DOM;
+
+/**
+ * Request panel component
+ */
+function RequestList({ isEmpty }) {
+  return (
+    div({ className: "request-list-container" },
+      RequestListHeader(),
+      isEmpty ? RequestListEmptyNotice() : RequestListContent(),
+    )
+  );
+}
+
+RequestList.displayName = "RequestList";
+
+RequestList.propTypes = {
+  isEmpty: PropTypes.bool.isRequired,
+};
+
+module.exports = RequestList;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/RequestListContent.js
@@ -0,0 +1,280 @@
+/* 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 { KeyCodes } = require("devtools/client/shared/keycodes");
+const {
+  createClass,
+  createFactory,
+  DOM,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+const { HTMLTooltip } = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
+const Actions = require("../actions/index");
+const {
+  setTooltipImageContent,
+  setTooltipStackTraceContent,
+} = require("../request-list-tooltip");
+const {
+  getDisplayedRequests,
+  getWaterfallScale,
+} = require("../selectors/index");
+
+// Components
+const RequestListItem = createFactory(require("./RequestListItem"));
+const RequestListContextMenu = require("../request-list-context-menu");
+
+const { div } = DOM;
+
+// tooltip show/hide delay in ms
+const REQUESTS_TOOLTIP_TOGGLE_DELAY = 500;
+
+/**
+ * Renders the actual contents of the request list.
+ */
+const RequestListContent = createClass({
+  displayName: "RequestListContent",
+
+  propTypes: {
+    dispatch: PropTypes.func.isRequired,
+    displayedRequests: PropTypes.object.isRequired,
+    firstRequestStartedMillis: PropTypes.number.isRequired,
+    fromCache: PropTypes.bool.isRequired,
+    onItemMouseDown: PropTypes.func.isRequired,
+    onSecurityIconClick: PropTypes.func.isRequired,
+    onSelectDelta: PropTypes.func.isRequired,
+    scale: PropTypes.number,
+    selectedRequestId: PropTypes.string,
+  },
+
+  componentWillMount() {
+    const { dispatch } = this.props;
+    this.contextMenu = new RequestListContextMenu({
+      cloneSelectedRequest: () => dispatch(Actions.cloneSelectedRequest()),
+      openStatistics: (open) => dispatch(Actions.openStatistics(open)),
+    });
+    this.tooltip = new HTMLTooltip(window.parent.document, { type: "arrow" });
+  },
+
+  componentDidMount() {
+    // Set the CSS variables for waterfall scaling
+    this.setScalingStyles();
+
+    // Install event handler for displaying a tooltip
+    this.tooltip.startTogglingOnHover(this.refs.contentEl, this.onHover, {
+      toggleDelay: REQUESTS_TOOLTIP_TOGGLE_DELAY,
+      interactive: true
+    });
+
+    // Install event handler to hide the tooltip on scroll
+    this.refs.contentEl.addEventListener("scroll", this.onScroll, true);
+  },
+
+  componentWillUpdate(nextProps) {
+    // Check if the list is scrolled to bottom before the UI update.
+    // The scroll is ever needed only if new rows are added to the list.
+    const delta = nextProps.displayedRequests.size - this.props.displayedRequests.size;
+    this.shouldScrollBottom = delta > 0 && this.isScrolledToBottom();
+  },
+
+  componentDidUpdate(prevProps) {
+    // Update the CSS variables for waterfall scaling after props change
+    this.setScalingStyles(prevProps);
+
+    // Keep the list scrolled to bottom if a new row was added
+    if (this.shouldScrollBottom) {
+      let node = this.refs.contentEl;
+      node.scrollTop = node.scrollHeight;
+    }
+  },
+
+  componentWillUnmount() {
+    this.refs.contentEl.removeEventListener("scroll", this.onScroll, true);
+
+    // Uninstall the tooltip event handler
+    this.tooltip.stopTogglingOnHover();
+  },
+
+  /**
+   * Set the CSS variables for waterfall scaling. If React supported setting CSS
+   * variables as part of the "style" property of a DOM element, we would use that.
+   *
+   * However, React doesn't support this, so we need to use a hack and update the
+   * DOM element directly: https://github.com/facebook/react/issues/6411
+   */
+  setScalingStyles(prevProps) {
+    const { scale } = this.props;
+    if (prevProps && prevProps.scale === scale) {
+      return;
+    }
+
+    const { style } = this.refs.contentEl;
+    style.removeProperty("--timings-scale");
+    style.removeProperty("--timings-rev-scale");
+    style.setProperty("--timings-scale", scale);
+    style.setProperty("--timings-rev-scale", 1 / scale);
+  },
+
+  isScrolledToBottom() {
+    const { contentEl } = this.refs;
+    const lastChildEl = contentEl.lastElementChild;
+
+    if (!lastChildEl) {
+      return false;
+    }
+
+    let lastChildRect = lastChildEl.getBoundingClientRect();
+    let contentRect = contentEl.getBoundingClientRect();
+
+    return (lastChildRect.height + lastChildRect.top) <= contentRect.bottom;
+  },
+
+  /**
+   * The predicate used when deciding whether a popup should be shown
+   * over a request item or not.
+   *
+   * @param nsIDOMNode target
+   *        The element node currently being hovered.
+   * @param object tooltip
+   *        The current tooltip instance.
+   * @return {Promise}
+   */
+  onHover(target, tooltip) {
+    let itemEl = target.closest(".request-list-item");
+    if (!itemEl) {
+      return false;
+    }
+    let itemId = itemEl.dataset.id;
+    if (!itemId) {
+      return false;
+    }
+    let requestItem = this.props.displayedRequests.find(r => r.id == itemId);
+    if (!requestItem) {
+      return false;
+    }
+
+    if (requestItem.responseContent && target.closest(".requests-list-icon-and-file")) {
+      return setTooltipImageContent(tooltip, itemEl, requestItem);
+    } else if (requestItem.cause && target.closest(".requests-list-cause-stack")) {
+      return setTooltipStackTraceContent(tooltip, requestItem);
+    }
+
+    return false;
+  },
+
+  /**
+   * Scroll listener for the requests menu view.
+   */
+  onScroll() {
+    this.tooltip.hide();
+  },
+
+  /**
+   * Handler for keyboard events. For arrow up/down, page up/down, home/end,
+   * move the selection up or down.
+   */
+  onKeyDown(e) {
+    let delta;
+
+    switch (e.keyCode) {
+      case KeyCodes.DOM_VK_UP:
+      case KeyCodes.DOM_VK_LEFT:
+        delta = -1;
+        break;
+      case KeyCodes.DOM_VK_DOWN:
+      case KeyCodes.DOM_VK_RIGHT:
+        delta = +1;
+        break;
+      case KeyCodes.DOM_VK_PAGE_UP:
+        delta = "PAGE_UP";
+        break;
+      case KeyCodes.DOM_VK_PAGE_DOWN:
+        delta = "PAGE_DOWN";
+        break;
+      case KeyCodes.DOM_VK_HOME:
+        delta = -Infinity;
+        break;
+      case KeyCodes.DOM_VK_END:
+        delta = +Infinity;
+        break;
+    }
+
+    if (delta) {
+      // Prevent scrolling when pressing navigation keys.
+      e.preventDefault();
+      e.stopPropagation();
+      this.props.onSelectDelta(delta);
+    }
+  },
+
+  onContextMenu(evt) {
+    evt.preventDefault();
+    this.contextMenu.open(evt);
+  },
+
+  /**
+   * If selection has just changed (by keyboard navigation), don't keep the list
+   * scrolled to bottom, but allow scrolling up with the selection.
+   */
+  onFocusedNodeChange() {
+    this.shouldScrollBottom = false;
+  },
+
+  render() {
+    const {
+      displayedRequests,
+      firstRequestStartedMillis,
+      selectedRequestId,
+      onItemMouseDown,
+      onSecurityIconClick,
+    } = this.props;
+
+    return (
+      div({
+        ref: "contentEl",
+        className: "requests-list-contents",
+        tabIndex: 0,
+        onKeyDown: this.onKeyDown,
+      },
+        displayedRequests.map((item, index) => RequestListItem({
+          firstRequestStartedMillis,
+          fromCache: item.status === "304" || item.fromCache,
+          item,
+          index,
+          isSelected: item.id === selectedRequestId,
+          key: item.id,
+          onContextMenu: this.onContextMenu,
+          onFocusedNodeChange: this.onFocusedNodeChange,
+          onMouseDown: () => onItemMouseDown(item.id),
+          onSecurityIconClick: () => onSecurityIconClick(item.securityState),
+        }))
+      )
+    );
+  },
+});
+
+module.exports = connect(
+  (state) => ({
+    displayedRequests: getDisplayedRequests(state),
+    firstRequestStartedMillis: state.requests.firstStartedMillis,
+    selectedRequestId: state.requests.selectedId,
+    scale: getWaterfallScale(state),
+  }),
+  (dispatch) => ({
+    dispatch,
+    onItemMouseDown: (id) => dispatch(Actions.selectRequest(id)),
+    /**
+     * A handler that opens the security tab in the details view if secure or
+     * broken security indicator is clicked.
+     */
+    onSecurityIconClick: (securityState) => {
+      if (securityState && securityState !== "insecure") {
+        dispatch(Actions.selectDetailsPanelTab("security"));
+      }
+    },
+    onSelectDelta: (delta) => dispatch(Actions.selectDelta(delta)),
+  }),
+)(RequestListContent);
rename from devtools/client/netmonitor/components/request-list-empty.js
rename to devtools/client/netmonitor/components/RequestListEmptyNotice.js
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/RequestListHeader.js
@@ -0,0 +1,203 @@
+/* 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,
+  PropTypes,
+  DOM,
+} = require("devtools/client/shared/vendor/react");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+const { setNamedTimeout } = require("devtools/client/shared/widgets/view-helpers");
+const Actions = require("../actions/index");
+const { getWaterfallScale } = require("../selectors/index");
+const { getFormattedTime } = require("../utils/format-utils");
+const { L10N } = require("../utils/l10n");
+const WaterfallBackground = require("../waterfall-background");
+
+const { div, button } = DOM;
+
+const REQUESTS_WATERFALL_HEADER_TICKS_MULTIPLE = 5; // ms
+const REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN = 60; // px
+
+const HEADERS = [
+  { name: "status", label: "status3" },
+  { name: "method" },
+  { name: "file", boxName: "icon-and-file" },
+  { name: "domain", boxName: "security-and-domain" },
+  { name: "cause" },
+  { name: "type" },
+  { name: "transferred" },
+  { name: "size" },
+  { name: "waterfall" }
+];
+
+/**
+ * Render the request list header with sorting arrows for columns.
+ * Displays tick marks in the waterfall column header.
+ * Also draws the waterfall background canvas and updates it when needed.
+ */
+const RequestListHeader = createClass({
+  displayName: "RequestListHeader",
+
+  propTypes: {
+    sort: PropTypes.object,
+    scale: PropTypes.number,
+    waterfallWidth: PropTypes.number,
+    onHeaderClick: PropTypes.func.isRequired,
+    resizeWaterfall: PropTypes.func.isRequired,
+  },
+
+  componentDidMount() {
+    // Create the object that takes care of drawing the waterfall canvas background
+    this.background = new WaterfallBackground(document);
+    this.background.draw(this.props);
+    this.resizeWaterfall();
+    window.addEventListener("resize", this.resizeWaterfall);
+  },
+
+  componentDidUpdate() {
+    this.background.draw(this.props);
+  },
+
+  componentWillUnmount() {
+    this.background.destroy();
+    this.background = null;
+    window.removeEventListener("resize", this.resizeWaterfall);
+  },
+
+  resizeWaterfall() {
+    // Measure its width and update the 'waterfallWidth' property in the store.
+    // The 'waterfallWidth' will be further updated on every window resize.
+    setNamedTimeout("resize-events", 50, () => {
+      const { width } = this.refs.header.getBoundingClientRect();
+      this.props.resizeWaterfall(width);
+    });
+  },
+
+  render() {
+    const { sort, scale, waterfallWidth, onHeaderClick } = this.props;
+
+    return div(
+      { className: "devtools-toolbar requests-list-toolbar" },
+      div({ className: "toolbar-labels" },
+        HEADERS.map(header => {
+          const name = header.name;
+          const boxName = header.boxName || name;
+          const label = L10N.getStr(`netmonitor.toolbar.${header.label || name}`);
+
+          let sorted, sortedTitle;
+          const active = sort.type == name ? true : undefined;
+          if (active) {
+            sorted = sort.ascending ? "ascending" : "descending";
+            sortedTitle = L10N.getStr(sort.ascending
+              ? "networkMenu.sortedAsc"
+              : "networkMenu.sortedDesc");
+          }
+
+          return div(
+            {
+              id: `requests-list-${boxName}-header-box`,
+              className: `requests-list-header requests-list-${boxName}`,
+              key: name,
+              ref: "header",
+              // Used to style the next column.
+              "data-active": active,
+            },
+            button(
+              {
+                id: `requests-list-${name}-button`,
+                className: `requests-list-header-button requests-list-${name}`,
+                "data-sorted": sorted,
+                title: sortedTitle,
+                onClick: () => onHeaderClick(name),
+              },
+              name == "waterfall" ? WaterfallLabel(waterfallWidth, scale, label)
+                                  : div({ className: "button-text" }, label),
+              div({ className: "button-icon" })
+            )
+          );
+        })
+      )
+    );
+  }
+});
+
+/**
+ * Build the waterfall header - timing tick marks with the right spacing
+ */
+function waterfallDivisionLabels(waterfallWidth, scale) {
+  let labels = [];
+
+  // Build new millisecond tick labels...
+  let timingStep = REQUESTS_WATERFALL_HEADER_TICKS_MULTIPLE;
+  let scaledStep = scale * timingStep;
+
+  // Ignore any divisions that would end up being too close to each other.
+  while (scaledStep < REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN) {
+    scaledStep *= 2;
+  }
+
+  // Insert one label for each division on the current scale.
+  for (let x = 0; x < waterfallWidth; x += scaledStep) {
+    let millisecondTime = x / scale;
+    let divisionScale = "millisecond";
+
+    // If the division is greater than 1 minute.
+    if (millisecondTime > 60000) {
+      divisionScale = "minute";
+    } else if (millisecondTime > 1000) {
+      // If the division is greater than 1 second.
+      divisionScale = "second";
+    }
+
+    let width = (x + scaledStep | 0) - (x | 0);
+    // Adjust the first marker for the borders
+    if (x == 0) {
+      width -= 2;
+    }
+    // Last marker doesn't need a width specified at all
+    if (x + scaledStep >= waterfallWidth) {
+      width = undefined;
+    }
+
+    labels.push(div(
+      {
+        key: labels.length,
+        className: "requests-list-timings-division",
+        "data-division-scale": divisionScale,
+        style: { width }
+      },
+      getFormattedTime(millisecondTime)
+    ));
+  }
+
+  return labels;
+}
+
+function WaterfallLabel(waterfallWidth, scale, label) {
+  let className = "button-text requests-list-waterfall-label-wrapper";
+
+  if (waterfallWidth != null && scale != null) {
+    label = waterfallDivisionLabels(waterfallWidth, scale);
+    className += " requests-list-waterfall-visible";
+  }
+
+  return div({ className }, label);
+}
+
+module.exports = connect(
+  state => ({
+    sort: state.sort,
+    scale: getWaterfallScale(state),
+    waterfallWidth: state.ui.waterfallWidth,
+    firstRequestStartedMillis: state.requests.firstStartedMillis,
+    timingMarkers: state.timingMarkers,
+  }),
+  dispatch => ({
+    onHeaderClick: type => dispatch(Actions.sortBy(type)),
+    resizeWaterfall: width => dispatch(Actions.resizeWaterfall(width)),
+  })
+)(RequestListHeader);
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/RequestListItem.js
@@ -0,0 +1,528 @@
+/* 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,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { getFormattedSize } = require("../utils/format-utils");
+const { L10N } = require("../utils/l10n");
+const { getAbbreviatedMimeType } = require("../utils/request-utils");
+
+const { div, img, span } = DOM;
+
+/**
+ * Compare two objects on a subset of their properties
+ */
+function propertiesEqual(props, item1, item2) {
+  return item1 === item2 || props.every(p => item1[p] === item2[p]);
+}
+
+/**
+ * Used by shouldComponentUpdate: compare two items, and compare only properties
+ * relevant for rendering the RequestListItem. Other properties (like request and
+ * response headers, cookies, bodies) are ignored. These are very useful for the
+ * network details, but not here.
+ */
+const UPDATED_REQ_ITEM_PROPS = [
+  "mimeType",
+  "eventTimings",
+  "securityState",
+  "responseContentDataUri",
+  "status",
+  "statusText",
+  "fromCache",
+  "fromServiceWorker",
+  "method",
+  "url",
+  "remoteAddress",
+  "cause",
+  "contentSize",
+  "transferredSize",
+  "startedMillis",
+  "totalTime",
+];
+
+const UPDATED_REQ_PROPS = [
+  "index",
+  "isSelected",
+  "firstRequestStartedMillis",
+];
+
+/**
+ * Render one row in the request list.
+ */
+const RequestListItem = createClass({
+  displayName: "RequestListItem",
+
+  propTypes: {
+    item: PropTypes.object.isRequired,
+    index: PropTypes.number.isRequired,
+    isSelected: PropTypes.bool.isRequired,
+    firstRequestStartedMillis: PropTypes.number.isRequired,
+    fromCache: PropTypes.bool.isRequired,
+    onContextMenu: PropTypes.func.isRequired,
+    onFocusedNodeChange: PropTypes.func,
+    onMouseDown: PropTypes.func.isRequired,
+    onSecurityIconClick: PropTypes.func.isRequired,
+  },
+
+  componentDidMount() {
+    if (this.props.isSelected) {
+      this.refs.el.focus();
+    }
+  },
+
+  shouldComponentUpdate(nextProps) {
+    return !propertiesEqual(UPDATED_REQ_ITEM_PROPS, this.props.item, nextProps.item) ||
+      !propertiesEqual(UPDATED_REQ_PROPS, this.props, nextProps);
+  },
+
+  componentDidUpdate(prevProps) {
+    if (!prevProps.isSelected && this.props.isSelected) {
+      this.refs.el.focus();
+      if (this.props.onFocusedNodeChange) {
+        this.props.onFocusedNodeChange();
+      }
+    }
+  },
+
+  render() {
+    const {
+      item,
+      index,
+      isSelected,
+      firstRequestStartedMillis,
+      fromCache,
+      onContextMenu,
+      onMouseDown,
+      onSecurityIconClick
+    } = this.props;
+
+    let classList = ["request-list-item"];
+    if (isSelected) {
+      classList.push("selected");
+    }
+
+    if (fromCache) {
+      classList.push("fromCache");
+    }
+
+    classList.push(index % 2 ? "odd" : "even");
+
+    return (
+      div({
+        ref: "el",
+        className: classList.join(" "),
+        "data-id": item.id,
+        tabIndex: 0,
+        onContextMenu,
+        onMouseDown,
+      },
+        StatusColumn({ item }),
+        MethodColumn({ item }),
+        FileColumn({ item }),
+        DomainColumn({ item, onSecurityIconClick }),
+        CauseColumn({ item }),
+        TypeColumn({ item }),
+        TransferredSizeColumn({ item }),
+        ContentSizeColumn({ item }),
+        WaterfallColumn({ item, firstRequestStartedMillis }),
+      )
+    );
+  }
+});
+
+const UPDATED_STATUS_PROPS = [
+  "status",
+  "statusText",
+  "fromCache",
+  "fromServiceWorker",
+];
+
+const StatusColumn = createFactory(createClass({
+  displayName: "StatusColumn",
+
+  propTypes: {
+    item: PropTypes.object.isRequired,
+  },
+
+  shouldComponentUpdate(nextProps) {
+    return !propertiesEqual(UPDATED_STATUS_PROPS, this.props.item, nextProps.item);
+  },
+
+  render() {
+    const { status, statusText, fromCache, fromServiceWorker } = this.props.item;
+
+    let code, title;
+
+    if (status) {
+      if (fromCache) {
+        code = "cached";
+      } else if (fromServiceWorker) {
+        code = "service worker";
+      } else {
+        code = status;
+      }
+
+      if (statusText) {
+        title = `${status} ${statusText}`;
+        if (fromCache) {
+          title += " (cached)";
+        }
+        if (fromServiceWorker) {
+          title += " (service worker)";
+        }
+      }
+    }
+
+    return (
+        div({ className: "requests-list-subitem requests-list-status", title },
+        div({ className: "requests-list-status-icon", "data-code": code }),
+        span({ className: "subitem-label requests-list-status-code" }, status)
+      )
+    );
+  }
+}));
+
+const MethodColumn = createFactory(createClass({
+  displayName: "MethodColumn",
+
+  propTypes: {
+    item: PropTypes.object.isRequired,
+  },
+
+  shouldComponentUpdate(nextProps) {
+    return this.props.item.method !== nextProps.item.method;
+  },
+
+  render() {
+    const { method } = this.props.item;
+    return (
+      div({ className: "requests-list-subitem requests-list-method-box" },
+        span({ className: "subitem-label requests-list-method" }, method)
+      )
+    );
+  }
+}));
+
+const UPDATED_FILE_PROPS = [
+  "urlDetails",
+  "responseContentDataUri",
+];
+
+const FileColumn = createFactory(createClass({
+  displayName: "FileColumn",
+
+  propTypes: {
+    item: PropTypes.object.isRequired,
+  },
+
+  shouldComponentUpdate(nextProps) {
+    return !propertiesEqual(UPDATED_FILE_PROPS, this.props.item, nextProps.item);
+  },
+
+  render() {
+    const { urlDetails, responseContentDataUri } = this.props.item;
+
+    return (
+      div({ className: "requests-list-subitem requests-list-icon-and-file" },
+        img({
+          className: "requests-list-icon",
+          src: responseContentDataUri,
+          hidden: !responseContentDataUri,
+          "data-type": responseContentDataUri ? "thumbnail" : undefined,
+        }),
+        div({
+          className: "subitem-label requests-list-file",
+          title: urlDetails.unicodeUrl,
+        },
+          urlDetails.baseNameWithQuery,
+        ),
+      )
+    );
+  }
+}));
+
+const UPDATED_DOMAIN_PROPS = [
+  "urlDetails",
+  "remoteAddress",
+  "securityState",
+];
+
+const DomainColumn = createFactory(createClass({
+  displayName: "DomainColumn",
+
+  propTypes: {
+    item: PropTypes.object.isRequired,
+    onSecurityIconClick: PropTypes.func.isRequired,
+  },
+
+  shouldComponentUpdate(nextProps) {
+    return !propertiesEqual(UPDATED_DOMAIN_PROPS, this.props.item, nextProps.item);
+  },
+
+  render() {
+    const { item, onSecurityIconClick } = this.props;
+    const { urlDetails, remoteAddress, securityState } = item;
+
+    let iconClassList = ["requests-security-state-icon"];
+    let iconTitle;
+    if (urlDetails.isLocal) {
+      iconClassList.push("security-state-local");
+      iconTitle = L10N.getStr("netmonitor.security.state.secure");
+    } else if (securityState) {
+      iconClassList.push(`security-state-${securityState}`);
+      iconTitle = L10N.getStr(`netmonitor.security.state.${securityState}`);
+    }
+
+    let title = urlDetails.host + (remoteAddress ? ` (${remoteAddress})` : "");
+
+    return (
+      div({ className: "requests-list-subitem requests-list-security-and-domain" },
+        div({
+          className: iconClassList.join(" "),
+          title: iconTitle,
+          onClick: onSecurityIconClick,
+        }),
+        span({ className: "subitem-label requests-list-domain", title }, urlDetails.host),
+      )
+    );
+  }
+}));
+
+const CauseColumn = createFactory(createClass({
+  displayName: "CauseColumn",
+
+  propTypes: {
+    item: PropTypes.object.isRequired,
+  },
+
+  shouldComponentUpdate(nextProps) {
+    return this.props.item.cause !== nextProps.item.cause;
+  },
+
+  render() {
+    const { cause } = this.props.item;
+
+    let causeType = "";
+    let causeUri = undefined;
+    let causeHasStack = false;
+
+    if (cause) {
+      // Legacy server might send a numeric value. Display it as "unknown"
+      causeType = typeof cause.type === "string" ? cause.type : "unknown";
+      causeUri = cause.loadingDocumentUri;
+      causeHasStack = cause.stacktrace && cause.stacktrace.length > 0;
+    }
+
+    return (
+      div({
+        className: "requests-list-subitem requests-list-cause",
+        title: causeUri,
+      },
+        span({
+          className: "requests-list-cause-stack",
+          hidden: !causeHasStack,
+        }, "JS"),
+        span({ className: "subitem-label" }, causeType),
+      )
+    );
+  }
+}));
+
+const CONTENT_MIME_TYPE_ABBREVIATIONS = {
+  "ecmascript": "js",
+  "javascript": "js",
+  "x-javascript": "js"
+};
+
+const TypeColumn = createFactory(createClass({
+  displayName: "TypeColumn",
+
+  propTypes: {
+    item: PropTypes.object.isRequired,
+  },
+
+  shouldComponentUpdate(nextProps) {
+    return this.props.item.mimeType !== nextProps.item.mimeType;
+  },
+
+  render() {
+    const { mimeType } = this.props.item;
+    let abbrevType;
+    if (mimeType) {
+      abbrevType = getAbbreviatedMimeType(mimeType);
+      abbrevType = CONTENT_MIME_TYPE_ABBREVIATIONS[abbrevType] || abbrevType;
+    }
+
+    return (
+      div({
+        className: "requests-list-subitem requests-list-type",
+        title: mimeType,
+      },
+        span({ className: "subitem-label" }, abbrevType),
+      )
+    );
+  }
+}));
+
+const UPDATED_TRANSFERRED_PROPS = [
+  "transferredSize",
+  "fromCache",
+  "fromServiceWorker",
+];
+
+const TransferredSizeColumn = createFactory(createClass({
+  displayName: "TransferredSizeColumn",
+
+  propTypes: {
+    item: PropTypes.object.isRequired,
+  },
+
+  shouldComponentUpdate(nextProps) {
+    return !propertiesEqual(UPDATED_TRANSFERRED_PROPS, this.props.item, nextProps.item);
+  },
+
+  render() {
+    const { transferredSize, fromCache, fromServiceWorker, status } = this.props.item;
+
+    let text;
+    let className = "subitem-label";
+    if (fromCache || status === "304") {
+      text = L10N.getStr("networkMenu.sizeCached");
+      className += " theme-comment";
+    } else if (fromServiceWorker) {
+      text = L10N.getStr("networkMenu.sizeServiceWorker");
+      className += " theme-comment";
+    } else if (typeof transferredSize == "number") {
+      text = getFormattedSize(transferredSize);
+    } else if (transferredSize === null) {
+      text = L10N.getStr("networkMenu.sizeUnavailable");
+    }
+
+    return (
+      div({
+        className: "requests-list-subitem requests-list-transferred",
+        title: text,
+      },
+        span({ className }, text),
+      )
+    );
+  }
+}));
+
+const ContentSizeColumn = createFactory(createClass({
+  displayName: "ContentSizeColumn",
+
+  propTypes: {
+    item: PropTypes.object.isRequired,
+  },
+
+  shouldComponentUpdate(nextProps) {
+    return this.props.item.contentSize !== nextProps.item.contentSize;
+  },
+
+  render() {
+    const { contentSize } = this.props.item;
+
+    let text;
+    if (typeof contentSize == "number") {
+      text = getFormattedSize(contentSize);
+    }
+
+    return (
+      div({
+        className: "requests-list-subitem subitem-label requests-list-size",
+        title: text,
+      },
+        span({ className: "subitem-label" }, text),
+      )
+    );
+  }
+}));
+
+const UPDATED_WATERFALL_PROPS = [
+  "eventTimings",
+  "totalTime",
+  "fromCache",
+  "fromServiceWorker",
+];
+
+const WaterfallColumn = createFactory(createClass({
+  displayName: "WaterfallColumn",
+
+  propTypes: {
+    firstRequestStartedMillis: PropTypes.number.isRequired,
+    item: PropTypes.object.isRequired,
+  },
+
+  shouldComponentUpdate(nextProps) {
+    return this.props.firstRequestStartedMillis !== nextProps.firstRequestStartedMillis ||
+      !propertiesEqual(UPDATED_WATERFALL_PROPS, this.props.item, nextProps.item);
+  },
+
+  render() {
+    const { item, firstRequestStartedMillis } = this.props;
+
+    return (
+      div({ className: "requests-list-subitem requests-list-waterfall" },
+        div({
+          className: "requests-list-timings",
+          style: {
+            paddingInlineStart: `${item.startedMillis - firstRequestStartedMillis}px`,
+          },
+        },
+          timingBoxes(item),
+        )
+      )
+    );
+  }
+}));
+
+// List of properties of the timing info we want to create boxes for
+const TIMING_KEYS = ["blocked", "dns", "connect", "send", "wait", "receive"];
+
+function timingBoxes(item) {
+  const { eventTimings, totalTime, fromCache, fromServiceWorker } = item;
+  let boxes = [];
+
+  if (fromCache || fromServiceWorker) {
+    return boxes;
+  }
+
+  if (eventTimings) {
+    // Add a set of boxes representing timing information.
+    for (let key of TIMING_KEYS) {
+      let width = eventTimings.timings[key];
+
+      // Don't render anything if it surely won't be visible.
+      // One millisecond == one unscaled pixel.
+      if (width > 0) {
+        boxes.push(div({
+          key,
+          className: "requests-list-timings-box " + key,
+          style: { width }
+        }));
+      }
+    }
+  }
+
+  if (typeof totalTime === "number") {
+    let text = L10N.getFormatStr("networkMenu.totalMS", totalTime);
+    boxes.push(div({
+      key: "total",
+      className: "requests-list-timings-total",
+      title: text
+    }, text));
+  }
+
+  return boxes;
+}
+
+module.exports = RequestListItem;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/ResponsePanel.js
@@ -0,0 +1,185 @@
+/* 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,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { L10N } = require("../utils/l10n");
+const { formDataURI, getUrlBaseName } = require("../utils/request-utils");
+
+// Components
+const PropertiesView = createFactory(require("./PropertiesView"));
+
+const { div, img } = DOM;
+const JSON_SCOPE_NAME = L10N.getStr("jsonScopeName");
+const JSON_FILTER_TEXT = L10N.getStr("jsonFilterText");
+const RESPONSE_IMG_NAME = L10N.getStr("netmonitor.response.name");
+const RESPONSE_IMG_DIMENSIONS = L10N.getStr("netmonitor.response.dimensions");
+const RESPONSE_IMG_MIMETYPE = L10N.getStr("netmonitor.response.mime");
+const RESPONSE_PAYLOAD = L10N.getStr("responsePayload");
+
+/*
+ * Response panel component
+ * Displays the GET parameters and POST data of a request
+ */
+const ResponsePanel = createClass({
+  displayName: "ResponsePanel",
+
+  propTypes: {
+    request: PropTypes.object.isRequired,
+  },
+
+  getInitialState() {
+    return {
+      imageDimensions: {
+        width: 0,
+        height: 0,
+      },
+    };
+  },
+
+  updateImageDimemsions({ target }) {
+    this.setState({
+      imageDimensions: {
+        width: target.naturalWidth,
+        height: target.naturalHeight,
+      },
+    });
+  },
+
+  // Handle json, which we tentatively identify by checking the MIME type
+  // for "json" after any word boundary. This works for the standard
+  // "application/json", and also for custom types like "x-bigcorp-json".
+  // Additionally, we also directly parse the response text content to
+  // verify whether it's json or not, to handle responses incorrectly
+  // labeled as text/plain instead.
+  isJSON(mimeType, response) {
+    let json, error;
+    try {
+      json = JSON.parse(response);
+    } catch (err) {
+      try {
+        json = JSON.parse(atob(response));
+      } catch (err64) {
+        error = err;
+      }
+    }
+
+    if (/\bjson/.test(mimeType) || json) {
+      // Extract the actual json substring in case this might be a "JSONP".
+      // This regex basically parses a function call and captures the
+      // function name and arguments in two separate groups.
+      let jsonpRegex = /^\s*([\w$]+)\s*\(\s*([^]*)\s*\)\s*;?\s*$/;
+      let [, jsonpCallback, jsonp] = response.match(jsonpRegex) || [];
+      let result = {};
+
+      // Make sure this is a valid JSON object first. If so, nicely display
+      // the parsing results in a tree view.
+      if (jsonpCallback && jsonp) {
+        error = null;
+        try {
+          json = JSON.parse(jsonp);
+        } catch (err) {
+          error = err;
+        }
+      }
+
+      // Valid JSON
+      if (json) {
+        result.json = json;
+      }
+      // Valid JSONP
+      if (jsonpCallback) {
+        result.jsonpCallback = jsonpCallback;
+      }
+      // Malformed JSON
+      if (error) {
+        result.error = "" + error;
+      }
+
+      return result;
+    }
+
+    return null;
+  },
+
+  render() {
+    let { responseContent, url } = this.props.request;
+
+    if (!responseContent || typeof responseContent.content.text !== "string") {
+      return null;
+    }
+
+    let { encoding, mimeType, text } = responseContent.content;
+
+    if (mimeType.includes("image/")) {
+      let { width, height } = this.state.imageDimensions;
+
+      return (
+        div({ className: "panel-container response-image-box devtools-monospace" },
+          img({
+            className: "response-image",
+            src: formDataURI(mimeType, encoding, text),
+            onLoad: this.updateImageDimemsions,
+          }),
+          div({ className: "response-summary" },
+            div({ className: "tabpanel-summary-label" }, RESPONSE_IMG_NAME),
+            div({ className: "tabpanel-summary-value" }, getUrlBaseName(url)),
+          ),
+          div({ className: "response-summary" },
+            div({ className: "tabpanel-summary-label" }, RESPONSE_IMG_DIMENSIONS),
+            div({ className: "tabpanel-summary-value" }, `${width} × ${height}`),
+          ),
+          div({ className: "response-summary" },
+            div({ className: "tabpanel-summary-label" }, RESPONSE_IMG_MIMETYPE),
+            div({ className: "tabpanel-summary-value" }, mimeType),
+          ),
+        )
+      );
+    }
+
+    // Display Properties View
+    let { json, jsonpCallback, error } = this.isJSON(mimeType, text) || {};
+    let object = {};
+    let sectionName;
+
+    if (json) {
+      if (jsonpCallback) {
+        sectionName = L10N.getFormatStr("jsonpScopeName", jsonpCallback);
+      } else {
+        sectionName = JSON_SCOPE_NAME;
+      }
+      object[sectionName] = json;
+    } else {
+      sectionName = RESPONSE_PAYLOAD;
+
+      object[sectionName] = {
+        EDITOR_CONFIG: {
+          text,
+          mode: mimeType.replace(/;.+/, ""),
+        },
+      };
+    }
+
+    return (
+      div({ className: "panel-container" },
+        error && div({ className: "response-error-header", title: error },
+          error
+        ),
+        PropertiesView({
+          object,
+          filterPlaceHolder: JSON_FILTER_TEXT,
+          sectionNames: [sectionName],
+        }),
+      )
+    );
+  }
+});
+
+module.exports = ResponsePanel;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/SecurityPanel.js
@@ -0,0 +1,160 @@
+/* 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,
+  DOM,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { L10N } = require("../utils/l10n");
+const { getUrlHost } = require("../utils/request-utils");
+
+// Components
+const PropertiesView = createFactory(require("./PropertiesView"));
+
+const { div, input, span } = DOM;
+
+/*
+ * Security panel component
+ * If the site is being served over HTTPS, you get an extra tab labeled "Security".
+ * This contains details about the secure connection used including the protocol,
+ * the cipher suite, and certificate details
+ */
+function SecurityPanel({ request }) {
+  const { securityInfo, url } = request;
+
+  if (!securityInfo || !url) {
+    return null;
+  }
+
+  const notAvailable = L10N.getStr("netmonitor.security.notAvailable");
+  let object;
+
+  if (securityInfo.state === "secure" || securityInfo.state === "weak") {
+    const { subject, issuer, validity, fingerprint } = securityInfo.cert;
+    const enabledLabel = L10N.getStr("netmonitor.security.enabled");
+    const disabledLabel = L10N.getStr("netmonitor.security.disabled");
+
+    object = {
+      [L10N.getStr("netmonitor.security.connection")]: {
+        [L10N.getStr("netmonitor.security.protocolVersion")]:
+          securityInfo.protocolVersion || notAvailable,
+        [L10N.getStr("netmonitor.security.cipherSuite")]:
+          securityInfo.cipherSuite || notAvailable,
+      },
+      [L10N.getFormatStr("netmonitor.security.hostHeader", getUrlHost(url))]: {
+        [L10N.getStr("netmonitor.security.hsts")]:
+          securityInfo.hsts ? enabledLabel : disabledLabel,
+        [L10N.getStr("netmonitor.security.hpkp")]:
+          securityInfo.hpkp ? enabledLabel : disabledLabel,
+      },
+      [L10N.getStr("netmonitor.security.certificate")]: {
+        [L10N.getStr("certmgr.subjectinfo.label")]: {
+          [L10N.getStr("certmgr.certdetail.cn")]:
+            subject.commonName || notAvailable,
+          [L10N.getStr("certmgr.certdetail.o")]:
+            subject.organization || notAvailable,
+          [L10N.getStr("certmgr.certdetail.ou")]:
+            subject.organizationUnit || notAvailable,
+        },
+        [L10N.getStr("certmgr.issuerinfo.label")]: {
+          [L10N.getStr("certmgr.certdetail.cn")]:
+            issuer.commonName || notAvailable,
+          [L10N.getStr("certmgr.certdetail.o")]:
+            issuer.organization || notAvailable,
+          [L10N.getStr("certmgr.certdetail.ou")]:
+            issuer.organizationUnit || notAvailable,
+        },
+        [L10N.getStr("certmgr.periodofvalidity.label")]: {
+          [L10N.getStr("certmgr.begins")]:
+            validity.start || notAvailable,
+          [L10N.getStr("certmgr.expires")]:
+            validity.end || notAvailable,
+        },
+        [L10N.getStr("certmgr.fingerprints.label")]: {
+          [L10N.getStr("certmgr.certdetail.sha256fingerprint")]:
+            fingerprint.sha256 || notAvailable,
+          [L10N.getStr("certmgr.certdetail.sha1fingerprint")]:
+            fingerprint.sha1 || notAvailable,
+        },
+      },
+    };
+  } else {
+    object = {
+      [L10N.getStr("netmonitor.security.error")]:
+        new DOMParser().parseFromString(securityInfo.errorMessage, "text/html")
+          .body.textContent || notAvailable
+    };
+  }
+
+  return div({ className: "panel-container security-panel" },
+    PropertiesView({
+      object,
+      renderValue: (props) => renderValue(props, securityInfo.weaknessReasons),
+      enableFilter: false,
+      expandedNodes: getExpandedNodes(object),
+    })
+  );
+}
+
+SecurityPanel.displayName = "SecurityPanel";
+
+SecurityPanel.propTypes = {
+  request: PropTypes.object.isRequired,
+};
+
+function renderValue(props, weaknessReasons = []) {
+  const { member, value } = props;
+
+  // Hide object summary
+  if (typeof member.value === "object") {
+    return null;
+  }
+
+  return span({ className: "security-info-value" },
+    member.name === L10N.getStr("netmonitor.security.error") ?
+      // Display multiline text for security error
+      value
+      :
+      // Display one line selectable text for security details
+      input({
+        className: "textbox-input",
+        readOnly: "true",
+        value,
+      })
+    ,
+    weaknessReasons.indexOf("cipher") !== -1 &&
+    member.name === L10N.getStr("netmonitor.security.cipherSuite") ?
+      // Display an extra warning icon after the cipher suite
+      div({
+        id: "security-warning-cipher",
+        className: "security-warning-icon",
+        title: L10N.getStr("netmonitor.security.warning.cipher"),
+      })
+      :
+      null
+  );
+}
+
+function getExpandedNodes(object, path = "", level = 0) {
+  if (typeof object !== "object") {
+    return null;
+  }
+
+  let expandedNodes = new Set();
+  for (let prop in object) {
+    let nodePath = path + "/" + prop;
+    expandedNodes.add(nodePath);
+
+    let nodes = getExpandedNodes(object[prop], nodePath, level + 1);
+    if (nodes) {
+      expandedNodes = new Set([...expandedNodes, ...nodes]);
+    }
+  }
+  return expandedNodes;
+}
+
+module.exports = SecurityPanel;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/StatisticsPanel.js
@@ -0,0 +1,276 @@
+/* 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,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+const { Chart } = require("devtools/client/shared/widgets/Chart");
+const { PluralForm } = require("devtools/shared/plural-form");
+const Actions = require("../actions/index");
+const { Filters } = require("../utils/filter-predicates");
+const {
+  getSizeWithDecimals,
+  getTimeWithDecimals
+} = require("../utils/format-utils");
+const { L10N } = require("../utils/l10n");
+
+const { button, div } = DOM;
+const MediaQueryList = window.matchMedia("(min-width: 700px)");
+
+const NETWORK_ANALYSIS_PIE_CHART_DIAMETER = 200;
+const BACK_BUTTON = L10N.getStr("netmonitor.backButton");
+const CHARTS_CACHE_ENABLED = L10N.getStr("charts.cacheEnabled");
+const CHARTS_CACHE_DISABLED = L10N.getStr("charts.cacheDisabled");
+
+/*
+ * Statistics panel component
+ * Performance analysis tool which shows you how long the browser takes to
+ * download the different parts of your site.
+ */
+const StatisticsPanel = createClass({
+  displayName: "StatisticsPanel",
+
+  propTypes: {
+    closeStatistics: PropTypes.func.isRequired,
+    enableRequestFilterTypeOnly: PropTypes.func.isRequired,
+    requests: PropTypes.object,
+  },
+
+  getInitialState() {
+    return {
+      isVerticalSpliter: MediaQueryList.matches,
+    };
+  },
+
+  componentDidUpdate(prevProps) {
+    MediaQueryList.addListener(this.onLayoutChange);
+
+    const { requests } = this.props;
+    let ready = requests && !requests.isEmpty() && requests.every((req) =>
+      req.contentSize !== undefined && req.mimeType && req.responseHeaders &&
+      req.status !== undefined && req.totalTime !== undefined
+    );
+
+    this.createChart({
+      id: "primedCacheChart",
+      title: CHARTS_CACHE_ENABLED,
+      data: ready ? this.sanitizeChartDataSource(requests, false) : null,
+    });
+
+    this.createChart({
+      id: "emptyCacheChart",
+      title: CHARTS_CACHE_DISABLED,
+      data: ready ? this.sanitizeChartDataSource(requests, true) : null,
+    });
+  },
+
+  componentWillUnmount() {
+    MediaQueryList.removeListener(this.onLayoutChange);
+  },
+
+  createChart({ id, title, data }) {
+    // Create a new chart.
+    let chart = Chart.PieTable(document, {
+      diameter: NETWORK_ANALYSIS_PIE_CHART_DIAMETER,
+      title,
+      header: {
+        cached: "",
+        count: "",
+        label: L10N.getStr("charts.type"),
+        size: L10N.getStr("charts.size"),
+        transferredSize: L10N.getStr("charts.transferred"),
+        time: L10N.getStr("charts.time"),
+      },
+      data,
+      strings: {
+        size: (value) =>
+          L10N.getFormatStr("charts.sizeKB", getSizeWithDecimals(value / 1024)),
+        transferredSize: (value) =>
+          L10N.getFormatStr("charts.transferredSizeKB",
+            getSizeWithDecimals(value / 1024)),
+        time: (value) =>
+          L10N.getFormatStr("charts.totalS", getTimeWithDecimals(value / 1000)),
+      },
+      totals: {
+        cached: (total) => L10N.getFormatStr("charts.totalCached", total),
+        count: (total) => L10N.getFormatStr("charts.totalCount", total),
+        size: (total) =>
+          L10N.getFormatStr("charts.totalSize", getSizeWithDecimals(total / 1024)),
+        transferredSize: total =>
+          L10N.getFormatStr("charts.totalTransferredSize",
+            getSizeWithDecimals(total / 1024)),
+        time: (total) => {
+          let seconds = total / 1000;
+          let string = getTimeWithDecimals(seconds);
+          return PluralForm.get(seconds,
+            L10N.getStr("charts.totalSeconds")).replace("#1", string);
+        },
+      },
+      sorted: true,
+    });
+
+    chart.on("click", (_, { label }) => {
+      // Reset FilterButtons and enable one filter exclusively
+      this.props.closeStatistics();
+      this.props.enableRequestFilterTypeOnly(label);
+    });
+
+    let container = this.refs[id];
+
+    // Nuke all existing charts of the specified type.
+    while (container.hasChildNodes()) {
+      container.firstChild.remove();
+    }
+
+    container.appendChild(chart.node);
+  },
+
+  sanitizeChartDataSource(requests, emptyCache) {
+    const data = [
+      "html", "css", "js", "xhr", "fonts", "images", "media", "flash", "ws", "other"
+    ].map((type) => ({
+      cached: 0,
+      count: 0,
+      label: type,
+      size: 0,
+      transferredSize: 0,
+      time: 0,
+    }));
+
+    for (let request of requests) {
+      let type;
+
+      if (Filters.html(request)) {
+        // "html"
+        type = 0;
+      } else if (Filters.css(request)) {
+        // "css"
+        type = 1;
+      } else if (Filters.js(request)) {
+        // "js"
+        type = 2;
+      } else if (Filters.fonts(request)) {
+        // "fonts"
+        type = 4;
+      } else if (Filters.images(request)) {
+        // "images"
+        type = 5;
+      } else if (Filters.media(request)) {
+        // "media"
+        type = 6;
+      } else if (Filters.flash(request)) {
+        // "flash"
+        type = 7;
+      } else if (Filters.ws(request)) {
+        // "ws"
+        type = 8;
+      } else if (Filters.xhr(request)) {
+        // Verify XHR last, to categorize other mime types in their own blobs.
+        // "xhr"
+        type = 3;
+      } else {
+        // "other"
+        type = 9;
+      }
+
+      if (emptyCache || !this.responseIsFresh(request)) {
+        data[type].time += request.totalTime || 0;
+        data[type].size += request.contentSize || 0;
+        data[type].transferredSize += request.transferredSize || 0;
+      } else {
+        data[type].cached++;
+      }
+      data[type].count++;
+    }
+
+    return data.filter(e => e.count > 0);
+  },
+
+  /**
+   * Checks if the "Expiration Calculations" defined in section 13.2.4 of the
+   * "HTTP/1.1: Caching in HTTP" spec holds true for a collection of headers.
+   *
+   * @param object
+   *        An object containing the { responseHeaders, status } properties.
+   * @return boolean
+   *         True if the response is fresh and loaded from cache.
+   */
+  responseIsFresh({ responseHeaders, status }) {
+    // Check for a "304 Not Modified" status and response headers availability.
+    if (status != 304 || !responseHeaders) {
+      return false;
+    }
+
+    let list = responseHeaders.headers;
+    let cacheControl = list.find(e => e.name.toLowerCase() === "cache-control");
+    let expires = list.find(e => e.name.toLowerCase() === "expires");
+
+    // Check the "Cache-Control" header for a maximum age value.
+    if (cacheControl) {
+      let maxAgeMatch =
+        cacheControl.value.match(/s-maxage\s*=\s*(\d+)/) ||
+        cacheControl.value.match(/max-age\s*=\s*(\d+)/);
+
+      if (maxAgeMatch && maxAgeMatch.pop() > 0) {
+        return true;
+      }
+    }
+
+    // Check the "Expires" header for a valid date.
+    if (expires && Date.parse(expires.value)) {
+      return true;
+    }
+
+    return false;
+  },
+
+  onLayoutChange() {
+    this.setState({
+      isVerticalSpliter: MediaQueryList.matches,
+    });
+  },
+
+  render() {
+    const { closeStatistics } = this.props;
+    let splitterClassName = ["splitter"];
+
+    if (this.state.isVerticalSpliter) {
+      splitterClassName.push("devtools-side-splitter");
+    } else {
+      splitterClassName.push("devtools-horizontal-splitter");
+    }
+
+    return (
+      div({ className: "statistics-panel" },
+        button({
+          className: "back-button devtools-button",
+          "data-text-only": "true",
+          title: BACK_BUTTON,
+          onClick: closeStatistics,
+        }, BACK_BUTTON),
+        div({ className: "charts-container" },
+          div({ ref: "primedCacheChart", className: "charts primed-cache-chart" }),
+          div({ className: splitterClassName.join(" ") }),
+          div({ ref: "emptyCacheChart", className: "charts empty-cache-chart" }),
+        ),
+      )
+    );
+  }
+});
+
+module.exports = connect(
+  (state) => ({
+    requests: state.requests.requests.valueSeq(),
+  }),
+  (dispatch) => ({
+    closeStatistics: () => dispatch(Actions.openStatistics(false)),
+    enableRequestFilterTypeOnly: (label) =>
+      dispatch(Actions.enableRequestFilterTypeOnly(label)),
+  })
+)(StatisticsPanel);
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/TabboxPanel.js
@@ -0,0 +1,123 @@
+/* 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,
+  PropTypes,
+} = require("devtools/client/shared/vendor/react");
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+const Actions = require("../actions/index");
+const { Filters } = require("../utils/filter-predicates");
+const { L10N } = require("../utils/l10n");
+const { getSelectedRequest } = require("../selectors/index");
+
+// Components
+const Tabbar = createFactory(require("devtools/client/shared/components/tabs/tabbar"));
+const TabPanel = createFactory(require("devtools/client/shared/components/tabs/tabs").TabPanel);
+const CookiesPanel = createFactory(require("./CookiesPanel"));
+const HeadersPanel = createFactory(require("./HeadersPanel"));
+const ParamsPanel = createFactory(require("./ParamsPanel"));
+const PreviewPanel = createFactory(require("./PreviewPanel"));
+const ResponsePanel = createFactory(require("./ResponsePanel"));
+const SecurityPanel = createFactory(require("./SecurityPanel"));
+const TimingsPanel = createFactory(require("./TimingsPanel"));
+
+const HEADERS_TITLE = L10N.getStr("netmonitor.tab.headers");
+const COOKIES_TITLE = L10N.getStr("netmonitor.tab.cookies");
+const PARAMS_TITLE = L10N.getStr("netmonitor.tab.params");
+const RESPONSE_TITLE = L10N.getStr("netmonitor.tab.response");
+const TIMINGS_TITLE = L10N.getStr("netmonitor.tab.timings");
+const SECURITY_TITLE = L10N.getStr("netmonitor.tab.security");
+const PREVIEW_TITLE = L10N.getStr("netmonitor.tab.preview");
+
+/*
+ * Tabbox panel component
+ * Display the network request details
+ */
+function TabboxPanel({
+  activeTabId,
+  cloneSelectedRequest,
+  request,
+  selectTab,
+}) {
+  if (!request) {
+    return null;
+  }
+
+  return (
+    Tabbar({
+      activeTabId,
+      onSelect: selectTab,
+      renderOnlySelected: true,
+      showAllTabsMenu: true,
+    },
+      TabPanel({
+        id: "headers",
+        title: HEADERS_TITLE,
+      },
+        HeadersPanel({ request, cloneSelectedRequest }),
+      ),
+      TabPanel({
+        id: "cookies",
+        title: COOKIES_TITLE,
+      },
+        CookiesPanel({ request }),
+      ),
+      TabPanel({
+        id: "params",
+        title: PARAMS_TITLE,
+      },
+        ParamsPanel({ request }),
+      ),
+      TabPanel({
+        id: "response",
+        title: RESPONSE_TITLE,
+      },
+        ResponsePanel({ request }),
+      ),
+      TabPanel({
+        id: "timings",
+        title: TIMINGS_TITLE,
+      },
+        TimingsPanel({ request }),
+      ),
+      request.securityState && request.securityState !== "insecure" &&
+      TabPanel({
+        id: "security",
+        title: SECURITY_TITLE,
+      },
+        SecurityPanel({ request }),
+      ),
+      Filters.html(request) &&
+      TabPanel({
+        id: "preview",
+        title: PREVIEW_TITLE,
+      },
+        PreviewPanel({ request }),
+      ),
+    )
+  );
+}
+
+TabboxPanel.displayName = "TabboxPanel";
+
+TabboxPanel.propTypes = {
+  activeTabId: PropTypes.string,
+  cloneSelectedRequest: PropTypes.func.isRequired,
+  request: PropTypes.object,
+  selectTab: PropTypes.func.isRequired,
+};
+
+module.exports = connect(
+  (state) => ({
+    activeTabId: state.ui.detailsPanelSelectedTab,
+    request: getSelectedRequest(state),
+  }),
+  (dispatch) => ({
+    cloneSelectedRequest: () => dispatch(Actions.cloneSelectedRequest()),
+    selectTab: (tabId) => dispatch(Actions.selectDetailsPanelTab(tabId)),
+  }),
+)(TabboxPanel);
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/TimingsPanel.js
@@ -0,0 +1,70 @@
+/* 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 { DOM, PropTypes } = require("devtools/client/shared/vendor/react");
+const { L10N } = require("../utils/l10n");
+
+const { div, span } = DOM;
+const types = ["blocked", "dns", "connect", "send", "wait", "receive"];
+const TIMINGS_END_PADDING = "80px";
+
+/*
+ * Timings panel component
+ * Display timeline bars that shows the total wait time for various stages
+ */
+function TimingsPanel({ request }) {
+  if (!request.eventTimings) {
+    return null;
+  }
+
+  const { timings, totalTime } = request.eventTimings;
+  const timelines = types.map((type, idx) => {
+    // Determine the relative offset for each timings box. For example, the
+    // offset of third timings box will be 0 + blocked offset + dns offset
+    const offset = types
+      .slice(0, idx)
+      .reduce((acc, cur) => (acc + timings[cur] || 0), 0);
+    const offsetScale = offset / totalTime || 0;
+    const timelineScale = timings[type] / totalTime || 0;
+
+    return div({
+      key: type,
+      id: `timings-summary-${type}`,
+      className: "tabpanel-summary-container timings-container",
+    },
+      span({ className: "tabpanel-summary-label timings-label" },
+        L10N.getStr(`netmonitor.timings.${type}`)
+      ),
+      div({ className: "requests-list-timings-container" },
+        span({
+          className: "requests-list-timings-offset",
+          style: {
+            width: `calc(${offsetScale} * (100% - ${TIMINGS_END_PADDING})`,
+          },
+        }),
+        span({
+          className: `requests-list-timings-box ${type}`,
+          style: {
+            width: `calc(${timelineScale} * (100% - ${TIMINGS_END_PADDING}))`,
+          },
+        }),
+        span({ className: "requests-list-timings-total" },
+          L10N.getFormatStr("networkMenu.totalMS", timings[type])
+        )
+      ),
+    );
+  });
+
+  return div({ className: "panel-container" }, timelines);
+}
+
+TimingsPanel.displayName = "TimingsPanel";
+
+TimingsPanel.propTypes = {
+  request: PropTypes.object.isRequired,
+};
+
+module.exports = TimingsPanel;
deleted file mode 100644
--- a/devtools/client/netmonitor/components/monitor-panel.js
+++ /dev/null
@@ -1,135 +0,0 @@
-/* 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,
-  PropTypes,
-} = require("devtools/client/shared/vendor/react");
-const { connect } = require("devtools/client/shared/vendor/react-redux");
-const { findDOMNode } = require("devtools/client/shared/vendor/react-dom");
-const Actions = require("../actions/index");
-const { getLongString } = require("../utils/client");
-const { Prefs } = require("../utils/prefs");
-const { getFormDataSections } = require("../utils/request-utils");
-const { getSelectedRequest } = require("../selectors/index");
-
-// Components
-const SplitBox = createFactory(require("devtools/client/shared/components/splitter/split-box"));
-const NetworkDetailsPanel = createFactory(require("../shared/components/network-details-panel"));
-const RequestList = createFactory(require("./request-list"));
-const Toolbar = createFactory(require("./toolbar"));
-
-const { div } = DOM;
-const MediaQueryList = window.matchMedia("(min-width: 700px)");
-
-/*
- * Monitor panel component
- * The main panel for displaying various network request information
- */
-const MonitorPanel = createClass({
-  displayName: "MonitorPanel",
-
-  propTypes: {
-    isEmpty: PropTypes.bool.isRequired,
-    networkDetailsOpen: PropTypes.bool.isRequired,
-    openNetworkDetails: PropTypes.func.isRequired,
-    request: PropTypes.object,
-    updateRequest: PropTypes.func.isRequired,
-  },
-
-  getInitialState() {
-    return {
-      isVerticalSpliter: MediaQueryList.matches,
-    };
-  },
-
-  componentDidMount() {
-    MediaQueryList.addListener(this.onLayoutChange);
-  },
-
-  componentWillReceiveProps(nextProps) {
-    let {
-      request = {},
-      updateRequest,
-    } = nextProps;
-    let {
-      formDataSections,
-      requestHeaders,
-      requestHeadersFromUploadStream,
-      requestPostData,
-    } = request;
-
-    if (!formDataSections && requestHeaders &&
-        requestHeadersFromUploadStream && requestPostData) {
-      getFormDataSections(
-        requestHeaders,
-        requestHeadersFromUploadStream,
-        requestPostData,
-        getLongString,
-      ).then((newFormDataSections) => {
-        updateRequest(
-          request.id,
-          { formDataSections: newFormDataSections },
-          true,
-        );
-      });
-    }
-  },
-
-  componentWillUnmount() {
-    MediaQueryList.removeListener(this.onLayoutChange);
-
-    let { clientWidth, clientHeight } = findDOMNode(this.refs.endPanel) || {};
-
-    if (this.state.isVerticalSpliter && clientWidth) {
-      Prefs.networkDetailsWidth = clientWidth;
-    }
-    if (!this.state.isVerticalSpliter && clientHeight) {
-      Prefs.networkDetailsHeight = clientHeight;
-    }
-  },
-
-  onLayoutChange() {
-    this.setState({
-      isVerticalSpliter: MediaQueryList.matches,
-    });
-  },
-
-  render() {
-    let { isEmpty, networkDetailsOpen } = this.props;
-    return (
-      div({ className: "monitor-panel" },
-        Toolbar(),
-        SplitBox({
-          className: "devtools-responsive-container",
-          initialWidth: `${Prefs.networkDetailsWidth}px`,
-          initialHeight: `${Prefs.networkDetailsHeight}px`,
-          minSize: "50px",
-          maxSize: "80%",
-          splitterSize: "1px",
-          startPanel: RequestList({ isEmpty }),
-          endPanel: networkDetailsOpen && NetworkDetailsPanel({ ref: "endPanel" }),
-          endPanelControl: true,
-          vert: this.state.isVerticalSpliter,
-        }),
-      )
-    );
-  }
-});
-
-module.exports = connect(
-  (state) => ({
-    isEmpty: state.requests.requests.isEmpty(),
-    networkDetailsOpen: state.ui.networkDetailsOpen,
-    request: getSelectedRequest(state),
-  }),
-  (dispatch) => ({
-    openNetworkDetails: (open) => dispatch(Actions.openNetworkDetails(open)),
-    updateRequest: (id, data, batch) => dispatch(Actions.updateRequest(id, data, batch)),
-  }),
-)(MonitorPanel);
--- a/devtools/client/netmonitor/components/moz.build
+++ b/devtools/client/netmonitor/components/moz.build
@@ -1,15 +1,28 @@
 # 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(
-    'monitor-panel.js',
-    'network-monitor.js',
-    'request-list-content.js',
-    'request-list-empty.js',
-    'request-list-header.js',
-    'request-list-item.js',
-    'request-list.js',
-    'statistics-panel.js',
-    'toolbar.js',
+    'App.js',
+    'CookiesPanel.js',
+    'CustomRequestPanel.js',
+    'Editor.js',
+    'HeadersPanel.js',
+    'MDNLink.js',
+    'MonitorPanel.js',
+    'NetworkDetailsPanel.js',
+    'ParamsPanel.js',
+    'PreviewPanel.js',
+    'PropertiesView.js',
+    'RequestList.js',
+    'RequestListContent.js',
+    'RequestListEmptyNotice.js',
+    'RequestListHeader.js',
+    'RequestListItem.js',
+    'ResponsePanel.js',
+    'SecurityPanel.js',
+    'StatisticsPanel.js',
+    'TabboxPanel.js',
+    'TimingsPanel.js',
+    'Toolbar.js',
 )
deleted file mode 100644
--- a/devtools/client/netmonitor/components/network-monitor.js
+++ /dev/null
@@ -1,39 +0,0 @@
-/* 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,
-  DOM,
-  PropTypes,
-} = require("devtools/client/shared/vendor/react");
-const { connect } = require("devtools/client/shared/vendor/react-redux");
-
-// Components
-const MonitorPanel = createFactory(require("./monitor-panel"));
-const StatisticsPanel = createFactory(require("./statistics-panel"));
-
-const { div } = DOM;
-
-/*
- * Network monitor component
- */
-function NetworkMonitor({ statisticsOpen }) {
-  return (
-    div({ className: "network-monitor" },
-      !statisticsOpen ? MonitorPanel() : StatisticsPanel()
-    )
-  );
-}
-
-NetworkMonitor.displayName = "NetworkMonitor";
-
-NetworkMonitor.propTypes = {
-  statisticsOpen: PropTypes.bool.isRequired,
-};
-
-module.exports = connect(
-  (state) => ({ statisticsOpen: state.ui.statisticsOpen }),
-)(NetworkMonitor);
deleted file mode 100644
--- a/devtools/client/netmonitor/components/request-list-content.js
+++ /dev/null
@@ -1,280 +0,0 @@
-/* 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 { KeyCodes } = require("devtools/client/shared/keycodes");
-const {
-  createClass,
-  createFactory,
-  DOM,
-  PropTypes,
-} = require("devtools/client/shared/vendor/react");
-const { connect } = require("devtools/client/shared/vendor/react-redux");
-const { HTMLTooltip } = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
-const Actions = require("../actions/index");
-const {
-  setTooltipImageContent,
-  setTooltipStackTraceContent,
-} = require("../request-list-tooltip");
-const {
-  getDisplayedRequests,
-  getWaterfallScale,
-} = require("../selectors/index");
-
-// Components
-const RequestListItem = createFactory(require("./request-list-item"));
-const RequestListContextMenu = require("../request-list-context-menu");
-
-const { div } = DOM;
-
-// tooltip show/hide delay in ms
-const REQUESTS_TOOLTIP_TOGGLE_DELAY = 500;
-
-/**
- * Renders the actual contents of the request list.
- */
-const RequestListContent = createClass({
-  displayName: "RequestListContent",
-
-  propTypes: {
-    dispatch: PropTypes.func.isRequired,
-    displayedRequests: PropTypes.object.isRequired,
-    firstRequestStartedMillis: PropTypes.number.isRequired,
-    fromCache: PropTypes.bool.isRequired,
-    onItemMouseDown: PropTypes.func.isRequired,
-    onSecurityIconClick: PropTypes.func.isRequired,
-    onSelectDelta: PropTypes.func.isRequired,
-    scale: PropTypes.number,
-    selectedRequestId: PropTypes.string,
-  },
-
-  componentWillMount() {
-    const { dispatch } = this.props;
-    this.contextMenu = new RequestListContextMenu({
-      cloneSelectedRequest: () => dispatch(Actions.cloneSelectedRequest()),
-      openStatistics: (open) => dispatch(Actions.openStatistics(open)),
-    });
-    this.tooltip = new HTMLTooltip(window.parent.document, { type: "arrow" });
-  },
-
-  componentDidMount() {
-    // Set the CSS variables for waterfall scaling
-    this.setScalingStyles();
-
-    // Install event handler for displaying a tooltip
-    this.tooltip.startTogglingOnHover(this.refs.contentEl, this.onHover, {
-      toggleDelay: REQUESTS_TOOLTIP_TOGGLE_DELAY,
-      interactive: true
-    });
-
-    // Install event handler to hide the tooltip on scroll
-    this.refs.contentEl.addEventListener("scroll", this.onScroll, true);
-  },
-
-  componentWillUpdate(nextProps) {
-    // Check if the list is scrolled to bottom before the UI update.
-    // The scroll is ever needed only if new rows are added to the list.
-    const delta = nextProps.displayedRequests.size - this.props.displayedRequests.size;
-    this.shouldScrollBottom = delta > 0 && this.isScrolledToBottom();
-  },
-
-  componentDidUpdate(prevProps) {
-    // Update the CSS variables for waterfall scaling after props change
-    this.setScalingStyles(prevProps);
-
-    // Keep the list scrolled to bottom if a new row was added
-    if (this.shouldScrollBottom) {
-      let node = this.refs.contentEl;
-      node.scrollTop = node.scrollHeight;
-    }
-  },
-
-  componentWillUnmount() {
-    this.refs.contentEl.removeEventListener("scroll", this.onScroll, true);
-
-    // Uninstall the tooltip event handler
-    this.tooltip.stopTogglingOnHover();
-  },
-
-  /**
-   * Set the CSS variables for waterfall scaling. If React supported setting CSS
-   * variables as part of the "style" property of a DOM element, we would use that.
-   *
-   * However, React doesn't support this, so we need to use a hack and update the
-   * DOM element directly: https://github.com/facebook/react/issues/6411
-   */
-  setScalingStyles(prevProps) {
-    const { scale } = this.props;
-    if (prevProps && prevProps.scale === scale) {
-      return;
-    }
-
-    const { style } = this.refs.contentEl;
-    style.removeProperty("--timings-scale");
-    style.removeProperty("--timings-rev-scale");
-    style.setProperty("--timings-scale", scale);
-    style.setProperty("--timings-rev-scale", 1 / scale);
-  },
-
-  isScrolledToBottom() {
-    const { contentEl } = this.refs;
-    const lastChildEl = contentEl.lastElementChild;
-
-    if (!lastChildEl) {
-      return false;
-    }
-
-    let lastChildRect = lastChildEl.getBoundingClientRect();
-    let contentRect = contentEl.getBoundingClientRect();
-
-    return (lastChildRect.height + lastChildRect.top) <= contentRect.bottom;
-  },
-
-  /**
-   * The predicate used when deciding whether a popup should be shown
-   * over a request item or not.
-   *
-   * @param nsIDOMNode target
-   *        The element node currently being hovered.
-   * @param object tooltip
-   *        The current tooltip instance.
-   * @return {Promise}
-   */
-  onHover(target, tooltip) {
-    let itemEl = target.closest(".request-list-item");
-    if (!itemEl) {
-      return false;
-    }
-    let itemId = itemEl.dataset.id;
-    if (!itemId) {
-      return false;
-    }
-    let requestItem = this.props.displayedRequests.find(r => r.id == itemId);
-    if (!requestItem) {
-      return false;
-    }
-
-    if (requestItem.responseContent && target.closest(".requests-list-icon-and-file")) {
-      return setTooltipImageContent(tooltip, itemEl, requestItem);
-    } else if (requestItem.cause && target.closest(".requests-list-cause-stack")) {
-      return setTooltipStackTraceContent(tooltip, requestItem);
-    }
-
-    return false;
-  },
-
-  /**
-   * Scroll listener for the requests menu view.
-   */
-  onScroll() {
-    this.tooltip.hide();
-  },
-
-  /**
-   * Handler for keyboard events. For arrow up/down, page up/down, home/end,
-   * move the selection up or down.
-   */
-  onKeyDown(e) {
-    let delta;
-
-    switch (e.keyCode) {
-      case KeyCodes.DOM_VK_UP:
-      case KeyCodes.DOM_VK_LEFT:
-        delta = -1;
-        break;
-      case KeyCodes.DOM_VK_DOWN:
-      case KeyCodes.DOM_VK_RIGHT:
-        delta = +1;
-        break;
-      case KeyCodes.DOM_VK_PAGE_UP:
-        delta = "PAGE_UP";
-        break;
-      case KeyCodes.DOM_VK_PAGE_DOWN:
-        delta = "PAGE_DOWN";
-        break;
-      case KeyCodes.DOM_VK_HOME:
-        delta = -Infinity;
-        break;
-      case KeyCodes.DOM_VK_END:
-        delta = +Infinity;
-        break;
-    }
-
-    if (delta) {
-      // Prevent scrolling when pressing navigation keys.
-      e.preventDefault();
-      e.stopPropagation();
-      this.props.onSelectDelta(delta);
-    }
-  },
-
-  onContextMenu(evt) {
-    evt.preventDefault();
-    this.contextMenu.open(evt);
-  },
-
-  /**
-   * If selection has just changed (by keyboard navigation), don't keep the list
-   * scrolled to bottom, but allow scrolling up with the selection.
-   */
-  onFocusedNodeChange() {
-    this.shouldScrollBottom = false;
-  },
-
-  render() {
-    const {
-      displayedRequests,
-      firstRequestStartedMillis,
-      selectedRequestId,
-      onItemMouseDown,
-      onSecurityIconClick,
-    } = this.props;
-
-    return (
-      div({
-        ref: "contentEl",
-        className: "requests-list-contents",
-        tabIndex: 0,
-        onKeyDown: this.onKeyDown,
-      },
-        displayedRequests.map((item, index) => RequestListItem({
-          firstRequestStartedMillis,
-          fromCache: item.status === "304" || item.fromCache,
-          item,
-          index,
-          isSelected: item.id === selectedRequestId,
-          key: item.id,
-          onContextMenu: this.onContextMenu,
-          onFocusedNodeChange: this.onFocusedNodeChange,
-          onMouseDown: () => onItemMouseDown(item.id),
-          onSecurityIconClick: () => onSecurityIconClick(item.securityState),
-        }))
-      )
-    );
-  },
-});
-
-module.exports = connect(
-  (state) => ({
-    displayedRequests: getDisplayedRequests(state),
-    firstRequestStartedMillis: state.requests.firstStartedMillis,
-    selectedRequestId: state.requests.selectedId,
-    scale: getWaterfallScale(state),
-  }),
-  (dispatch) => ({
-    dispatch,
-    onItemMouseDown: (id) => dispatch(Actions.selectRequest(id)),
-    /**
-     * A handler that opens the security tab in the details view if secure or
-     * broken security indicator is clicked.
-     */
-    onSecurityIconClick: (securityState) => {
-      if (securityState && securityState !== "insecure") {
-        dispatch(Actions.selectDetailsPanelTab("security"));
-      }
-    },
-    onSelectDelta: (delta) => dispatch(Actions.selectDelta(delta)),
-  }),
-)(RequestListContent);
deleted file mode 100644
--- a/devtools/client/netmonitor/components/request-list-header.js
+++ /dev/null
@@ -1,198 +0,0 @@
-/* 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, PropTypes, DOM } = require("devtools/client/shared/vendor/react");
-const { div, button } = DOM;
-const { connect } = require("devtools/client/shared/vendor/react-redux");
-const { setNamedTimeout } = require("devtools/client/shared/widgets/view-helpers");
-const { L10N } = require("../utils/l10n");
-const { getWaterfallScale } = require("../selectors/index");
-const Actions = require("../actions/index");
-const WaterfallBackground = require("../waterfall-background");
-const { getFormattedTime } = require("../utils/format-utils");
-
-const REQUESTS_WATERFALL_HEADER_TICKS_MULTIPLE = 5; // ms
-const REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN = 60; // px
-
-const HEADERS = [
-  { name: "status", label: "status3" },
-  { name: "method" },
-  { name: "file", boxName: "icon-and-file" },
-  { name: "domain", boxName: "security-and-domain" },
-  { name: "cause" },
-  { name: "type" },
-  { name: "transferred" },
-  { name: "size" },
-  { name: "waterfall" }
-];
-
-/**
- * Render the request list header with sorting arrows for columns.
- * Displays tick marks in the waterfall column header.
- * Also draws the waterfall background canvas and updates it when needed.
- */
-const RequestListHeader = createClass({
-  displayName: "RequestListHeader",
-
-  propTypes: {
-    sort: PropTypes.object,
-    scale: PropTypes.number,
-    waterfallWidth: PropTypes.number,
-    onHeaderClick: PropTypes.func.isRequired,
-    resizeWaterfall: PropTypes.func.isRequired,
-  },
-
-  componentDidMount() {
-    // Create the object that takes care of drawing the waterfall canvas background
-    this.background = new WaterfallBackground(document);
-    this.background.draw(this.props);
-    this.resizeWaterfall();
-    window.addEventListener("resize", this.resizeWaterfall);
-  },
-
-  componentDidUpdate() {
-    this.background.draw(this.props);
-  },
-
-  componentWillUnmount() {
-    this.background.destroy();
-    this.background = null;
-    window.removeEventListener("resize", this.resizeWaterfall);
-  },
-
-  resizeWaterfall() {
-    // Measure its width and update the 'waterfallWidth' property in the store.
-    // The 'waterfallWidth' will be further updated on every window resize.
-    setNamedTimeout("resize-events", 50, () => {
-      const { width } = this.refs.header.getBoundingClientRect();
-      this.props.resizeWaterfall(width);
-    });
-  },
-
-  render() {
-    const { sort, scale, waterfallWidth, onHeaderClick } = this.props;
-
-    return div(
-      { className: "devtools-toolbar requests-list-toolbar" },
-      div({ className: "toolbar-labels" },
-        HEADERS.map(header => {
-          const name = header.name;
-          const boxName = header.boxName || name;
-          const label = L10N.getStr(`netmonitor.toolbar.${header.label || name}`);
-
-          let sorted, sortedTitle;
-          const active = sort.type == name ? true : undefined;
-          if (active) {
-            sorted = sort.ascending ? "ascending" : "descending";
-            sortedTitle = L10N.getStr(sort.ascending
-              ? "networkMenu.sortedAsc"
-              : "networkMenu.sortedDesc");
-          }
-
-          return div(
-            {
-              id: `requests-list-${boxName}-header-box`,
-              className: `requests-list-header requests-list-${boxName}`,
-              key: name,
-              ref: "header",
-              // Used to style the next column.
-              "data-active": active,
-            },
-            button(
-              {
-                id: `requests-list-${name}-button`,
-                className: `requests-list-header-button requests-list-${name}`,
-                "data-sorted": sorted,
-                title: sortedTitle,
-                onClick: () => onHeaderClick(name),
-              },
-              name == "waterfall" ? WaterfallLabel(waterfallWidth, scale, label)
-                                  : div({ className: "button-text" }, label),
-              div({ className: "button-icon" })
-            )
-          );
-        })
-      )
-    );
-  }
-});
-
-/**
- * Build the waterfall header - timing tick marks with the right spacing
- */
-function waterfallDivisionLabels(waterfallWidth, scale) {
-  let labels = [];
-
-  // Build new millisecond tick labels...
-  let timingStep = REQUESTS_WATERFALL_HEADER_TICKS_MULTIPLE;
-  let scaledStep = scale * timingStep;
-
-  // Ignore any divisions that would end up being too close to each other.
-  while (scaledStep < REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN) {
-    scaledStep *= 2;
-  }
-
-  // Insert one label for each division on the current scale.
-  for (let x = 0; x < waterfallWidth; x += scaledStep) {
-    let millisecondTime = x / scale;
-    let divisionScale = "millisecond";
-
-    // If the division is greater than 1 minute.
-    if (millisecondTime > 60000) {
-      divisionScale = "minute";
-    } else if (millisecondTime > 1000) {
-      // If the division is greater than 1 second.
-      divisionScale = "second";
-    }
-
-    let width = (x + scaledStep | 0) - (x | 0);
-    // Adjust the first marker for the borders
-    if (x == 0) {
-      width -= 2;
-    }
-    // Last marker doesn't need a width specified at all
-    if (x + scaledStep >= waterfallWidth) {
-      width = undefined;
-    }
-
-    labels.push(div(
-      {
-        key: labels.length,
-        className: "requests-list-timings-division",
-        "data-division-scale": divisionScale,
-        style: { width }
-      },
-      getFormattedTime(millisecondTime)
-    ));
-  }
-
-  return labels;
-}
-
-function WaterfallLabel(waterfallWidth, scale, label) {
-  let className = "button-text requests-list-waterfall-label-wrapper";
-
-  if (waterfallWidth != null && scale != null) {
-    label = waterfallDivisionLabels(waterfallWidth, scale);
-    className += " requests-list-waterfall-visible";
-  }
-
-  return div({ className }, label);
-}
-
-module.exports = connect(
-  state => ({
-    sort: state.sort,
-    scale: getWaterfallScale(state),
-    waterfallWidth: state.ui.waterfallWidth,
-    firstRequestStartedMillis: state.requests.firstStartedMillis,
-    timingMarkers: state.timingMarkers,
-  }),
-  dispatch => ({
-    onHeaderClick: type => dispatch(Actions.sortBy(type)),
-    resizeWaterfall: width => dispatch(Actions.resizeWaterfall(width)),
-  })
-)(RequestListHeader);
deleted file mode 100644
--- a/devtools/client/netmonitor/components/request-list-item.js
+++ /dev/null
@@ -1,528 +0,0 @@
-/* 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,
-  PropTypes,
-} = require("devtools/client/shared/vendor/react");
-const { L10N } = require("../utils/l10n");
-const { getAbbreviatedMimeType } = require("../utils/request-utils");
-const { getFormattedSize } = require("../utils/format-utils");
-
-const { div, img, span } = DOM;
-
-/**
- * Compare two objects on a subset of their properties
- */
-function propertiesEqual(props, item1, item2) {
-  return item1 === item2 || props.every(p => item1[p] === item2[p]);
-}
-
-/**
- * Used by shouldComponentUpdate: compare two items, and compare only properties
- * relevant for rendering the RequestListItem. Other properties (like request and
- * response headers, cookies, bodies) are ignored. These are very useful for the
- * network details, but not here.
- */
-const UPDATED_REQ_ITEM_PROPS = [
-  "mimeType",
-  "eventTimings",
-  "securityState",
-  "responseContentDataUri",
-  "status",
-  "statusText",
-  "fromCache",
-  "fromServiceWorker",
-  "method",
-  "url",
-  "remoteAddress",
-  "cause",
-  "contentSize",
-  "transferredSize",
-  "startedMillis",
-  "totalTime",
-];
-
-const UPDATED_REQ_PROPS = [
-  "index",
-  "isSelected",
-  "firstRequestStartedMillis",
-];
-
-/**
- * Render one row in the request list.
- */
-const RequestListItem = createClass({
-  displayName: "RequestListItem",
-
-  propTypes: {
-    item: PropTypes.object.isRequired,
-    index: PropTypes.number.isRequired,
-    isSelected: PropTypes.bool.isRequired,
-    firstRequestStartedMillis: PropTypes.number.isRequired,
-    fromCache: PropTypes.bool.isRequired,
-    onContextMenu: PropTypes.func.isRequired,
-    onFocusedNodeChange: PropTypes.func,
-    onMouseDown: PropTypes.func.isRequired,
-    onSecurityIconClick: PropTypes.func.isRequired,
-  },
-
-  componentDidMount() {
-    if (this.props.isSelected) {
-      this.refs.el.focus();
-    }
-  },
-
-  shouldComponentUpdate(nextProps) {
-    return !propertiesEqual(UPDATED_REQ_ITEM_PROPS, this.props.item, nextProps.item) ||
-      !propertiesEqual(UPDATED_REQ_PROPS, this.props, nextProps);
-  },
-
-  componentDidUpdate(prevProps) {
-    if (!prevProps.isSelected && this.props.isSelected) {
-      this.refs.el.focus();
-      if (this.props.onFocusedNodeChange) {
-        this.props.onFocusedNodeChange();
-      }
-    }
-  },
-
-  render() {
-    const {
-      item,
-      index,
-      isSelected,
-      firstRequestStartedMillis,
-      fromCache,
-      onContextMenu,
-      onMouseDown,
-      onSecurityIconClick
-    } = this.props;
-
-    let classList = ["request-list-item"];
-    if (isSelected) {
-      classList.push("selected");
-    }
-
-    if (fromCache) {
-      classList.push("fromCache");
-    }
-
-    classList.push(index % 2 ? "odd" : "even");
-
-    return (
-      div({
-        ref: "el",
-        className: classList.join(" "),
-        "data-id": item.id,
-        tabIndex: 0,
-        onContextMenu,
-        onMouseDown,
-      },
-        StatusColumn({ item }),
-        MethodColumn({ item }),
-        FileColumn({ item }),
-        DomainColumn({ item, onSecurityIconClick }),
-        CauseColumn({ item }),
-        TypeColumn({ item }),
-        TransferredSizeColumn({ item }),
-        ContentSizeColumn({ item }),
-        WaterfallColumn({ item, firstRequestStartedMillis }),
-      )
-    );
-  }
-});
-
-const UPDATED_STATUS_PROPS = [
-  "status",
-  "statusText",
-  "fromCache",
-  "fromServiceWorker",
-];
-
-const StatusColumn = createFactory(createClass({
-  displayName: "StatusColumn",
-
-  propTypes: {
-    item: PropTypes.object.isRequired,
-  },
-
-  shouldComponentUpdate(nextProps) {
-    return !propertiesEqual(UPDATED_STATUS_PROPS, this.props.item, nextProps.item);
-  },
-
-  render() {
-    const { status, statusText, fromCache, fromServiceWorker } = this.props.item;
-
-    let code, title;
-
-    if (status) {
-      if (fromCache) {
-        code = "cached";
-      } else if (fromServiceWorker) {
-        code = "service worker";
-      } else {
-        code = status;
-      }
-
-      if (statusText) {
-        title = `${status} ${statusText}`;
-        if (fromCache) {
-          title += " (cached)";
-        }
-        if (fromServiceWorker) {
-          title += " (service worker)";
-        }
-      }
-    }
-
-    return (
-        div({ className: "requests-list-subitem requests-list-status", title },
-        div({ className: "requests-list-status-icon", "data-code": code }),
-        span({ className: "subitem-label requests-list-status-code" }, status)
-      )
-    );
-  }
-}));
-
-const MethodColumn = createFactory(createClass({
-  displayName: "MethodColumn",
-
-  propTypes: {
-    item: PropTypes.object.isRequired,
-  },
-
-  shouldComponentUpdate(nextProps) {
-    return this.props.item.method !== nextProps.item.method;
-  },
-
-  render() {
-    const { method } = this.props.item;
-    return (
-      div({ className: "requests-list-subitem requests-list-method-box" },
-        span({ className: "subitem-label requests-list-method" }, method)
-      )
-    );
-  }
-}));
-
-const UPDATED_FILE_PROPS = [
-  "urlDetails",
-  "responseContentDataUri",
-];
-
-const FileColumn = createFactory(createClass({
-  displayName: "FileColumn",
-
-  propTypes: {
-    item: PropTypes.object.isRequired,
-  },
-
-  shouldComponentUpdate(nextProps) {
-    return !propertiesEqual(UPDATED_FILE_PROPS, this.props.item, nextProps.item);
-  },
-
-  render() {
-    const { urlDetails, responseContentDataUri } = this.props.item;
-
-    return (
-      div({ className: "requests-list-subitem requests-list-icon-and-file" },
-        img({
-          className: "requests-list-icon",
-          src: responseContentDataUri,
-          hidden: !responseContentDataUri,
-          "data-type": responseContentDataUri ? "thumbnail" : undefined,
-        }),
-        div({
-          className: "subitem-label requests-list-file",
-          title: urlDetails.unicodeUrl,
-        },
-          urlDetails.baseNameWithQuery,
-        ),
-      )
-    );
-  }
-}));
-
-const UPDATED_DOMAIN_PROPS = [
-  "urlDetails",
-  "remoteAddress",
-  "securityState",
-];
-
-const DomainColumn = createFactory(createClass({
-  displayName: "DomainColumn",
-
-  propTypes: {
-    item: PropTypes.object.isRequired,
-    onSecurityIconClick: PropTypes.func.isRequired,
-  },
-
-  shouldComponentUpdate(nextProps) {
-    return !propertiesEqual(UPDATED_DOMAIN_PROPS, this.props.item, nextProps.item);
-  },
-
-  render() {
-    const { item, onSecurityIconClick } = this.props;
-    const { urlDetails, remoteAddress, securityState } = item;
-
-    let iconClassList = ["requests-security-state-icon"];
-    let iconTitle;
-    if (urlDetails.isLocal) {
-      iconClassList.push("security-state-local");
-      iconTitle = L10N.getStr("netmonitor.security.state.secure");
-    } else if (securityState) {
-      iconClassList.push(`security-state-${securityState}`);
-      iconTitle = L10N.getStr(`netmonitor.security.state.${securityState}`);
-    }
-
-    let title = urlDetails.host + (remoteAddress ? ` (${remoteAddress})` : "");
-
-    return (
-      div({ className: "requests-list-subitem requests-list-security-and-domain" },
-        div({
-          className: iconClassList.join(" "),
-          title: iconTitle,
-          onClick: onSecurityIconClick,
-        }),
-        span({ className: "subitem-label requests-list-domain", title }, urlDetails.host),
-      )
-    );
-  }
-}));
-
-const CauseColumn = createFactory(createClass({
-  displayName: "CauseColumn",
-
-  propTypes: {
-    item: PropTypes.object.isRequired,
-  },
-
-  shouldComponentUpdate(nextProps) {
-    return this.props.item.cause !== nextProps.item.cause;
-  },
-
-  render() {
-    const { cause } = this.props.item;
-
-    let causeType = "";
-    let causeUri = undefined;
-    let causeHasStack = false;
-
-    if (cause) {
-      // Legacy server might send a numeric value. Display it as "unknown"
-      causeType = typeof cause.type === "string" ? cause.type : "unknown";
-      causeUri = cause.loadingDocumentUri;
-      causeHasStack = cause.stacktrace && cause.stacktrace.length > 0;
-    }
-
-    return (
-      div({
-        className: "requests-list-subitem requests-list-cause",
-        title: causeUri,
-      },
-        span({
-          className: "requests-list-cause-stack",
-          hidden: !causeHasStack,
-        }, "JS"),
-        span({ className: "subitem-label" }, causeType),
-      )
-    );
-  }
-}));
-
-const CONTENT_MIME_TYPE_ABBREVIATIONS = {
-  "ecmascript": "js",
-  "javascript": "js",
-  "x-javascript": "js"
-};
-
-const TypeColumn = createFactory(createClass({
-  displayName: "TypeColumn",
-
-  propTypes: {
-    item: PropTypes.object.isRequired,
-  },
-
-  shouldComponentUpdate(nextProps) {
-    return this.props.item.mimeType !== nextProps.item.mimeType;
-  },
-
-  render() {
-    const { mimeType } = this.props.item;
-    let abbrevType;
-    if (mimeType) {
-      abbrevType = getAbbreviatedMimeType(mimeType);
-      abbrevType = CONTENT_MIME_TYPE_ABBREVIATIONS[abbrevType] || abbrevType;
-    }
-
-    return (
-      div({
-        className: "requests-list-subitem requests-list-type",
-        title: mimeType,
-      },
-        span({ className: "subitem-label" }, abbrevType),
-      )
-    );
-  }
-}));
-
-const UPDATED_TRANSFERRED_PROPS = [
-  "transferredSize",
-  "fromCache",
-  "fromServiceWorker",
-];
-
-const TransferredSizeColumn = createFactory(createClass({
-  displayName: "TransferredSizeColumn",
-
-  propTypes: {
-    item: PropTypes.object.isRequired,
-  },
-
-  shouldComponentUpdate(nextProps) {
-    return !propertiesEqual(UPDATED_TRANSFERRED_PROPS, this.props.item, nextProps.item);
-  },
-
-  render() {
-    const { transferredSize, fromCache, fromServiceWorker, status } = this.props.item;
-
-    let text;
-    let className = "subitem-label";
-    if (fromCache || status === "304") {
-      text = L10N.getStr("networkMenu.sizeCached");
-      className += " theme-comment";
-    } else if (fromServiceWorker) {
-      text = L10N.getStr("networkMenu.sizeServiceWorker");
-      className += " theme-comment";
-    } else if (typeof transferredSize == "number") {
-      text = getFormattedSize(transferredSize);
-    } else if (transferredSize === null) {
-      text = L10N.getStr("networkMenu.sizeUnavailable");
-    }
-
-    return (
-      div({
-        className: "requests-list-subitem requests-list-transferred",
-        title: text,
-      },
-        span({ className }, text),
-      )
-    );
-  }
-}));
-
-const ContentSizeColumn = createFactory(createClass({
-  displayName: "ContentSizeColumn",
-
-  propTypes: {
-    item: PropTypes.object.isRequired,
-  },
-
-  shouldComponentUpdate(nextProps) {
-    return this.props.item.contentSize !== nextProps.item.contentSize;
-  },
-
-  render() {
-    const { contentSize } = this.props.item;
-
-    let text;
-    if (typeof contentSize == "number") {
-      text = getFormattedSize(contentSize);
-    }
-
-    return (
-      div({
-        className: "requests-list-subitem subitem-label requests-list-size",
-        title: text,
-      },
-        span({ className: "subitem-label" }, text),
-      )
-    );
-  }
-}));
-
-const UPDATED_WATERFALL_PROPS = [
-  "eventTimings",
-  "totalTime",
-  "fromCache",
-  "fromServiceWorker",
-];
-
-const WaterfallColumn = createFactory(createClass({
-  displayName: "WaterfallColumn",
-
-  propTypes: {
-    firstRequestStartedMillis: PropTypes.number.isRequired,
-    item: PropTypes.object.isRequired,
-  },
-
-  shouldComponentUpdate(nextProps) {
-    return this.props.firstRequestStartedMillis !== nextProps.firstRequestStartedMillis ||
-      !propertiesEqual(UPDATED_WATERFALL_PROPS, this.props.item, nextProps.item);
-  },
-
-  render() {
-    const { item, firstRequestStartedMillis } = this.props;
-
-    return (
-      div({ className: "requests-list-subitem requests-list-waterfall" },
-        div({
-          className: "requests-list-timings",
-          style: {
-            paddingInlineStart: `${item.startedMillis - firstRequestStartedMillis}px`,
-          },
-        },
-          timingBoxes(item),
-        )
-      )
-    );
-  }
-}));
-
-// List of properties of the timing info we want to create boxes for
-const TIMING_KEYS = ["blocked", "dns", "connect", "send", "wait", "receive"];
-
-function timingBoxes(item) {
-  const { eventTimings, totalTime, fromCache, fromServiceWorker } = item;
-  let boxes = [];
-
-  if (fromCache || fromServiceWorker) {
-    return boxes;
-  }
-
-  if (eventTimings) {
-    // Add a set of boxes representing timing information.
-    for (let key of TIMING_KEYS) {
-      let width = eventTimings.timings[key];
-
-      // Don't render anything if it surely won't be visible.
-      // One millisecond == one unscaled pixel.
-      if (width > 0) {
-        boxes.push(div({
-          key,
-          className: "requests-list-timings-box " + key,
-          style: { width }
-        }));
-      }
-    }
-  }
-
-  if (typeof totalTime === "number") {
-    let text = L10N.getFormatStr("networkMenu.totalMS", totalTime);
-    boxes.push(div({
-      key: "total",
-      className: "requests-list-timings-total",
-      title: text
-    }, text));
-  }
-
-  return boxes;
-}
-
-module.exports = RequestListItem;
deleted file mode 100644
--- a/devtools/client/netmonitor/components/request-list.js
+++ /dev/null
@@ -1,38 +0,0 @@
-/* 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,
-  DOM,
-  PropTypes,
-} = require("devtools/client/shared/vendor/react");
-
-// Components
-const RequestListContent = createFactory(require("./request-list-content"));
-const RequestListEmptyNotice = createFactory(require("./request-list-empty"));
-const RequestListHeader = createFactory(require("./request-list-header"));
-
-const { div } = DOM;
-
-/**
- * Request panel component
- */
-function RequestList({ isEmpty }) {
-  return (
-    div({ className: "request-list-container" },
-      RequestListHeader(),
-      isEmpty ? RequestListEmptyNotice() : RequestListContent(),
-    )
-  );
-}
-
-RequestList.displayName = "RequestList";
-
-RequestList.propTypes = {
-  isEmpty: PropTypes.bool.isRequired,
-};
-
-module.exports = RequestList;
deleted file mode 100644
--- a/devtools/client/netmonitor/components/statistics-panel.js
+++ /dev/null
@@ -1,276 +0,0 @@
-/* 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,
-  PropTypes,
-} = require("devtools/client/shared/vendor/react");
-const { connect } = require("devtools/client/shared/vendor/react-redux");
-const { Chart } = require("devtools/client/shared/widgets/Chart");
-const { PluralForm } = require("devtools/shared/plural-form");
-const Actions = require("../actions/index");
-const { Filters } = require("../utils/filter-predicates");
-const { L10N } = require("../utils/l10n");
-const {
-  getSizeWithDecimals,
-  getTimeWithDecimals
-} = require("../utils/format-utils");
-
-const { button, div } = DOM;
-const MediaQueryList = window.matchMedia("(min-width: 700px)");
-
-const NETWORK_ANALYSIS_PIE_CHART_DIAMETER = 200;
-const BACK_BUTTON = L10N.getStr("netmonitor.backButton");
-const CHARTS_CACHE_ENABLED = L10N.getStr("charts.cacheEnabled");
-const CHARTS_CACHE_DISABLED = L10N.getStr("charts.cacheDisabled");
-
-/*
- * Statistics panel component
- * Performance analysis tool which shows you how long the browser takes to
- * download the different parts of your site.
- */
-const StatisticsPanel = createClass({
-  displayName: "StatisticsPanel",
-
-  propTypes: {
-    closeStatistics: PropTypes.func.isRequired,
-    enableRequestFilterTypeOnly: PropTypes.func.isRequired,
-    requests: PropTypes.object,
-  },
-
-  getInitialState() {
-    return {
-      isVerticalSpliter: MediaQueryList.matches,
-    };
-  },
-
-  componentDidUpdate(prevProps) {
-    MediaQueryList.addListener(this.onLayoutChange);
-
-    const { requests } = this.props;
-    let ready = requests && !requests.isEmpty() && requests.every((req) =>
-      req.contentSize !== undefined && req.mimeType && req.responseHeaders &&
-      req.status !== undefined && req.totalTime !== undefined
-    );
-
-    this.createChart({
-      id: "primedCacheChart",
-      title: CHARTS_CACHE_ENABLED,
-      data: ready ? this.sanitizeChartDataSource(requests, false) : null,
-    });
-
-    this.createChart({
-      id: "emptyCacheChart",
-      title: CHARTS_CACHE_DISABLED,
-      data: ready ? this.sanitizeChartDataSource(requests, true) : null,
-    });
-  },
-
-  componentWillUnmount() {
-    MediaQueryList.removeListener(this.onLayoutChange);
-  },
-
-  createChart({ id, title, data }) {
-    // Create a new chart.
-    let chart = Chart.PieTable(document, {
-      diameter: NETWORK_ANALYSIS_PIE_CHART_DIAMETER,
-      title,
-      header: {
-        cached: "",
-        count: "",
-        label: L10N.getStr("charts.type"),
-        size: L10N.getStr("charts.size"),
-        transferredSize: L10N.getStr("charts.transferred"),
-        time: L10N.getStr("charts.time"),
-      },
-      data,
-      strings: {
-        size: (value) =>
-          L10N.getFormatStr("charts.sizeKB", getSizeWithDecimals(value / 1024)),
-        transferredSize: (value) =>
-          L10N.getFormatStr("charts.transferredSizeKB",
-            getSizeWithDecimals(value / 1024)),
-        time: (value) =>
-          L10N.getFormatStr("charts.totalS", getTimeWithDecimals(value / 1000)),
-      },
-      totals: {
-        cached: (total) => L10N.getFormatStr("charts.totalCached", total),
-        count: (total) => L10N.getFormatStr("charts.totalCount", total),
-        size: (total) =>
-          L10N.getFormatStr("charts.totalSize", getSizeWithDecimals(total / 1024)),
-        transferredSize: total =>
-          L10N.getFormatStr("charts.totalTransferredSize",
-            getSizeWithDecimals(total / 1024)),
-        time: (total) => {
-          let seconds = total / 1000;
-          let string = getTimeWithDecimals(seconds);
-          return PluralForm.get(seconds,
-            L10N.getStr("charts.totalSeconds")).replace("#1", string);
-        },
-      },
-      sorted: true,
-    });
-
-    chart.on("click", (_, { label }) => {
-      // Reset FilterButtons and enable one filter exclusively
-      this.props.closeStatistics();
-      this.props.enableRequestFilterTypeOnly(label);
-    });
-
-    let container = this.refs[id];
-
-    // Nuke all existing charts of the specified type.
-    while (container.hasChildNodes()) {
-      container.firstChild.remove();
-    }
-
-    container.appendChild(chart.node);
-  },
-
-  sanitizeChartDataSource(requests, emptyCache) {
-    const data = [
-      "html", "css", "js", "xhr", "fonts", "images", "media", "flash", "ws", "other"
-    ].map((type) => ({
-      cached: 0,
-      count: 0,
-      label: type,
-      size: 0,
-      transferredSize: 0,
-      time: 0,
-    }));
-
-    for (let request of requests) {
-      let type;
-
-      if (Filters.html(request)) {
-        // "html"
-        type = 0;
-      } else if (Filters.css(request)) {
-        // "css"
-        type = 1;
-      } else if (Filters.js(request)) {
-        // "js"
-        type = 2;
-      } else if (Filters.fonts(request)) {
-        // "fonts"
-        type = 4;
-      } else if (Filters.images(request)) {
-        // "images"
-        type = 5;
-      } else if (Filters.media(request)) {
-        // "media"
-        type = 6;
-      } else if (Filters.flash(request)) {
-        // "flash"
-        type = 7;
-      } else if (Filters.ws(request)) {
-        // "ws"
-        type = 8;
-      } else if (Filters.xhr(request)) {
-        // Verify XHR last, to categorize other mime types in their own blobs.
-        // "xhr"
-        type = 3;
-      } else {
-        // "other"
-        type = 9;
-      }
-
-      if (emptyCache || !this.responseIsFresh(request)) {
-        data[type].time += request.totalTime || 0;
-        data[type].size += request.contentSize || 0;
-        data[type].transferredSize += request.transferredSize || 0;
-      } else {
-        data[type].cached++;
-      }
-      data[type].count++;
-    }
-
-    return data.filter(e => e.count > 0);
-  },
-
-  /**
-   * Checks if the "Expiration Calculations" defined in section 13.2.4 of the
-   * "HTTP/1.1: Caching in HTTP" spec holds true for a collection of headers.
-   *
-   * @param object
-   *        An object containing the { responseHeaders, status } properties.
-   * @return boolean
-   *         True if the response is fresh and loaded from cache.
-   */
-  responseIsFresh({ responseHeaders, status }) {
-    // Check for a "304 Not Modified" status and response headers availability.
-    if (status != 304 || !responseHeaders) {
-      return false;
-    }
-
-    let list = responseHeaders.headers;
-    let cacheControl = list.find(e => e.name.toLowerCase() === "cache-control");
-    let expires = list.find(e => e.name.toLowerCase() === "expires");
-
-    // Check the "Cache-Control" header for a maximum age value.
-    if (cacheControl) {
-      let maxAgeMatch =
-        cacheControl.value.match(/s-maxage\s*=\s*(\d+)/) ||
-        cacheControl.value.match(/max-age\s*=\s*(\d+)/);
-
-      if (maxAgeMatch && maxAgeMatch.pop() > 0) {
-        return true;
-      }
-    }
-
-    // Check the "Expires" header for a valid date.
-    if (expires && Date.parse(expires.value)) {
-      return true;
-    }
-
-    return false;
-  },
-
-  onLayoutChange() {
-    this.setState({
-      isVerticalSpliter: MediaQueryList.matches,
-    });
-  },
-
-  render() {
-    const { closeStatistics } = this.props;
-    let splitterClassName = ["splitter"];
-
-    if (this.state.isVerticalSpliter) {
-      splitterClassName.push("devtools-side-splitter");
-    } else {
-      splitterClassName.push("devtools-horizontal-splitter");
-    }
-
-    return (
-      div({ className: "statistics-panel" },
-        button({
-          className: "back-button devtools-button",
-          "data-text-only": "true",
-          title: BACK_BUTTON,
-          onClick: closeStatistics,
-        }, BACK_BUTTON),
-        div({ className: "charts-container" },
-          div({ ref: "primedCacheChart", className: "charts primed-cache-chart" }),
-          div({ className: splitterClassName.join(" ") }),
-          div({ ref: "emptyCacheChart", className: "charts empty-cache-chart" }),
-        ),
-      )
-    );
-  }
-});
-
-module.exports = connect(
-  (state) => ({
-    requests: state.requests.requests.valueSeq(),
-  }),
-  (dispatch) => ({
-    closeStatistics: () => dispatch(Actions.openStatistics(false)),
-    enableRequestFilterTypeOnly: (label) =>
-      dispatch(Actions.enableRequestFilterTypeOnly(label)),
-  })
-)(StatisticsPanel);
--- a/devtools/client/netmonitor/components/toolbar.js
+++ b/devtools/client/netmonitor/components/toolbar.js
@@ -8,27 +8,27 @@ const {
   createClass,
   createFactory,
   DOM,
   PropTypes,
 } = require("devtools/client/shared/vendor/react");
 const { connect } = require("devtools/client/shared/vendor/react-redux");
 const { PluralForm } = require("devtools/shared/plural-form");
 const Actions = require("../actions/index");
-const { L10N } = require("../utils/l10n");
+const { FILTER_SEARCH_DELAY } = require("../constants");
 const {
   getDisplayedRequestsSummary,
   getRequestFilterTypes,
   isNetworkDetailsToggleButtonDisabled,
 } = require("../selectors/index");
 const {
   getFormattedSize,
   getFormattedTime
 } = require("../utils/format-utils");
-const { FILTER_SEARCH_DELAY } = require("../constants");
+const { L10N } = require("../utils/l10n");
 
 // Components
 const SearchBox = createFactory(require("devtools/client/shared/components/search-box"));
 
 const { button, div, span } = DOM;
 
 const COLLPASE_DETAILS_PANE = L10N.getStr("collapseDetailsPane");
 const EXPAND_DETAILS_PANE = L10N.getStr("expandDetailsPane");
--- a/devtools/client/netmonitor/moz.build
+++ b/devtools/client/netmonitor/moz.build
@@ -4,17 +4,16 @@
 
 DIRS += [
     'actions',
     'components',
     'har',
     'middleware',
     'reducers',
     'selectors',
-    'shared',
     'utils',
 ]
 
 DevToolsModules(
     'constants.js',
     'netmonitor-controller.js',
     'panel.js',
     'request-list-context-menu.js',
--- a/devtools/client/netmonitor/netmonitor.js
+++ b/devtools/client/netmonitor/netmonitor.js
@@ -20,25 +20,25 @@ var Netmonitor = {
     const { createFactory } = require("devtools/client/shared/vendor/react");
     const { render } = require("devtools/client/shared/vendor/react-dom");
     const Provider = createFactory(require("devtools/client/shared/vendor/react-redux").Provider);
     const { configureStore } = require("./store");
     const store = window.gStore = configureStore();
     const { NetMonitorController } = require("./netmonitor-controller");
     this.NetMonitorController = NetMonitorController;
 
-    // Components
-    const NetworkMonitor = createFactory(require("./components/network-monitor"));
-
     // Inject EventEmitter into netmonitor window.
     EventEmitter.decorate(window);
 
+    // Components
+    const App = createFactory(require("./components/App"));
+
     this.root = document.querySelector(".root");
 
-    render(Provider({ store }, NetworkMonitor()), this.root);
+    render(Provider({ store }, App()), this.root);
 
     return NetMonitorController.startupNetMonitor({
       client: {
         getTabTarget: () => toolbox.target,
       },
       toolbox,
     });
   },
deleted file mode 100644
--- a/devtools/client/netmonitor/shared/components/cookies-panel.js
+++ /dev/null
@@ -1,99 +0,0 @@
-/* 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,
-  DOM,
-  PropTypes,
-} = require("devtools/client/shared/vendor/react");
-const { L10N } = require("../../utils/l10n");
-
-// Component
-const PropertiesView = createFactory(require("./properties-view"));
-
-const { div } = DOM;
-
-const COOKIES_EMPTY_TEXT = L10N.getStr("cookiesEmptyText");
-const COOKIES_FILTER_TEXT = L10N.getStr("cookiesFilterText");
-const REQUEST_COOKIES = L10N.getStr("requestCookies");
-const RESPONSE_COOKIES = L10N.getStr("responseCookies");
-const SECTION_NAMES = [
-  RESPONSE_COOKIES,
-  REQUEST_COOKIES,
-];
-
-/*
- * Cookies panel component
- * This tab lists full details of any cookies sent with the request or response
- */
-function CookiesPanel({
-  request,
-}) {
-  let {
-    requestCookies = { cookies: [] },
-    responseCookies = { cookies: [] },
-  } = request;
-
-  requestCookies = requestCookies.cookies || requestCookies;
-  responseCookies = responseCookies.cookies || responseCookies;
-
-  if (!requestCookies.length && !responseCookies.length) {
-    return div({ className: "empty-notice" },
-      COOKIES_EMPTY_TEXT
-    );
-  }
-
-  let object = {};
-
-  if (responseCookies.length) {
-    object[RESPONSE_COOKIES] = getProperties(responseCookies);
-  }
-
-  if (requestCookies.length) {
-    object[REQUEST_COOKIES] = getProperties(requestCookies);
-  }
-
-  return (
-    div({ className: "panel-container" },
-      PropertiesView({
-        object,
-        filterPlaceHolder: COOKIES_FILTER_TEXT,
-        sectionNames: SECTION_NAMES,
-      })
-    )
-  );
-}
-
-CookiesPanel.displayName = "CookiesPanel";
-
-CookiesPanel.propTypes = {
-  request: PropTypes.object.isRequired,
-};
-
-/**
- * Mapping array to dict for TreeView usage.
- * Since TreeView only support Object(dict) format.
- *
- * @param {Object[]} arr - key-value pair array like cookies or params
- * @returns {Object}
- */
-function getProperties(arr) {
-  return arr.reduce((map, obj) => {
-    // Generally cookies object contains only name and value properties and can
-    // be rendered as name: value pair.
-    // When there are more properties in cookies object such as extra or path,
-    // We will pass the object to display these extra information
-    if (Object.keys(obj).length > 2) {
-      map[obj.name] = Object.assign({}, obj);
-      delete map[obj.name].name;
-    } else {
-      map[obj.name] = obj.value;
-    }
-    return map;
-  }, {});
-}
-
-module.exports = CookiesPanel;
deleted file mode 100644
--- a/devtools/client/netmonitor/shared/components/custom-request-panel.js
+++ /dev/null
@@ -1,257 +0,0 @@
-/* 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 {
-  DOM,
-  PropTypes,
-} = require("devtools/client/shared/vendor/react");
-const { connect } = require("devtools/client/shared/vendor/react-redux");
-const { L10N } = require("../../utils/l10n");
-const Actions = require("../../actions/index");
-const { getSelectedRequest } = require("../../selectors/index");
-const {
-  getUrlQuery,
-  parseQueryString,
-  writeHeaderText,
-} = require("../../utils/request-utils");
-
-const {
-  button,
-  div,
-  input,
-  textarea,
-} = DOM;
-
-const CUSTOM_CANCEL = L10N.getStr("netmonitor.custom.cancel");
-const CUSTOM_HEADERS = L10N.getStr("netmonitor.custom.headers");
-const CUSTOM_NEW_REQUEST = L10N.getStr("netmonitor.custom.newRequest");
-const CUSTOM_POSTDATA = L10N.getStr("netmonitor.custom.postData");
-const CUSTOM_QUERY = L10N.getStr("netmonitor.custom.query");
-const CUSTOM_SEND = L10N.getStr("netmonitor.custom.send");
-
-function CustomRequestPanel({
-  removeSelectedCustomRequest,
-  request = {},
-  sendCustomRequest,
-  updateRequest,
-}) {
-  let {
-    method,
-    customQueryValue,
-    requestHeaders,
-    requestPostData,
-    url,
-  } = request;
-
-  let headers = "";
-  if (requestHeaders) {
-    headers = requestHeaders.customHeadersValue ?
-      requestHeaders.customHeadersValue : writeHeaderText(requestHeaders.headers);
-  }
-  let queryArray = url ? parseQueryString(getUrlQuery(url)) : [];
-  let params = customQueryValue;
-  if (!params) {
-    params = queryArray ?
-      queryArray.map(({ name, value }) => name + "=" + value).join("\n") : "";
-  }
-  let postData = requestPostData && requestPostData.postData.text ?
-    requestPostData.postData.text : "";
-
-  return (
-    div({ className: "custom-request-panel" },
-      div({ className: "tabpanel-summary-container custom-request" },
-        div({ className: "custom-request-label custom-header" },
-          CUSTOM_NEW_REQUEST
-        ),
-        button({
-          className: "devtools-button",
-          id: "custom-request-send-button",
-          onClick: sendCustomRequest,
-        },
-          CUSTOM_SEND
-        ),
-        button({
-          className: "devtools-button",
-          id: "custom-request-close-button",
-          onClick: removeSelectedCustomRequest,
-        },
-          CUSTOM_CANCEL
-        ),
-      ),
-      div({
-        className: "tabpanel-summary-container custom-method-and-url",
-        id: "custom-method-and-url",
-      },
-        input({
-          className: "custom-method-value",
-          id: "custom-method-value",
-          onChange: (evt) => updateCustomRequestFields(evt, request, updateRequest),
-          value: method || "GET",
-        }),
-        input({
-          className: "custom-url-value",
-          id: "custom-url-value",
-          onChange: (evt) => updateCustomRequestFields(evt, request, updateRequest),
-          value: url || "http://",
-        }),
-      ),
-      // Hide query field when there is no params
-      params ? div({
-        className: "tabpanel-summary-container custom-section",
-        id: "custom-query",
-      },
-        div({ className: "custom-request-label" }, CUSTOM_QUERY),
-        textarea({
-          className: "tabpanel-summary-input",
-          id: "custom-query-value",
-          onChange: (evt) => updateCustomRequestFields(evt, request, updateRequest),
-          rows: 4,
-          value: params,
-          wrap: "off",
-        })
-      ) : null,
-      div({
-        id: "custom-headers",
-        className: "tabpanel-summary-container custom-section",
-      },
-        div({ className: "custom-request-label" }, CUSTOM_HEADERS),
-        textarea({
-          className: "tabpanel-summary-input",
-          id: "custom-headers-value",
-          onChange: (evt) => updateCustomRequestFields(evt, request, updateRequest),
-          rows: 8,
-          value: headers,
-          wrap: "off",
-        })
-      ),
-      div({
-        id: "custom-postdata",
-        className: "tabpanel-summary-container custom-section",
-      },
-        div({ className: "custom-request-label" }, CUSTOM_POSTDATA),
-        textarea({
-          className: "tabpanel-summary-input",
-          id: "custom-postdata-value",
-          onChange: (evt) => updateCustomRequestFields(evt, request, updateRequest),
-          rows: 6,
-          value: postData,
-          wrap: "off",
-        })
-      ),
-    )
-  );
-}
-
-CustomRequestPanel.displayName = "CustomRequestPanel";
-
-CustomRequestPanel.propTypes = {
-  removeSelectedCustomRequest: PropTypes.func.isRequired,
-  request: PropTypes.object,
-  sendCustomRequest: PropTypes.func.isRequired,
-  updateRequest: PropTypes.func.isRequired,
-};
-
-/**
- * Parse a text representation of a name[divider]value list with
- * the given name regex and divider character.
- *
- * @param {string} text - Text of list
- * @return {array} array of headers info {name, value}
- */
-function parseRequestText(text, namereg, divider) {
-  let regex = new RegExp(`(${namereg})\\${divider}\\s*(.+)`);
-  let pairs = [];
-
-  for (let line of text.split("\n")) {
-    let matches = regex.exec(line);
-    if (matches) {
-      let [, name, value] = matches;
-      pairs.push({ name, value });
-    }
-  }
-  return pairs;
-}
-
-/**
- * Update Custom Request Fields
- *
- * @param {Object} evt click event
- * @param {Object} request current request
- * @param {updateRequest} updateRequest action
- */
-function updateCustomRequestFields(evt, request, updateRequest) {
-  const val = evt.target.value;
-  let data;
-  switch (evt.target.id) {
-    case "custom-headers-value":
-      let customHeadersValue = val || "";
-      // Parse text representation of multiple HTTP headers
-      let headersArray = parseRequestText(customHeadersValue, "\\S+?", ":");
-      // Remove temp customHeadersValue while query string is parsable
-      if (customHeadersValue === "" ||
-          headersArray.length === customHeadersValue.split("\n").length) {
-        customHeadersValue = null;
-      }
-      data = {
-        requestHeaders: {
-          customHeadersValue,
-          headers: headersArray,
-        },
-      };
-      break;
-    case "custom-method-value":
-      data = { method: val.trim() };
-      break;
-    case "custom-postdata-value":
-      data = {
-        requestPostData: {
-          postData: { text: val },
-        }
-      };
-      break;
-    case "custom-query-value":
-      let customQueryValue = val || "";
-      // Parse readable text list of a query string
-      let queryArray = customQueryValue ?
-        parseRequestText(customQueryValue, ".+?", "=") : [];
-      // Write out a list of query params into a query string
-      let queryString = queryArray.map(
-        ({ name, value }) => name + "=" + value).join("&");
-      let url = queryString ? [request.url.split("?")[0], queryString].join("?") :
-        request.url.split("?")[0];
-      // Remove temp customQueryValue while query string is parsable
-      if (customQueryValue === "" ||
-          queryArray.length === customQueryValue.split("\n").length) {
-        customQueryValue = null;
-      }
-      data = {
-        customQueryValue,
-        url,
-      };
-      break;
-    case "custom-url-value":
-      data = {
-        customQueryValue: null,
-        url: val
-      };
-      break;
-    default:
-      break;
-  }
-  if (data) {
-    // All updateRequest batch mode should be disabled to make UI editing in sync
-    updateRequest(request.id, data, false);
-  }
-}
-
-module.exports = connect(
-  (state) => ({ request: getSelectedRequest(state) }),
-  (dispatch) => ({
-    removeSelectedCustomRequest: () => dispatch(Actions.removeSelectedCustomRequest()),
-    sendCustomRequest: () => dispatch(Actions.sendCustomRequest()),
-    updateRequest: (id, data, batch) => dispatch(Actions.updateRequest(id, data, batch)),
-  })
-)(CustomRequestPanel);
deleted file mode 100644
--- a/devtools/client/netmonitor/shared/components/editor.js
+++ /dev/null
@@ -1,103 +0,0 @@
-/* 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-disable react/prop-types */
-
-"use strict";
-
-const { createClass, DOM, PropTypes } = require("devtools/client/shared/vendor/react");
-const SourceEditor = require("devtools/client/sourceeditor/editor");
-
-const { div } = DOM;
-const SYNTAX_HIGHLIGHT_MAX_SIZE = 102400;
-
-/**
- * CodeMirror editor as a React component
- */
-const Editor = createClass({
-  displayName: "Editor",
-
-  propTypes: {
-    // Source editor syntax hightligh mode, which is a mime type defined in CodeMirror
-    mode: PropTypes.string,
-    // Source editor is displayed if set to true
-    open: PropTypes.bool,
-    // Source editor content
-    text: PropTypes.string,
-  },
-
-  getDefaultProps() {
-    return {
-      mode: null,
-      open: true,
-      text: "",
-    };
-  },
-
-  componentDidMount() {
-    const { mode, text } = this.props;
-
-    this.editor = new SourceEditor({
-      lineNumbers: true,
-      mode: text.length < SYNTAX_HIGHLIGHT_MAX_SIZE ? mode : null,
-      readOnly: true,
-      value: text,
-    });
-
-    this.deferEditor = this.editor.appendTo(this.refs.editorElement);
-  },
-
-  componentDidUpdate(prevProps) {
-    const { mode, open, text } = this.props;
-
-    if (!open) {
-      return;
-    }
-
-    if (prevProps.mode !== mode && text.length < SYNTAX_HIGHLIGHT_MAX_SIZE) {
-      this.deferEditor.then(() => {
-        this.editor.setMode(mode);
-      });
-    }
-
-    if (prevProps.text !== text) {
-      this.deferEditor.then(() => {
-        // FIXME: Workaround for browser_net_accessibility test to
-        // make sure editor node exists while setting editor text.
-        // deferEditor workaround should be removed in bug 1308442
-        if (this.refs.editorElement) {
-          this.editor.setText(text);
-        }
-      });
-    }
-  },
-
-  componentWillUnmount() {
-    this.deferEditor.then(() => {
-      this.editor.destroy();
-      this.editor = null;
-    });
-    this.deferEditor = null;
-  },
-
-  render() {
-    const { open } = this.props;
-
-    return (
-      div({ className: "editor-container devtools-monospace" },
-        div({
-          ref: "editorElement",
-          className: "editor-mount devtools-monospace",
-          // Using visibility instead of display property to avoid breaking
-          // CodeMirror indentation
-          style: { visibility: open ? "visible" : "hidden" },
-        }),
-      )
-    );
-  }
-});
-
-module.exports = Editor;
-
-/* eslint-enable react/prop-types */
deleted file mode 100644
--- a/devtools/client/netmonitor/shared/components/headers-panel.js
+++ /dev/null
@@ -1,260 +0,0 @@
-/* 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,
-  PropTypes,
-} = require("devtools/client/shared/vendor/react");
-const { L10N } = require("../../utils/l10n");
-const { writeHeaderText } = require("../../utils/request-utils");
-const {
-  getHeadersURL,
-  getHTTPStatusCodeURL,
-} = require("../../utils/mdn-utils");
-const { getFormattedSize } = require("../../utils/format-utils");
-const { REPS, MODE } = require("devtools/client/shared/components/reps/reps");
-const Rep = createFactory(REPS.Rep);
-
-// Components
-const MDNLink = createFactory(require("./mdn-link"));
-const PropertiesView = createFactory(require("./properties-view"));
-
-const { button, div, input, textarea } = DOM;
-
-const EDIT_AND_RESEND = L10N.getStr("netmonitor.summary.editAndResend");
-const RAW_HEADERS = L10N.getStr("netmonitor.summary.rawHeaders");
-const RAW_HEADERS_REQUEST = L10N.getStr("netmonitor.summary.rawHeaders.requestHeaders");
-const RAW_HEADERS_RESPONSE = L10N.getStr("netmonitor.summary.rawHeaders.responseHeaders");
-const HEADERS_EMPTY_TEXT = L10N.getStr("headersEmptyText");
-const HEADERS_FILTER_TEXT = L10N.getStr("headersFilterText");
-const REQUEST_HEADERS = L10N.getStr("requestHeaders");
-const REQUEST_HEADERS_FROM_UPLOAD = L10N.getStr("requestHeadersFromUpload");
-const RESPONSE_HEADERS = L10N.getStr("responseHeaders");
-const SUMMARY_ADDRESS = L10N.getStr("netmonitor.summary.address");
-const SUMMARY_METHOD = L10N.getStr("netmonitor.summary.method");
-const SUMMARY_URL = L10N.getStr("netmonitor.summary.url");
-const SUMMARY_STATUS = L10N.getStr("netmonitor.summary.status");
-const SUMMARY_VERSION = L10N.getStr("netmonitor.summary.version");
-
-/*
- * Headers panel component
- * Lists basic information about the request
- */
-const HeadersPanel = createClass({
-  displayName: "HeadersPanel",
-
-  propTypes: {
-    cloneSelectedRequest: PropTypes.func.isRequired,
-    request: PropTypes.object.isRequired,
-    renderValue: PropTypes.func
-  },
-
-  getInitialState() {
-    return {
-      rawHeadersOpened: false,
-    };
-  },
-
-  getProperties(headers, title) {
-    if (headers && headers.headers.length) {
-      return {
-        [`${title} (${getFormattedSize(headers.headersSize, 3)})`]:
-          headers.headers.reduce((acc, { name, value }) =>
-            name ? Object.assign(acc, { [name]: value }) : acc
-          , {})
-      };
-    }
-
-    return null;
-  },
-
-  toggleRawHeaders() {
-    this.setState({
-      rawHeadersOpened: !this.state.rawHeadersOpened,
-    });
-  },
-
-  renderSummary(label, value) {
-    return (
-      div({ className: "tabpanel-summary-container headers-summary" },
-        div({
-          className: "tabpanel-summary-label headers-summary-label",
-        }, label),
-        input({
-          className: "tabpanel-summary-value textbox-input devtools-monospace",
-          readOnly: true,
-          value,
-        }),
-      )
-    );
-  },
-
-  renderValue(props) {
-    const member = props.member;
-    const value = props.value;
-
-    if (typeof value !== "string") {
-      return null;
-    }
-
-    let headerDocURL = getHeadersURL(member.name);
-
-    return (
-      div({ className: "treeValueCellDivider" },
-        Rep(Object.assign(props, {
-          // FIXME: A workaround for the issue in StringRep
-          // Force StringRep to crop the text everytime
-          member: Object.assign({}, member, { open: false }),
-          mode: MODE.TINY,
-          cropLimit: 60,
-        })),
-        headerDocURL ? MDNLink({
-          url: headerDocURL,
-        }) : null
-      )
-    );
-  },
-
-  render() {
-    const {
-      cloneSelectedRequest,
-      request: {
-        fromCache,
-        fromServiceWorker,
-        httpVersion,
-        method,
-        remoteAddress,
-        remotePort,
-        requestHeaders,
-        requestHeadersFromUploadStream: uploadHeaders,
-        responseHeaders,
-        status,
-        statusText,
-        urlDetails,
-      },
-    } = this.props;
-
-    if ((!requestHeaders || !requestHeaders.headers.length) &&
-        (!uploadHeaders || !uploadHeaders.headers.length) &&
-        (!responseHeaders || !responseHeaders.headers.length)) {
-      return div({ className: "empty-notice" },
-        HEADERS_EMPTY_TEXT
-      );
-    }
-
-    let object = Object.assign({},
-      this.getProperties(responseHeaders, RESPONSE_HEADERS),
-      this.getProperties(requestHeaders, REQUEST_HEADERS),
-      this.getProperties(uploadHeaders, REQUEST_HEADERS_FROM_UPLOAD),
-    );
-
-    let summaryUrl = urlDetails.unicodeUrl ?
-      this.renderSummary(SUMMARY_URL, urlDetails.unicodeUrl) : null;
-
-    let summaryMethod = method ?
-      this.renderSummary(SUMMARY_METHOD, method) : null;
-
-    let summaryAddress = remoteAddress ?
-      this.renderSummary(SUMMARY_ADDRESS,
-        remotePort ? `${remoteAddress}:${remotePort}` : remoteAddress) : null;
-
-    let summaryStatus;
-
-    if (status) {
-      let code;
-      if (fromCache) {
-        code = "cached";
-      } else if (fromServiceWorker) {
-        code = "service worker";
-      } else {
-        code = status;
-      }
-
-      let statusCodeDocURL = getHTTPStatusCodeURL(code);
-      let inputWidth = status.length + statusText.length + 1;
-
-      summaryStatus = (
-        div({ className: "tabpanel-summary-container headers-summary" },
-          div({
-            className: "tabpanel-summary-label headers-summary-label",
-          }, SUMMARY_STATUS),
-          div({
-            className: "requests-list-status-icon",
-            "data-code": code,
-          }),
-          input({
-            className: "tabpanel-summary-value textbox-input devtools-monospace"
-              + " status-text",
-            readOnly: true,
-            value: `${status} ${statusText}`,
-            size: `${inputWidth}`,
-          }),
-          statusCodeDocURL ? MDNLink({
-            url: statusCodeDocURL,
-          }) : null,
-          window.NetMonitorController.supportsCustomRequest && button({
-            className: "devtools-button",
-            onClick: cloneSelectedRequest,
-          }, EDIT_AND_RESEND),
-          button({
-            className: "devtools-button",
-            onClick: this.toggleRawHeaders,
-          }, RAW_HEADERS),
-        )
-      );
-    }
-
-    let summaryVersion = httpVersion ?
-      this.renderSummary(SUMMARY_VERSION, httpVersion) : null;
-
-    let summaryRawHeaders;
-    if (this.state.rawHeadersOpened) {
-      summaryRawHeaders = (
-        div({ className: "tabpanel-summary-container headers-summary" },
-          div({ className: "raw-headers-container" },
-            div({ className: "raw-headers" },
-              div({ className: "tabpanel-summary-label" }, RAW_HEADERS_REQUEST),
-              textarea({
-                value: writeHeaderText(requestHeaders.headers),
-                readOnly: true,
-              }),
-            ),
-            div({ className: "raw-headers" },
-              div({ className: "tabpanel-summary-label" }, RAW_HEADERS_RESPONSE),
-              textarea({
-                value: writeHeaderText(responseHeaders.headers),
-                readOnly: true,
-              }),
-            ),
-          )
-        )
-      );
-    }
-
-    return (
-      div({ className: "panel-container" },
-        div({ className: "headers-overview" },
-          summaryUrl,
-          summaryMethod,
-          summaryAddress,
-          summaryStatus,
-          summaryVersion,
-          summaryRawHeaders,
-        ),
-        PropertiesView({
-          object,
-          filterPlaceHolder: HEADERS_FILTER_TEXT,
-          sectionNames: Object.keys(object),
-          renderValue: this.renderValue,
-        }),
-      )
-    );
-  }
-});
-
-module.exports = HeadersPanel;
deleted file mode 100644
--- a/devtools/client/netmonitor/shared/components/mdn-link.js
+++ /dev/null
@@ -1,47 +0,0 @@
-/* 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 Services = require("Services");
-const {
-  DOM,
-  PropTypes,
-} = require("devtools/client/shared/vendor/react");
-const { gDevTools } = require("devtools/client/framework/devtools");
-const { L10N } = require("../../utils/l10n");
-
-const { a } = DOM;
-
-const LEARN_MORE = L10N.getStr("netmonitor.headers.learnMore");
-
-function MDNLink({ url }) {
-  return (
-    a({
-      className: "learn-more-link",
-      title: url,
-      onClick: (e) => onLearnMoreClick(e, url),
-    }, `[${LEARN_MORE}]`)
-  );
-}
-
-MDNLink.displayName = "MDNLink";
-
-MDNLink.propTypes = {
-  url: PropTypes.string.isRequired,
-};
-
-function onLearnMoreClick(e, url) {
-  e.stopPropagation();
-  e.preventDefault();
-
-  let win = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
-  if (e.button === 1) {
-    win.openUILinkIn(url, "tabshifted");
-  } else {
-    win.openUILinkIn(url, "tab");
-  }
-}
-
-module.exports = MDNLink;
deleted file mode 100644
--- a/devtools/client/netmonitor/shared/components/moz.build
+++ /dev/null
@@ -1,19 +0,0 @@
-# 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(
-    'cookies-panel.js',
-    'custom-request-panel.js',
-    'editor.js',
-    'headers-panel.js',
-    'mdn-link.js',
-    'network-details-panel.js',
-    'params-panel.js',
-    'preview-panel.js',
-    'properties-view.js',
-    'response-panel.js',
-    'security-panel.js',
-    'tabbox-panel.js',
-    'timings-panel.js',
-)
deleted file mode 100644
--- a/devtools/client/netmonitor/shared/components/network-details-panel.js
+++ /dev/null
@@ -1,70 +0,0 @@
-/* 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,
-  DOM,
-  PropTypes,
-} = require("devtools/client/shared/vendor/react");
-const { connect } = require("devtools/client/shared/vendor/react-redux");
-const Actions = require("../../actions/index");
-const { getSelectedRequest } = require("../../selectors/index");
-
-// Components
-const CustomRequestPanel = createFactory(require("./custom-request-panel"));
-const TabboxPanel = createFactory(require("./tabbox-panel"));
-
-const { div } = DOM;
-
-/*
- * Network details panel component
- */
-function NetworkDetailsPanel({
-  activeTabId,
-  cloneSelectedRequest,
-  request,
-  selectTab,
-}) {
-  if (!request) {
-    return null;
-  }
-
-  return (
-    div({ className: "network-details-panel" },
-      !request.isCustom ?
-        TabboxPanel({
-          activeTabId,
-          request,
-          selectTab,
-        }) :
-        CustomRequestPanel({
-          cloneSelectedRequest,
-          request,
-        })
-    )
-  );
-}
-
-NetworkDetailsPanel.displayName = "NetworkDetailsPanel";
-
-NetworkDetailsPanel.propTypes = {
-  activeTabId: PropTypes.string,
-  cloneSelectedRequest: PropTypes.func.isRequired,
-  open: PropTypes.bool,
-  request: PropTypes.object,
-  selectTab: PropTypes.func.isRequired,
-};
-
-module.exports = connect(
-  (state) => ({
-    activeTabId: state.ui.detailsPanelSelectedTab,
-    request: getSelectedRequest(state),
-  }),
-  (dispatch) => ({
-    cloneSelectedRequest: () => dispatch(Actions.cloneSelectedRequest()),
-    selectTab: (tabId) => dispatch(Actions.selectDetailsPanelTab(tabId)),
-  }),
-)(NetworkDetailsPanel);
deleted file mode 100644
--- a/devtools/client/netmonitor/shared/components/params-panel.js
+++ /dev/null
@@ -1,132 +0,0 @@
-/* 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,
-  DOM,
-  PropTypes,
-} = require("devtools/client/shared/vendor/react");
-const { L10N } = require("../../utils/l10n");
-const { getUrlQuery, parseQueryString } = require("../../utils/request-utils");
-
-// Components
-const PropertiesView = createFactory(require("./properties-view"));
-
-const { div } = DOM;
-
-const JSON_SCOPE_NAME = L10N.getStr("jsonScopeName");
-const PARAMS_EMPTY_TEXT = L10N.getStr("paramsEmptyText");
-const PARAMS_FILTER_TEXT = L10N.getStr("paramsFilterText");
-const PARAMS_FORM_DATA = L10N.getStr("paramsFormData");
-const PARAMS_POST_PAYLOAD = L10N.getStr("paramsPostPayload");
-const PARAMS_QUERY_STRING = L10N.getStr("paramsQueryString");
-const SECTION_NAMES = [
-  JSON_SCOPE_NAME,
-  PARAMS_FORM_DATA,
-  PARAMS_POST_PAYLOAD,
-  PARAMS_QUERY_STRING,
-];
-
-/*
- * Params panel component
- * Displays the GET parameters and POST data of a request
- */
-function ParamsPanel({
-  request,
-}) {
-  let {
-    formDataSections,
-    mimeType,
-    requestPostData,
-    url,
-  } = request;
-  let postData = requestPostData ? requestPostData.postData.text : null;
-  let query = getUrlQuery(url);
-
-  if (!formDataSections && !postData && !query) {
-    return div({ className: "empty-notice" },
-      PARAMS_EMPTY_TEXT
-    );
-  }
-
-  let object = {};
-  let json;
-
-  // Query String section
-  if (query) {
-    object[PARAMS_QUERY_STRING] = getProperties(parseQueryString(query));
-  }
-
-  // Form Data section
-  if (formDataSections && formDataSections.length > 0) {
-    let sections = formDataSections.filter((str) => /\S/.test(str)).join("&");
-    object[PARAMS_FORM_DATA] = getProperties(parseQueryString(sections));
-  }
-
-  // Request payload section
-  if (formDataSections && formDataSections.length === 0 && postData) {
-    try {
-      json = JSON.parse(postData);
-    } catch (error) {
-      // Continue regardless of parsing error
-    }
-
-    if (json) {
-      object[JSON_SCOPE_NAME] = json;
-    } else {
-      object[PARAMS_POST_PAYLOAD] = {
-        EDITOR_CONFIG: {
-          text: postData,
-          mode: mimeType.replace(/;.+/, ""),
-        },
-      };
-    }
-  } else {
-    postData = "";
-  }
-
-  return (
-    div({ className: "panel-container" },
-      PropertiesView({
-        object,
-        filterPlaceHolder: PARAMS_FILTER_TEXT,
-        sectionNames: SECTION_NAMES,
-      })
-    )
-  );
-}
-
-ParamsPanel.displayName = "ParamsPanel";
-
-ParamsPanel.propTypes = {
-  request: PropTypes.object.isRequired,
-};
-
-/**
- * Mapping array to dict for TreeView usage.
- * Since TreeView only support Object(dict) format.
- * This function also deal with duplicate key case
- * (for multiple selection and query params with same keys)
- *
- * @param {Object[]} arr - key-value pair array like query or form params
- * @returns {Object} Rep compatible object
- */
-function getProperties(arr) {
-  return arr.reduce((map, obj) => {
-    let value = map[obj.name];
-    if (value) {
-      if (typeof value !== "object") {
-        map[obj.name] = [value];
-      }
-      map[obj.name].push(obj.value);
-    } else {
-      map[obj.name] = obj.value;
-    }
-    return map;
-  }, {});
-}
-
-module.exports = ParamsPanel;
deleted file mode 100644
--- a/devtools/client/netmonitor/shared/components/preview-panel.js
+++ /dev/null
@@ -1,37 +0,0 @@
-/* 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 { DOM, PropTypes } = require("devtools/client/shared/vendor/react");
-
-const { div, iframe } = DOM;
-
-/*
- * Preview panel component
- * Display HTML content within a sandbox enabled iframe
- */
-function PreviewPanel({
-  request,
-}) {
-  const htmlBody = request.responseContent ?
-    request.responseContent.content.text : "";
-
-  return (
-    div({ className: "panel-container" },
-      iframe({
-        sandbox: "",
-        srcDoc: typeof htmlBody === "string" ? htmlBody : "",
-      })
-    )
-  );
-}
-
-PreviewPanel.displayName = "PreviewPanel";
-
-PreviewPanel.propTypes = {
-  request: PropTypes.object.isRequired,
-};
-
-module.exports = PreviewPanel;
deleted file mode 100644
--- a/devtools/client/netmonitor/shared/components/properties-view.js
+++ /dev/null
@@ -1,218 +0,0 @@
-/* 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-disable react/prop-types */
-
-"use strict";
-
-const {
-  createClass,
-  createFactory,
-  DOM,
-  PropTypes,
-} = require("devtools/client/shared/vendor/react");
-
-const { REPS, MODE } = require("devtools/client/shared/components/reps/reps");
-const Rep = createFactory(REPS.Rep);
-
-const { FILTER_SEARCH_DELAY } = require("../../constants");
-
-// Components
-const Editor = createFactory(require("devtools/client/netmonitor/shared/components/editor"));
-const SearchBox = createFactory(require("devtools/client/shared/components/search-box"));
-const TreeView = createFactory(require("devtools/client/shared/components/tree/tree-view"));
-const TreeRow = createFactory(require("devtools/client/shared/components/tree/tree-row"));
-
-const { div, tr, td } = DOM;
-const AUTO_EXPAND_MAX_LEVEL = 7;
-const AUTO_EXPAND_MAX_NODES = 50;
-const EDITOR_CONFIG_ID = "EDITOR_CONFIG";
-
-/*
- * Properties View component
- * A scrollable tree view component which provides some useful features for
- * representing object properties.
- *
- * Search filter - Set enableFilter to enable / disable SearchBox feature.
- * Tree view - Default enabled.
- * Source editor - Enable by specifying object level 1 property name to EDITOR_CONFIG_ID.
- * Rep - Default enabled.
- */
-const PropertiesView = createClass({
-  displayName: "PropertiesView",
-
-  propTypes: {
-    object: PropTypes.object,
-    enableInput: PropTypes.bool,
-    expandableStrings: PropTypes.bool,
-    filterPlaceHolder: PropTypes.string,
-    sectionNames: PropTypes.array,
-  },
-
-  getDefaultProps() {
-    return {
-      enableInput: true,
-      enableFilter: true,
-      expandableStrings: false,
-      filterPlaceHolder: "",
-      sectionNames: [],
-    };
-  },
-
-  getInitialState() {
-    return {
-      filterText: "",
-    };
-  },
-
-  getRowClass(object, sectionNames) {
-    return sectionNames.includes(object.name) ? "tree-section" : "";
-  },
-
-  onFilter(object, whiteList) {
-    let { name, value } = object;
-    let filterText = this.state.filterText;
-
-    if (!filterText || whiteList.includes(name)) {
-      return true;
-    }
-
-    let jsonString = JSON.stringify({ [name]: value }).toLowerCase();
-    return jsonString.includes(filterText.toLowerCase());
-  },
-
-  renderRowWithEditor(props) {
-    const { level, name, value, path } = props.member;
-
-    // Display source editor when specifying to EDITOR_CONFIG_ID along with config
-    if (level === 1 && name === EDITOR_CONFIG_ID) {
-      return (
-        tr({ className: "editor-row-container" },
-          td({ colSpan: 2 },
-            Editor(value)
-          )
-        )
-      );
-    }
-
-    // Skip for editor config
-    if (level >= 1 && path.includes(EDITOR_CONFIG_ID)) {
-      return null;
-    }
-
-    return TreeRow(props);
-  },
-
-  renderValueWithRep(props) {
-    const { member } = props;
-
-    // Hide strings with following conditions
-    // 1. this row is a togglable section and content is object ('cause it shouldn't hide
-    //    when string or number)
-    // 2. the `value` object has a `value` property, only happened in Cookies panel
-    // Put 2 here to not dup this method
-    if (member.level === 0 && member.type === "object" ||
-      (typeof member.value === "object" && member.value && member.value.value)) {
-      return null;
-    }
-
-    return Rep(Object.assign(props, {
-      // FIXME: A workaround for the issue in StringRep
-      // Force StringRep to crop the text everytime
-      member: Object.assign({}, member, { open: false }),
-      mode: MODE.TINY,
-      cropLimit: 60,
-    }));
-  },
-
-  shouldRenderSearchBox(object) {
-    return this.props.enableFilter && object && Object.keys(object)
-      .filter((section) => !object[section][EDITOR_CONFIG_ID]).length > 0;
-  },
-
-  updateFilterText(filterText) {
-    this.setState({
-      filterText,
-    });
-  },
-
-  getExpandedNodes: function (object, path = "", level = 0) {
-    if (typeof object != "object") {
-      return null;
-    }
-
-    if (level > AUTO_EXPAND_MAX_LEVEL) {
-      return null;
-    }
-
-    let expandedNodes = new Set();
-    for (let prop in object) {
-      if (expandedNodes.size > AUTO_EXPAND_MAX_NODES) {
-        // If we reached the limit of expandable nodes, bail out to avoid performance
-        // issues.
-        break;
-      }
-
-      let nodePath = path + "/" + prop;
-      expandedNodes.add(nodePath);
-
-      let nodes = this.getExpandedNodes(object[prop], nodePath, level + 1);
-      if (nodes) {
-        let newSize = expandedNodes.size + nodes.size;
-        if (newSize < AUTO_EXPAND_MAX_NODES) {
-          // Avoid having a subtree half expanded.
-          expandedNodes = new Set([...expandedNodes, ...nodes]);
-        }
-      }
-    }
-    return expandedNodes;
-  },
-
-  render() {
-    const {
-      decorator,
-      enableInput,
-      expandableStrings,
-      filterPlaceHolder,
-      object,
-      renderRow,
-      renderValue,
-      sectionNames,
-    } = this.props;
-
-    return (
-      div({ className: "properties-view" },
-        this.shouldRenderSearchBox(object) &&
-          div({ className: "searchbox-section" },
-            SearchBox({
-              delay: FILTER_SEARCH_DELAY,
-              type: "filter",
-              onChange: this.updateFilterText,
-              placeholder: filterPlaceHolder,
-            }),
-          ),
-        div({ className: "tree-container" },
-          TreeView({
-            object,
-            columns: [{
-              id: "value",
-              width: "100%",
-            }],
-            decorator: decorator || {
-              getRowClass: (rowObject) => this.getRowClass(rowObject, sectionNames),
-            },
-            enableInput,
-            expandableStrings,
-            expandedNodes: this.getExpandedNodes(object),
-            onFilter: (props) => this.onFilter(props, sectionNames),
-            renderRow: renderRow || this.renderRowWithEditor,
-            renderValue: renderValue || this.renderValueWithRep,
-          }),
-        ),
-      )
-    );
-  }
-});
-
-module.exports = PropertiesView;
deleted file mode 100644
--- a/devtools/client/netmonitor/shared/components/response-panel.js
+++ /dev/null
@@ -1,185 +0,0 @@
-/* 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,
-  PropTypes,
-} = require("devtools/client/shared/vendor/react");
-const { L10N } = require("../../utils/l10n");
-const { formDataURI, getUrlBaseName } = require("../../utils/request-utils");
-
-// Components
-const PropertiesView = createFactory(require("./properties-view"));
-
-const { div, img } = DOM;
-const JSON_SCOPE_NAME = L10N.getStr("jsonScopeName");
-const JSON_FILTER_TEXT = L10N.getStr("jsonFilterText");
-const RESPONSE_IMG_NAME = L10N.getStr("netmonitor.response.name");
-const RESPONSE_IMG_DIMENSIONS = L10N.getStr("netmonitor.response.dimensions");
-const RESPONSE_IMG_MIMETYPE = L10N.getStr("netmonitor.response.mime");
-const RESPONSE_PAYLOAD = L10N.getStr("responsePayload");
-
-/*
- * Response panel component
- * Displays the GET parameters and POST data of a request
- */
-const ResponsePanel = createClass({
-  displayName: "ResponsePanel",
-
-  propTypes: {
-    request: PropTypes.object.isRequired,
-  },
-
-  getInitialState() {
-    return {
-      imageDimensions: {
-        width: 0,
-        height: 0,
-      },
-    };
-  },
-
-  updateImageDimemsions({ target }) {
-    this.setState({
-      imageDimensions: {
-        width: target.naturalWidth,
-        height: target.naturalHeight,
-      },
-    });
-  },
-
-  // Handle json, which we tentatively identify by checking the MIME type
-  // for "json" after any word boundary. This works for the standard
-  // "application/json", and also for custom types like "x-bigcorp-json".
-  // Additionally, we also directly parse the response text content to
-  // verify whether it's json or not, to handle responses incorrectly
-  // labeled as text/plain instead.
-  isJSON(mimeType, response) {
-    let json, error;
-    try {
-      json = JSON.parse(response);
-    } catch (err) {
-      try {
-        json = JSON.parse(atob(response));
-      } catch (err64) {
-        error = err;
-      }
-    }
-
-    if (/\bjson/.test(mimeType) || json) {
-      // Extract the actual json substring in case this might be a "JSONP".
-      // This regex basically parses a function call and captures the
-      // function name and arguments in two separate groups.
-      let jsonpRegex = /^\s*([\w$]+)\s*\(\s*([^]*)\s*\)\s*;?\s*$/;
-      let [, jsonpCallback, jsonp] = response.match(jsonpRegex) || [];
-      let result = {};
-
-      // Make sure this is a valid JSON object first. If so, nicely display
-      // the parsing results in a tree view.
-      if (jsonpCallback && jsonp) {
-        error = null;
-        try {
-          json = JSON.parse(jsonp);
-        } catch (err) {
-          error = err;
-        }
-      }
-
-      // Valid JSON
-      if (json) {
-        result.json = json;
-      }
-      // Valid JSONP
-      if (jsonpCallback) {
-        result.jsonpCallback = jsonpCallback;
-      }
-      // Malformed JSON
-      if (error) {
-        result.error = "" + error;
-      }
-
-      return result;
-    }
-
-    return null;
-  },
-
-  render() {
-    let { responseContent, url } = this.props.request;
-
-    if (!responseContent || typeof responseContent.content.text !== "string") {
-      return null;
-    }
-
-    let { encoding, mimeType, text } = responseContent.content;
-
-    if (mimeType.includes("image/")) {
-      let { width, height } = this.state.imageDimensions;
-
-      return (
-        div({ className: "panel-container response-image-box devtools-monospace" },
-          img({
-            className: "response-image",
-            src: formDataURI(mimeType, encoding, text),
-            onLoad: this.updateImageDimemsions,
-          }),
-          div({ className: "response-summary" },
-            div({ className: "tabpanel-summary-label" }, RESPONSE_IMG_NAME),
-            div({ className: "tabpanel-summary-value" }, getUrlBaseName(url)),
-          ),
-          div({ className: "response-summary" },
-            div({ className: "tabpanel-summary-label" }, RESPONSE_IMG_DIMENSIONS),
-            div({ className: "tabpanel-summary-value" }, `${width} × ${height}`),
-          ),
-          div({ className: "response-summary" },
-            div({ className: "tabpanel-summary-label" }, RESPONSE_IMG_MIMETYPE),
-            div({ className: "tabpanel-summary-value" }, mimeType),
-          ),
-        )
-      );
-    }
-
-    // Display Properties View
-    let { json, jsonpCallback, error } = this.isJSON(mimeType, text) || {};
-    let object = {};
-    let sectionName;
-
-    if (json) {
-      if (jsonpCallback) {
-        sectionName = L10N.getFormatStr("jsonpScopeName", jsonpCallback);
-      } else {
-        sectionName = JSON_SCOPE_NAME;
-      }
-      object[sectionName] = json;
-    } else {
-      sectionName = RESPONSE_PAYLOAD;
-
-      object[sectionName] = {
-        EDITOR_CONFIG: {
-          text,
-          mode: mimeType.replace(/;.+/, ""),
-        },
-      };
-    }
-
-    return (
-      div({ className: "panel-container" },
-        error && div({ className: "response-error-header", title: error },
-          error
-        ),
-        PropertiesView({
-          object,
-          filterPlaceHolder: JSON_FILTER_TEXT,
-          sectionNames: [sectionName],
-        }),
-      )
-    );
-  }
-});
-
-module.exports = ResponsePanel;
deleted file mode 100644
--- a/devtools/client/netmonitor/shared/components/security-panel.js
+++ /dev/null
@@ -1,162 +0,0 @@
-/* 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,
-  DOM,
-  PropTypes,
-} = require("devtools/client/shared/vendor/react");
-const { L10N } = require("../../utils/l10n");
-const { getUrlHost } = require("../../utils/request-utils");
-
-// Components
-const PropertiesView = createFactory(require("./properties-view"));
-
-const { div, input, span } = DOM;
-
-/*
- * Security panel component
- * If the site is being served over HTTPS, you get an extra tab labeled "Security".
- * This contains details about the secure connection used including the protocol,
- * the cipher suite, and certificate details
- */
-function SecurityPanel({
-  request,
-}) {
-  const { securityInfo, url } = request;
-
-  if (!securityInfo || !url) {
-    return null;
-  }
-
-  const notAvailable = L10N.getStr("netmonitor.security.notAvailable");
-  let object;
-
-  if (securityInfo.state === "secure" || securityInfo.state === "weak") {
-    const { subject, issuer, validity, fingerprint } = securityInfo.cert;
-    const enabledLabel = L10N.getStr("netmonitor.security.enabled");
-    const disabledLabel = L10N.getStr("netmonitor.security.disabled");
-
-    object = {
-      [L10N.getStr("netmonitor.security.connection")]: {
-        [L10N.getStr("netmonitor.security.protocolVersion")]:
-          securityInfo.protocolVersion || notAvailable,
-        [L10N.getStr("netmonitor.security.cipherSuite")]:
-          securityInfo.cipherSuite || notAvailable,
-      },
-      [L10N.getFormatStr("netmonitor.security.hostHeader", getUrlHost(url))]: {
-        [L10N.getStr("netmonitor.security.hsts")]:
-          securityInfo.hsts ? enabledLabel : disabledLabel,
-        [L10N.getStr("netmonitor.security.hpkp")]:
-          securityInfo.hpkp ? enabledLabel : disabledLabel,
-      },
-      [L10N.getStr("netmonitor.security.certificate")]: {
-        [L10N.getStr("certmgr.subjectinfo.label")]: {
-          [L10N.getStr("certmgr.certdetail.cn")]:
-            subject.commonName || notAvailable,
-          [L10N.getStr("certmgr.certdetail.o")]:
-            subject.organization || notAvailable,
-          [L10N.getStr("certmgr.certdetail.ou")]:
-            subject.organizationUnit || notAvailable,
-        },
-        [L10N.getStr("certmgr.issuerinfo.label")]: {
-          [L10N.getStr("certmgr.certdetail.cn")]:
-            issuer.commonName || notAvailable,
-          [L10N.getStr("certmgr.certdetail.o")]:
-            issuer.organization || notAvailable,
-          [L10N.getStr("certmgr.certdetail.ou")]:
-            issuer.organizationUnit || notAvailable,
-        },
-        [L10N.getStr("certmgr.periodofvalidity.label")]: {
-          [L10N.getStr("certmgr.begins")]:
-            validity.start || notAvailable,
-          [L10N.getStr("certmgr.expires")]:
-            validity.end || notAvailable,
-        },
-        [L10N.getStr("certmgr.fingerprints.label")]: {
-          [L10N.getStr("certmgr.certdetail.sha256fingerprint")]:
-            fingerprint.sha256 || notAvailable,
-          [L10N.getStr("certmgr.certdetail.sha1fingerprint")]:
-            fingerprint.sha1 || notAvailable,
-        },
-      },
-    };
-  } else {
-    object = {
-      [L10N.getStr("netmonitor.security.error")]:
-        new DOMParser().parseFromString(securityInfo.errorMessage, "text/html")
-          .body.textContent || notAvailable
-    };
-  }
-
-  return div({ className: "panel-container security-panel" },
-    PropertiesView({
-      object,
-      renderValue: (props) => renderValue(props, securityInfo.weaknessReasons),
-      enableFilter: false,
-      expandedNodes: getExpandedNodes(object),
-    })
-  );
-}
-
-SecurityPanel.displayName = "SecurityPanel";
-
-SecurityPanel.propTypes = {
-  request: PropTypes.object.isRequired,
-};
-
-function renderValue(props, weaknessReasons = []) {
-  const { member, value } = props;
-
-  // Hide object summary
-  if (typeof member.value === "object") {
-    return null;
-  }
-
-  return span({ className: "security-info-value" },
-    member.name === L10N.getStr("netmonitor.security.error") ?
-      // Display multiline text for security error
-      value
-      :
-      // Display one line selectable text for security details
-      input({
-        className: "textbox-input",
-        readOnly: "true",
-        value,
-      })
-    ,
-    weaknessReasons.indexOf("cipher") !== -1 &&
-    member.name === L10N.getStr("netmonitor.security.cipherSuite") ?
-      // Display an extra warning icon after the cipher suite
-      div({
-        id: "security-warning-cipher",
-        className: "security-warning-icon",
-        title: L10N.getStr("netmonitor.security.warning.cipher"),
-      })
-      :
-      null
-  );
-}
-
-function getExpandedNodes(object, path = "", level = 0) {
-  if (typeof object !== "object") {
-    return null;
-  }
-
-  let expandedNodes = new Set();
-  for (let prop in object) {
-    let nodePath = path + "/" + prop;
-    expandedNodes.add(nodePath);
-
-    let nodes = getExpandedNodes(object[prop], nodePath, level + 1);
-    if (nodes) {
-      expandedNodes = new Set([...expandedNodes, ...nodes]);
-    }
-  }
-  return expandedNodes;
-}
-
-module.exports = SecurityPanel;
deleted file mode 100644
--- a/devtools/client/netmonitor/shared/components/tabbox-panel.js
+++ /dev/null
@@ -1,123 +0,0 @@
-/* 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,
-  PropTypes,
-} = require("devtools/client/shared/vendor/react");
-const { connect } = require("devtools/client/shared/vendor/react-redux");
-const Actions = require("../../actions/index");
-const { Filters } = require("../../utils/filter-predicates");
-const { L10N } = require("../../utils/l10n");
-const { getSelectedRequest } = require("../../selectors/index");
-
-// Components
-const Tabbar = createFactory(require("devtools/client/shared/components/tabs/tabbar"));
-const TabPanel = createFactory(require("devtools/client/shared/components/tabs/tabs").TabPanel);
-const CookiesPanel = createFactory(require("./cookies-panel"));
-const HeadersPanel = createFactory(require("./headers-panel"));
-const ParamsPanel = createFactory(require("./params-panel"));
-const PreviewPanel = createFactory(require("./preview-panel"));
-const ResponsePanel = createFactory(require("./response-panel"));
-const SecurityPanel = createFactory(require("./security-panel"));
-const TimingsPanel = createFactory(require("./timings-panel"));
-
-const HEADERS_TITLE = L10N.getStr("netmonitor.tab.headers");
-const COOKIES_TITLE = L10N.getStr("netmonitor.tab.cookies");
-const PARAMS_TITLE = L10N.getStr("netmonitor.tab.params");
-const RESPONSE_TITLE = L10N.getStr("netmonitor.tab.response");
-const TIMINGS_TITLE = L10N.getStr("netmonitor.tab.timings");
-const SECURITY_TITLE = L10N.getStr("netmonitor.tab.security");
-const PREVIEW_TITLE = L10N.getStr("netmonitor.tab.preview");
-
-/*
- * Tabbox panel component
- * Display the network request details
- */
-function TabboxPanel({
-  activeTabId,
-  cloneSelectedRequest,
-  request,
-  selectTab,
-}) {
-  if (!request) {
-    return null;
-  }
-
-  return (
-    Tabbar({
-      activeTabId,
-      onSelect: selectTab,
-      renderOnlySelected: true,
-      showAllTabsMenu: true,
-    },
-      TabPanel({
-        id: "headers",
-        title: HEADERS_TITLE,
-      },
-        HeadersPanel({ request, cloneSelectedRequest }),
-      ),
-      TabPanel({
-        id: "cookies",
-        title: COOKIES_TITLE,
-      },
-        CookiesPanel({ request }),
-      ),
-      TabPanel({
-        id: "params",
-        title: PARAMS_TITLE,
-      },
-        ParamsPanel({ request }),
-      ),
-      TabPanel({
-        id: "response",
-        title: RESPONSE_TITLE,
-      },
-        ResponsePanel({ request }),
-      ),
-      TabPanel({
-        id: "timings",
-        title: TIMINGS_TITLE,
-      },
-        TimingsPanel({ request }),
-      ),
-      request.securityState && request.securityState !== "insecure" &&
-      TabPanel({
-        id: "security",
-        title: SECURITY_TITLE,
-      },
-        SecurityPanel({ request }),
-      ),
-      Filters.html(request) &&
-      TabPanel({
-        id: "preview",
-        title: PREVIEW_TITLE,
-      },
-        PreviewPanel({ request }),
-      ),
-    )
-  );
-}
-
-TabboxPanel.displayName = "TabboxPanel";
-
-TabboxPanel.propTypes = {
-  activeTabId: PropTypes.string,
-  cloneSelectedRequest: PropTypes.func.isRequired,
-  request: PropTypes.object,
-  selectTab: PropTypes.func.isRequired,
-};
-
-module.exports = connect(
-  (state) => ({
-    activeTabId: state.ui.detailsPanelSelectedTab,
-    request: getSelectedRequest(state),
-  }),
-  (dispatch) => ({
-    cloneSelectedRequest: () => dispatch(Actions.cloneSelectedRequest()),
-    selectTab: (tabId) => dispatch(Actions.selectDetailsPanelTab(tabId)),
-  }),
-)(TabboxPanel);
deleted file mode 100644
--- a/devtools/client/netmonitor/shared/components/timings-panel.js
+++ /dev/null
@@ -1,72 +0,0 @@
-/* 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 { DOM, PropTypes } = require("devtools/client/shared/vendor/react");
-const { L10N } = require("../../utils/l10n");
-
-const { div, span } = DOM;
-const types = ["blocked", "dns", "connect", "send", "wait", "receive"];
-const TIMINGS_END_PADDING = "80px";
-
-/*
- * Timings panel component
- * Display timeline bars that shows the total wait time for various stages
- */
-function TimingsPanel({
-  request,
-}) {
-  if (!request.eventTimings) {
-    return null;
-  }
-
-  const { timings, totalTime } = request.eventTimings;
-  const timelines = types.map((type, idx) => {
-    // Determine the relative offset for each timings box. For example, the
-    // offset of third timings box will be 0 + blocked offset + dns offset
-    const offset = types
-      .slice(0, idx)
-      .reduce((acc, cur) => (acc + timings[cur] || 0), 0);
-    const offsetScale = offset / totalTime || 0;
-    const timelineScale = timings[type] / totalTime || 0;
-
-    return div({
-      key: type,
-      id: `timings-summary-${type}`,
-      className: "tabpanel-summary-container timings-container",
-    },
-      span({ className: "tabpanel-summary-label timings-label" },
-        L10N.getStr(`netmonitor.timings.${type}`)
-      ),
-      div({ className: "requests-list-timings-container" },
-        span({
-          className: "requests-list-timings-offset",
-          style: {
-            width: `calc(${offsetScale} * (100% - ${TIMINGS_END_PADDING})`,
-          },
-        }),
-        span({
-          className: `requests-list-timings-box ${type}`,
-          style: {
-            width: `calc(${timelineScale} * (100% - ${TIMINGS_END_PADDING}))`,
-          },
-        }),
-        span({ className: "requests-list-timings-total" },
-          L10N.getFormatStr("networkMenu.totalMS", timings[type])
-        )
-      ),
-    );
-  });
-
-  return div({ className: "panel-container" }, timelines);
-}
-
-TimingsPanel.displayName = "TimingsPanel";
-
-TimingsPanel.propTypes = {
-  request: PropTypes.object.isRequired,
-};
-
-module.exports = TimingsPanel;
deleted file mode 100644
--- a/devtools/client/netmonitor/shared/moz.build
+++ /dev/null
@@ -1,7 +0,0 @@
-# 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 += [
-    'components',
-]