Bug 1309826 - convert netmonitor xul to xhtml r?honza,gasolin draft
authorRicky Chien <rchien@mozilla.com>
Mon, 13 Feb 2017 20:25:50 +0800
changeset 484622 96296a95beec5c26e22850b56ca3a7c4b76eb3a5
parent 484621 e526cc5e30ec72bebd7b393918994e4659d4af7a
child 545827 9f237caae9e22f8ba60cf2c02f7f3fa8b68bdf57
push id45523
push userbmo:rchien@mozilla.com
push dateWed, 15 Feb 2017 15:28:51 +0000
reviewershonza, gasolin
bugs1309826
milestone54.0a1
Bug 1309826 - convert netmonitor xul to xhtml r?honza,gasolin MozReview-Commit-ID: EExFUrgZqb7
devtools/client/definitions.js
devtools/client/jar.mn
devtools/client/netmonitor/components/monitor-panel.js
devtools/client/netmonitor/components/network-monitor.js
devtools/client/netmonitor/components/request-list-content.js
devtools/client/netmonitor/components/request-list-item.js
devtools/client/netmonitor/components/statistics-panel.js
devtools/client/netmonitor/netmonitor-controller.js
devtools/client/netmonitor/netmonitor.js
devtools/client/netmonitor/netmonitor.xhtml
devtools/client/netmonitor/netmonitor.xul
devtools/client/netmonitor/panel.js
devtools/client/netmonitor/request-list-context-menu.js
devtools/client/netmonitor/request-list-tooltip.js
devtools/client/netmonitor/request-utils.js
devtools/client/netmonitor/test/head.js
devtools/client/themes/netmonitor.css
--- a/devtools/client/definitions.js
+++ b/devtools/client/definitions.js
@@ -302,17 +302,17 @@ Tools.netMonitor = {
   id: "netmonitor",
   accesskey: l10n("netmonitor.accesskey"),
   key: l10n("netmonitor.commandkey"),
   ordinal: 9,
   modifiers: osString == "Darwin" ? "accel,alt" : "accel,shift",
   visibilityswitch: "devtools.netmonitor.enabled",
   icon: "chrome://devtools/skin/images/tool-network.svg",
   invertIconForDarkTheme: true,
-  url: "chrome://devtools/content/netmonitor/netmonitor.xul",
+  url: "chrome://devtools/content/netmonitor/netmonitor.xhtml",
   label: l10n("netmonitor.label"),
   panelLabel: l10n("netmonitor.panelLabel"),
   get tooltip() {
     return l10n("netmonitor.tooltip2",
     (osString == "Darwin" ? "Cmd+Opt+" : "Ctrl+Shift+") + this.key);
   },
   inMenu: true,
 
--- a/devtools/client/jar.mn
+++ b/devtools/client/jar.mn
@@ -5,17 +5,17 @@
 devtools.jar:
 %   content devtools %content/
     content/shared/vendor/d3.js (shared/vendor/d3.js)
     content/shared/vendor/dagre-d3.js (shared/vendor/dagre-d3.js)
     content/shared/widgets/widgets.css (shared/widgets/widgets.css)
     content/shared/widgets/VariablesView.xul (shared/widgets/VariablesView.xul)
     content/projecteditor/chrome/content/projecteditor.xul (projecteditor/chrome/content/projecteditor.xul)
     content/projecteditor/lib/helpers/readdir.js (projecteditor/lib/helpers/readdir.js)
-    content/netmonitor/netmonitor.xul (netmonitor/netmonitor.xul)
+    content/netmonitor/netmonitor.xhtml (netmonitor/netmonitor.xhtml)
     content/netmonitor/netmonitor.js (netmonitor/netmonitor.js)
     content/webconsole/webconsole.xul (webconsole/webconsole.xul)
 *   content/scratchpad/scratchpad.xul (scratchpad/scratchpad.xul)
     content/scratchpad/scratchpad.js (scratchpad/scratchpad.js)
     content/shared/splitview.css (shared/splitview.css)
     content/shared/theme-switching.js (shared/theme-switching.js)
     content/shared/frame-script-utils.js (shared/frame-script-utils.js)
     content/styleeditor/styleeditor.xul (styleeditor/styleeditor.xul)
--- a/devtools/client/netmonitor/components/monitor-panel.js
+++ b/devtools/client/netmonitor/components/monitor-panel.js
@@ -98,17 +98,17 @@ const MonitorPanel = createClass({
   },
 
   render() {
     let { isEmpty, networkDetailsOpen } = this.props;
     return (
       div({ className: "monitor-panel" },
         Toolbar(),
         SplitBox({
-          className: "devtools-responsive-container split-box",
+          className: "devtools-responsive-container",
           initialWidth: `${Prefs.networkDetailsWidth}px`,
           initialHeight: `${Prefs.networkDetailsHeight}px`,
           minSize: "50px",
           maxSize: "80%",
           splitterSize: "1px",
           startPanel: RequestList({ isEmpty }),
           endPanel: networkDetailsOpen ?
             NetworkDetailsPanel({
--- a/devtools/client/netmonitor/components/network-monitor.js
+++ b/devtools/client/netmonitor/components/network-monitor.js
@@ -17,17 +17,17 @@ const StatisticsPanel = createFactory(re
 
 const { div } = DOM;
 
 /*
  * Network monitor component
  */
 function NetworkMonitor({ statisticsOpen }) {
   return (
-    div({ className: "network-monitor theme-sidebar" },
+    div({ className: "network-monitor" },
       !statisticsOpen ? MonitoPanel() : StatisticsPanel()
     )
   );
 }
 
 NetworkMonitor.displayName = "NetworkMonitor";
 
 NetworkMonitor.propTypes = {
--- a/devtools/client/netmonitor/components/request-list-content.js
+++ b/devtools/client/netmonitor/components/request-list-content.js
@@ -8,17 +8,16 @@ const { KeyCodes } = require("devtools/c
 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 { Task } = require("devtools/shared/task");
 const Actions = require("../actions/index");
 const {
   setTooltipImageContent,
   setTooltipStackTraceContent,
 } = require("../request-list-tooltip");
 const {
   getDisplayedRequests,
   getWaterfallScale,
@@ -140,17 +139,17 @@ const RequestListContent = createClass({
    * 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: Task.async(function* (target, tooltip) {
+  onHover(target, tooltip) {
     let itemEl = target.closest(".request-list-item");
     if (!itemEl) {
       return false;
     }
     let itemId = itemEl.dataset.id;
     if (!itemId) {
       return false;
     }
@@ -161,17 +160,17 @@ const RequestListContent = createClass({
 
     if (requestItem.responseContent && target.closest(".requests-menu-icon-and-file")) {
       return setTooltipImageContent(tooltip, itemEl, requestItem);
     } else if (requestItem.cause && target.closest(".requests-menu-cause-stack")) {
       return setTooltipStackTraceContent(tooltip, requestItem);
     }
 
     return false;
-  }),
+  },
 
   /**
    * Scroll listener for the requests menu view.
    */
   onScroll() {
     this.tooltip.hide();
   },
 
@@ -221,25 +220,16 @@ const RequestListContent = createClass({
   /**
    * 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;
   },
 
-  /**
-   * If a focused item was unmounted, transfer the focus to the container element.
-   */
-  onFocusedNodeUnmount() {
-    if (this.refs.contentEl) {
-      this.refs.contentEl.focus();
-    }
-  },
-
   render() {
     const {
       displayedRequests,
       firstRequestStartedMillis,
       selectedRequestId,
       onItemMouseDown,
       onSecurityIconClick,
     } = this.props;
@@ -247,25 +237,24 @@ const RequestListContent = createClass({
     return (
       div({
         ref: "contentEl",
         className: "requests-menu-contents",
         tabIndex: 0,
         onKeyDown: this.onKeyDown,
       },
         displayedRequests.map((item, index) => RequestListItem({
-          key: item.id,
+          firstRequestStartedMillis,
           item,
           index,
           isSelected: item.id === selectedRequestId,
-          firstRequestStartedMillis,
-          onMouseDown: () => onItemMouseDown(item.id),
+          key: item.id,
           onContextMenu: this.onContextMenu,
           onFocusedNodeChange: this.onFocusedNodeChange,
-          onFocusedNodeUnmount: this.onFocusedNodeUnmount,
+          onMouseDown: () => onItemMouseDown(item.id),
           onSecurityIconClick: () => onSecurityIconClick(item.securityState),
         }))
       )
     );
   },
 });
 
 module.exports = connect(
--- a/devtools/client/netmonitor/components/request-list-item.js
+++ b/devtools/client/netmonitor/components/request-list-item.js
@@ -1,14 +1,12 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-/* eslint-disable react/prop-types */
-
 "use strict";
 
 const {
   createClass,
   createFactory,
   DOM,
   PropTypes,
 } = require("devtools/client/shared/vendor/react");
@@ -64,17 +62,16 @@ const RequestListItem = createClass({
 
   propTypes: {
     item: PropTypes.object.isRequired,
     index: PropTypes.number.isRequired,
     isSelected: PropTypes.bool.isRequired,
     firstRequestStartedMillis: PropTypes.number.isRequired,
     onContextMenu: PropTypes.func.isRequired,
     onFocusedNodeChange: PropTypes.func,
-    onFocusedNodeUnmount: PropTypes.func,
     onMouseDown: PropTypes.func.isRequired,
     onSecurityIconClick: PropTypes.func.isRequired,
   },
 
   componentDidMount() {
     if (this.props.isSelected) {
       this.refs.el.focus();
     }
@@ -89,42 +86,28 @@ const RequestListItem = createClass({
     if (!prevProps.isSelected && this.props.isSelected) {
       this.refs.el.focus();
       if (this.props.onFocusedNodeChange) {
         this.props.onFocusedNodeChange();
       }
     }
   },
 
-  componentWillUnmount() {
-    // If this node is being destroyed and has focus, transfer the focus manually
-    // to the parent tree component. Otherwise, the focus will get lost and keyboard
-    // navigation in the tree will stop working. This is a workaround for a XUL bug.
-    // See bugs 1259228 and 1152441 for details.
-    // DE-XUL: Remove this hack once all usages are only in HTML documents.
-    if (this.props.isSelected) {
-      this.refs.el.blur();
-      if (this.props.onFocusedNodeUnmount) {
-        this.props.onFocusedNodeUnmount();
-      }
-    }
-  },
-
   render() {
     const {
       item,
       index,
       isSelected,
       firstRequestStartedMillis,
       onContextMenu,
       onMouseDown,
       onSecurityIconClick
     } = this.props;
 
-    let classList = [ "request-list-item" ];
+    let classList = ["request-list-item"];
     if (isSelected) {
       classList.push("selected");
     }
     classList.push(index % 2 ? "odd" : "even");
 
     return (
       div({
         ref: "el",
@@ -153,16 +136,20 @@ const UPDATED_STATUS_PROPS = [
   "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;
@@ -194,16 +181,20 @@ const StatusColumn = createFactory(creat
       )
     );
   }
 }));
 
 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-menu-subitem requests-menu-method-box" },
@@ -216,16 +207,20 @@ const MethodColumn = createFactory(creat
 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 (
@@ -251,25 +246,30 @@ 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 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}`);
     }
@@ -287,16 +287,20 @@ const DomainColumn = createFactory(creat
       )
     );
   }
 }));
 
 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 = "";
@@ -329,16 +333,20 @@ 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) {
@@ -361,16 +369,20 @@ 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 } = this.props.item;
 
     let text;
@@ -396,16 +408,20 @@ const TransferredSizeColumn = createFact
       )
     );
   }
 }));
 
 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;
@@ -429,16 +445,21 @@ const UPDATED_WATERFALL_PROPS = [
   "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;
 
@@ -480,23 +501,21 @@ function timingBoxes(item) {
           key,
           className: "requests-menu-timings-box " + key,
           style: { width }
         }));
       }
     }
   }
 
-  if (typeof totalTime == "number") {
+  if (typeof totalTime === "number") {
     let text = L10N.getFormatStr("networkMenu.totalMS", totalTime);
     boxes.push(div({
       key: "total",
       className: "requests-menu-timings-total",
-      title: text
+      title: text,
     }, text));
   }
 
   return boxes;
 }
 
 module.exports = RequestListItem;
-
-/* eslint-enable react/prop-types */
--- a/devtools/client/netmonitor/components/statistics-panel.js
+++ b/devtools/client/netmonitor/components/statistics-panel.js
@@ -16,16 +16,17 @@ const Actions = require("../actions/inde
 const { Filters } = require("../filter-predicates");
 const { L10N } = require("../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
@@ -36,17 +37,25 @@ 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",
@@ -56,16 +65,20 @@ const StatisticsPanel = createClass({
 
     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: "",
@@ -212,29 +225,43 @@ const StatisticsPanel = createClass({
     // 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 devtools-responsive-container" },
+        div({ className: "charts-container" },
           div({ ref: "primedCacheChart", className: "charts primed-cache-chart" }),
-          div({ className: "splitter devtools-side-splitter" }),
+          div({ className: splitterClassName.join(" ") }),
           div({ ref: "emptyCacheChart", className: "charts empty-cache-chart" }),
         ),
       )
     );
   }
 });
 
 module.exports = connect(
--- a/devtools/client/netmonitor/netmonitor-controller.js
+++ b/devtools/client/netmonitor/netmonitor-controller.js
@@ -4,17 +4,16 @@
 
 "use strict";
 
 const promise = require("promise");
 const Services = require("Services");
 const EventEmitter = require("devtools/shared/event-emitter");
 const { TimelineFront } = require("devtools/shared/fronts/timeline");
 const { CurlUtils } = require("devtools/client/shared/curl");
-const { Task } = require("devtools/shared/task");
 const { ACTIVITY_TYPE } = require("./constants");
 const { EVENTS } = require("./events");
 const { configureStore } = require("./store");
 const Actions = require("./actions/index");
 const {
   fetchHeaders,
   formDataURI,
 } = require("./request-utils");
@@ -30,55 +29,55 @@ const gStore = window.gStore = configure
  */
 var NetMonitorController = {
   /**
    * Initializes the view and connects the monitor client.
    *
    * @return object
    *         A promise that is resolved when the monitor finishes startup.
    */
-  startupNetMonitor: Task.async(function* () {
+  async startupNetMonitor() {
     if (this._startup) {
       return this._startup.promise;
     }
     this._startup = promise.defer();
-    yield this.connect();
+    await this.connect();
     this._startup.resolve();
     return undefined;
-  }),
+  },
 
   /**
    * Destroys the view and disconnects the monitor client from the server.
    *
    * @return object
    *         A promise that is resolved when the monitor finishes shutdown.
    */
-  shutdownNetMonitor: Task.async(function* () {
+  async shutdownNetMonitor() {
     if (this._shutdown) {
       return this._shutdown.promise;
     }
     this._shutdown = promise.defer();
     gStore.dispatch(Actions.batchReset());
     this.TargetEventsHandler.disconnect();
     this.NetworkEventsHandler.disconnect();
-    yield this.disconnect();
+    await this.disconnect();
     this._shutdown.resolve();
     return undefined;
-  }),
+  },
 
   /**
    * Initiates remote or chrome network monitoring based on the current target,
    * wiring event handlers as necessary. Since the TabTarget will have already
    * started listening to network requests by now, this is largely
    * netmonitor-specific initialization.
    *
    * @return object
    *         A promise that is resolved when the monitor finishes connecting.
    */
-  connect: Task.async(function* () {
+  async connect() {
     if (this._connection) {
       return this._connection.promise;
     }
     this._connection = promise.defer();
 
     // Some actors like AddonActor or RootActor for chrome debugging
     // aren't actual tabs.
     if (this._target.isTabActor) {
@@ -92,58 +91,58 @@ var NetMonitorController = {
         this.timelineFront = new TimelineFront(this._target.client,
           this._target.form);
         return this.timelineFront.start({ withDocLoadingEvents: true });
       }
       return undefined;
     };
 
     this.webConsoleClient = this._target.activeConsole;
-    yield connectTimeline();
+    await connectTimeline();
 
     this.TargetEventsHandler.connect();
     this.NetworkEventsHandler.connect();
 
     window.emit(EVENTS.CONNECTED);
 
     this._connection.resolve();
     this._connected = true;
     return undefined;
-  }),
+  },
 
   /**
    * Disconnects the debugger client and removes event handlers as necessary.
    */
-  disconnect: Task.async(function* () {
+  async disconnect() {
     if (this._disconnection) {
       return this._disconnection.promise;
     }
     this._disconnection = promise.defer();
 
     // Wait for the connection to finish first.
     if (!this.isConnected()) {
-      yield this._connection.promise;
+      await this._connection.promise;
     }
 
     // When debugging local or a remote instance, the connection is closed by
     // the RemoteTarget. The webconsole actor is stopped on disconnect.
     this.tabClient = null;
     this.webConsoleClient = null;
 
     // The timeline front wasn't initialized and started if the server wasn't
     // recent enough to emit the markers we were interested in.
     if (this._target.getTrait("documentLoadingMarkers")) {
-      yield this.timelineFront.destroy();
+      await this.timelineFront.destroy();
       this.timelineFront = null;
     }
 
     this._disconnection.resolve();
     this._connected = false;
     return undefined;
-  }),
+  },
 
   /**
    * Checks whether the netmonitor connection is active.
    * @return boolean
    */
   isConnected: function () {
     return !!this._connected;
   },
@@ -561,133 +560,133 @@ NetworkEventsHandler.prototype = {
         fromCache,
         fromServiceWorker,
       },
       true
     ))
     .then(() => window.emit(EVENTS.REQUEST_ADDED, id));
   },
 
-  updateRequest: Task.async(function* (id, data) {
+  async updateRequest(id, data) {
     const action = Actions.updateRequest(id, data, true);
-    yield gStore.dispatch(action);
+    await gStore.dispatch(action);
     let {
       responseContent,
       responseCookies,
       responseHeaders,
       requestCookies,
       requestHeaders,
       requestPostData,
     } = action.data;
     let request = getRequestById(gStore.getState(), action.id);
 
     if (requestHeaders && requestHeaders.headers && requestHeaders.headers.length) {
-      let headers = yield fetchHeaders(requestHeaders, this.getString);
+      let headers = await fetchHeaders(requestHeaders, this.getString);
       if (headers) {
-        yield gStore.dispatch(Actions.updateRequest(
+        await gStore.dispatch(Actions.updateRequest(
           action.id,
           { requestHeaders: headers },
           true,
         ));
       }
     }
 
     if (responseHeaders && responseHeaders.headers && responseHeaders.headers.length) {
-      let headers = yield fetchHeaders(responseHeaders, this.getString);
+      let headers = await fetchHeaders(responseHeaders, this.getString);
       if (headers) {
-        yield gStore.dispatch(Actions.updateRequest(
+        await gStore.dispatch(Actions.updateRequest(
           action.id,
           { responseHeaders: headers },
           true,
         ));
       }
     }
 
     if (request && responseContent && responseContent.content) {
       let { mimeType } = request;
       let { text, encoding } = responseContent.content;
-      let response = yield this.getString(text);
+      let response = await this.getString(text);
       let payload = {};
 
       if (mimeType.includes("image/")) {
         payload.responseContentDataUri = formDataURI(mimeType, encoding, response);
       }
 
       responseContent.content.text = response;
       payload.responseContent = responseContent;
 
-      yield gStore.dispatch(Actions.updateRequest(action.id, payload, true));
+      await gStore.dispatch(Actions.updateRequest(action.id, payload, true));
 
       if (mimeType.includes("image/")) {
         window.emit(EVENTS.RESPONSE_IMAGE_THUMBNAIL_DISPLAYED);
       }
     }
 
     // Search the POST data upload stream for request headers and add
     // them as a separate property, different from the classic headers.
     if (requestPostData && requestPostData.postData) {
       let { text } = requestPostData.postData;
-      let postData = yield this.getString(text);
+      let postData = await this.getString(text);
       const headers = CurlUtils.getHeadersFromMultipartText(postData);
       const headersSize = headers.reduce((acc, { name, value }) => {
         return acc + name.length + value.length + 2;
       }, 0);
       let payload = {};
       requestPostData.postData.text = postData;
       payload.requestPostData = Object.assign({}, requestPostData);
       payload.requestHeadersFromUploadStream = { headers, headersSize };
 
-      yield gStore.dispatch(Actions.updateRequest(action.id, payload, true));
+      await gStore.dispatch(Actions.updateRequest(action.id, payload, true));
     }
 
     // Fetch request and response cookies long value.
     // Actor does not provide full sized cookie value when the value is too long
     // To display values correctly, we need fetch them in each request.
     if (requestCookies) {
       let reqCookies = [];
       // request store cookies in requestCookies or requestCookies.cookies
       let cookies = requestCookies.cookies ?
         requestCookies.cookies : requestCookies;
       // make sure cookies is iterable
       if (typeof cookies[Symbol.iterator] === "function") {
         for (let cookie of cookies) {
           reqCookies.push(Object.assign({}, cookie, {
-            value: yield this.getString(cookie.value),
+            value: await this.getString(cookie.value),
           }));
         }
         if (reqCookies.length) {
-          yield gStore.dispatch(Actions.updateRequest(
+          await gStore.dispatch(Actions.updateRequest(
             action.id,
             { requestCookies: reqCookies },
             true));
         }
       }
     }
 
     if (responseCookies) {
       let resCookies = [];
       // response store cookies in responseCookies or responseCookies.cookies
       let cookies = responseCookies.cookies ?
         responseCookies.cookies : responseCookies;
       // make sure cookies is iterable
       if (typeof cookies[Symbol.iterator] === "function") {
         for (let cookie of cookies) {
           resCookies.push(Object.assign({}, cookie, {
-            value: yield this.getString(cookie.value),
+            value: await this.getString(cookie.value),
           }));
         }
         if (resCookies.length) {
-          yield gStore.dispatch(Actions.updateRequest(
+          await gStore.dispatch(Actions.updateRequest(
             action.id,
             { responseCookies: resCookies },
             true));
         }
       }
     }
-  }),
+  },
 
   /**
    * The "networkEventUpdate" message type handler.
    *
    * @param string type
    *        Message type.
    * @param object packet
    *        The message received from the server.
@@ -897,22 +896,11 @@ NetworkEventsHandler.prototype = {
  */
 EventEmitter.decorate(window);
 
 /**
  * Preliminary setup for the NetMonitorController object.
  */
 NetMonitorController.TargetEventsHandler = new TargetEventsHandler();
 NetMonitorController.NetworkEventsHandler = new NetworkEventsHandler();
-
-/**
- * Export some properties to the global scope for easier access.
- */
-Object.defineProperties(window, {
-  "gNetwork": {
-    get: function () {
-      return NetMonitorController.NetworkEventsHandler;
-    },
-    configurable: true
-  }
-});
+window.gNetwork = NetMonitorController.NetworkEventsHandler;
 
 exports.NetMonitorController = NetMonitorController;
--- a/devtools/client/netmonitor/netmonitor.js
+++ b/devtools/client/netmonitor/netmonitor.js
@@ -1,42 +1,31 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-/* exported Netmonitor, NetMonitorController */
-
 "use strict";
 
-const Cu = Components.utils;
-const { BrowserLoader } = Cu.import("resource://devtools/client/shared/browser-loader.js", {});
-
 function Netmonitor(toolbox) {
-  const require = window.windowRequire = BrowserLoader({
-    baseURI: "resource://devtools/client/netmonitor/",
-    window,
-    commonLibRequire: toolbox.browserRequire,
-  }).require;
-
   window.NetMonitorController = require("./netmonitor-controller").NetMonitorController;
   window.NetMonitorController._toolbox = toolbox;
   window.NetMonitorController._target = toolbox.target;
 }
 
 Netmonitor.prototype = {
   init() {
     const require = window.windowRequire;
     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);
 
     // Components
     const NetworkMonitor = createFactory(require("./components/network-monitor"));
 
-    this.networkMonitor = document.querySelector("#react-network-monitor-hook");
+    this.networkMonitor = document.querySelector(".root");
 
     render(Provider(
       { store: window.gStore },
       NetworkMonitor({ toolbox: window.NetMonitorController._toolbox }),
     ), this.networkMonitor);
 
     return window.NetMonitorController.startupNetMonitor();
   },
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/netmonitor.xhtml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" dir="">
+  <head>
+    <link rel="stylesheet" href="chrome://devtools/content/shared/widgets/widgets.css"/>
+    <link rel="stylesheet" href="chrome://devtools/skin/widgets.css"/>
+    <link rel="stylesheet" href="chrome://devtools/skin/netmonitor.css"/>
+
+    <script src="chrome://devtools/content/shared/theme-switching.js"/>
+    <script>
+      "use strict";
+
+      const { BrowserLoader } = Components.utils.import(
+        "resource://devtools/client/shared/browser-loader.js", {});
+      const { require } = BrowserLoader({
+        baseURI: "resource://devtools/client/netmonitor/",
+        window,
+      });
+      window.windowRequire = require;
+    </script>
+  </head>
+  <body class="theme-sidebar" role="application">
+    <div class="root"></div>
+    <script src="netmonitor.js" defer="true"/>
+  </body>
+</html>
deleted file mode 100644
--- a/devtools/client/netmonitor/netmonitor.xul
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- This Source Code Form is subject to the terms of the Mozilla Public
-   - License, v. 2.0. If a copy of the MPL was not distributed with this
-   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
-<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
-<?xml-stylesheet href="chrome://devtools/content/shared/widgets/widgets.css" type="text/css"?>
-<?xml-stylesheet href="chrome://devtools/skin/widgets.css" type="text/css"?>
-<?xml-stylesheet href="chrome://devtools/skin/netmonitor.css" type="text/css"?>
-
-<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
-        xmlns:html="http://www.w3.org/1999/xhtml">
-
-  <script type="application/javascript;version=1.8"
-          src="chrome://devtools/content/shared/theme-switching.js"/>
-  <script type="text/javascript" src="netmonitor.js"/>
-
-  <html:div xmlns="http://www.w3.org/1999/xhtml"
-               id="react-network-monitor-hook"/>
-</window>
--- a/devtools/client/netmonitor/panel.js
+++ b/devtools/client/netmonitor/panel.js
@@ -7,26 +7,26 @@
 function NetMonitorPanel(iframeWindow, toolbox) {
   this.panelWin = iframeWindow;
   this.panelDoc = iframeWindow.document;
   this.toolbox = toolbox;
   this.netmonitor = new iframeWindow.Netmonitor(toolbox);
 }
 
 NetMonitorPanel.prototype = {
-  open: async function () {
+  async open() {
     if (!this.toolbox.target.isRemote) {
       await this.toolbox.target.makeRemote();
     }
     await this.netmonitor.init();
     this.emit("ready");
     this.isReady = true;
     return this;
   },
 
-  destroy: async function () {
+  async destroy() {
     await this.netmonitor.destroy();
     this.emit("destroyed");
     return this;
   },
 };
 
 exports.NetMonitorPanel = NetMonitorPanel;
--- a/devtools/client/netmonitor/request-list-context-menu.js
+++ b/devtools/client/netmonitor/request-list-context-menu.js
@@ -1,16 +1,15 @@
 /* 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 { Task } = require("devtools/shared/task");
 const { Curl } = require("devtools/client/shared/curl");
 const { gDevTools } = require("devtools/client/framework/devtools");
 const Menu = require("devtools/client/framework/menu");
 const MenuItem = require("devtools/client/framework/menu-item");
 const { L10N } = require("./l10n");
 const {
   formDataURI,
   getFormDataSections,
@@ -213,21 +212,21 @@ RequestListContextMenu.prototype = {
     let string = params.join(Services.appinfo.OS === "WINNT" ? "\r\n" : "\n");
     clipboardHelper.copyString(string);
   },
 
   /**
    * Copy the request form data parameters (or raw payload) from
    * the currently selected item.
    */
-  copyPostData: Task.async(function* () {
+  async copyPostData() {
     let selected = this.selectedRequest;
 
     // Try to extract any form data parameters.
-    let formDataSections = yield getFormDataSections(
+    let formDataSections = await getFormDataSections(
       selected.requestHeaders,
       selected.requestHeadersFromUploadStream,
       selected.requestPostData,
       window.gNetwork.getString.bind(window.gNetwork));
 
     let params = [];
     formDataSections.forEach(section => {
       let paramsArray = parseQueryString(section);
@@ -238,54 +237,54 @@ RequestListContextMenu.prototype = {
 
     let string = params
       .map(param => param.name + (param.value ? "=" + param.value : ""))
       .join(Services.appinfo.OS === "WINNT" ? "\r\n" : "\n");
 
     // Fall back to raw payload.
     if (!string) {
       let postData = selected.requestPostData.postData.text;
-      string = yield window.gNetwork.getString(postData);
+      string = await window.gNetwork.getString(postData);
       if (Services.appinfo.OS !== "WINNT") {
         string = string.replace(/\r/g, "");
       }
     }
 
     clipboardHelper.copyString(string);
-  }),
+  },
 
   /**
    * Copy a cURL command from the currently selected item.
    */
-  copyAsCurl: Task.async(function* () {
+  async copyAsCurl() {
     let selected = this.selectedRequest;
 
     // Create a sanitized object for the Curl command generator.
     let data = {
       url: selected.url,
       method: selected.method,
       headers: [],
       httpVersion: selected.httpVersion,
       postDataText: null
     };
 
     // Fetch header values.
     for (let { name, value } of selected.requestHeaders.headers) {
-      let text = yield window.gNetwork.getString(value);
+      let text = await window.gNetwork.getString(value);
       data.headers.push({ name: name, value: text });
     }
 
     // Fetch the request payload.
     if (selected.requestPostData) {
       let postData = selected.requestPostData.postData.text;
-      data.postDataText = yield window.gNetwork.getString(postData);
+      data.postDataText = await window.gNetwork.getString(postData);
     }
 
     clipboardHelper.copyString(Curl.generateCommand(data));
-  }),
+  },
 
   /**
    * Copy the raw request headers from the currently selected item.
    */
   copyRequestHeaders() {
     let rawHeaders = this.selectedRequest.requestHeaders.rawHeaders.trim();
     if (Services.appinfo.OS !== "WINNT") {
       rawHeaders = rawHeaders.replace(/\r/g, "");
--- a/devtools/client/netmonitor/request-list-tooltip.js
+++ b/devtools/client/netmonitor/request-list-tooltip.js
@@ -1,47 +1,46 @@
 /* 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 { Task } = require("devtools/shared/task");
 const {
   setImageTooltip,
   getImageDimensions,
 } = require("devtools/client/shared/widgets/tooltip/ImageTooltipHelper");
 const { WEBCONSOLE_L10N } = require("./l10n");
 const { formDataURI } = require("./request-utils");
 
 // px
 const REQUESTS_TOOLTIP_IMAGE_MAX_DIM = 400;
 // px
 const REQUESTS_TOOLTIP_STACK_TRACE_WIDTH = 600;
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 
-const setTooltipImageContent = Task.async(function* (tooltip, itemEl, requestItem) {
+async function setTooltipImageContent(tooltip, itemEl, requestItem) {
   let { mimeType, text, encoding } = requestItem.responseContent.content;
 
   if (!mimeType || !mimeType.includes("image/")) {
     return false;
   }
 
-  let string = yield window.gNetwork.getString(text);
+  let string = await window.gNetwork.getString(text);
   let src = formDataURI(mimeType, encoding, string);
   let maxDim = REQUESTS_TOOLTIP_IMAGE_MAX_DIM;
-  let { naturalWidth, naturalHeight } = yield getImageDimensions(tooltip.doc, src);
+  let { naturalWidth, naturalHeight } = await getImageDimensions(tooltip.doc, src);
   let options = { maxDim, naturalWidth, naturalHeight };
   setImageTooltip(tooltip, tooltip.doc, src, options);
 
   return itemEl.querySelector(".requests-menu-icon");
-});
+}
 
-const setTooltipStackTraceContent = Task.async(function* (tooltip, requestItem) {
+async function setTooltipStackTraceContent(tooltip, requestItem) {
   let {stacktrace} = requestItem.cause;
 
   if (!stacktrace || stacktrace.length == 0) {
     return false;
   }
 
   let doc = tooltip.doc;
   let el = doc.createElementNS(HTML_NS, "div");
@@ -94,14 +93,14 @@ const setTooltipStackTraceContent = Task
     });
 
     el.appendChild(frameEl);
   }
 
   tooltip.setContent(el, {width: REQUESTS_TOOLTIP_STACK_TRACE_WIDTH});
 
   return true;
-});
+}
 
 module.exports = {
   setTooltipImageContent,
   setTooltipStackTraceContent,
 };
--- a/devtools/client/netmonitor/request-utils.js
+++ b/devtools/client/netmonitor/request-utils.js
@@ -1,74 +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/. */
 
 /* eslint-disable mozilla/reject-some-requires */
 
 "use strict";
 
-const { Task } = require("devtools/shared/task");
-
 /**
  * Extracts any urlencoded form data sections (e.g. "?foo=bar&baz=42") from a
  * POST request.
  *
  * @param {object} headers - the "requestHeaders".
  * @param {object} uploadHeaders - the "requestHeadersFromUploadStream".
  * @param {object} postData - the "requestPostData".
  * @param {function} getString - callback to retrieve a string from a LongStringGrip.
  * @return {array} a promise list that is resolved with the extracted form data.
  */
-const getFormDataSections = Task.async(function* (headers, uploadHeaders, postData,
-                                                    getString) {
+async function getFormDataSections(headers, uploadHeaders, postData, getString) {
   let formDataSections = [];
 
   let requestHeaders = headers.headers;
   let payloadHeaders = uploadHeaders ? uploadHeaders.headers : [];
   let allHeaders = [...payloadHeaders, ...requestHeaders];
 
   let contentTypeHeader = allHeaders.find(e => {
     return e.name.toLowerCase() == "content-type";
   });
 
   let contentTypeLongString = contentTypeHeader ? contentTypeHeader.value : "";
 
-  let contentType = yield getString(contentTypeLongString);
+  let contentType = await getString(contentTypeLongString);
 
   if (contentType.includes("x-www-form-urlencoded")) {
     let postDataLongString = postData.postData.text;
-    let text = yield getString(postDataLongString);
+    let text = await getString(postDataLongString);
 
     for (let section of text.split(/\r\n|\r|\n/)) {
       // Before displaying it, make sure this section of the POST data
       // isn't a line containing upload stream headers.
       if (payloadHeaders.every(header => !section.startsWith(header.name))) {
         formDataSections.push(section);
       }
     }
   }
 
   return formDataSections;
-});
+}
 
 /**
  * Fetch headers full content from actor server
  *
  * @param {object} headers - a object presents headers data
  * @param {function} getString - callback to retrieve a string from a LongStringGrip
  * @return {object} a headers object with updated content payload
  */
-const fetchHeaders = Task.async(function* (headers, getString) {
+async function fetchHeaders(headers, getString) {
   for (let { value } of headers.headers) {
-    headers.headers.value = yield getString(value);
+    headers.headers.value = await getString(value);
   }
-
   return headers;
-});
+}
 
 /**
  * Form a data: URI given a mime type, encoding, and some text.
  *
  * @param {string} mimeType - mime type
  * @param {string} encoding - encoding to use; if not set, the
  *                            text will be base64-encoded.
  * @param {string} text - text of the URI.
--- a/devtools/client/netmonitor/test/head.js
+++ b/devtools/client/netmonitor/test/head.js
@@ -244,26 +244,16 @@ function waitForNetworkEvents(aMonitor, 
       executeSoon(deferred.resolve);
     }
   }
 
   awaitedEventsToListeners.forEach(([e, l]) => panel.on(EVENTS[e], l));
   return deferred.promise;
 }
 
-/**
- * Convert a store record (model) to the rendered element. Tests that need to use
- * this should be rewritten - test the rendered markup at unit level, integration
- * mochitest should check only the store state.
- */
-function getItemTarget(requestList, requestItem) {
-  const items = requestList.mountPoint.querySelectorAll(".request-list-item");
-  return [...items].find(el => el.dataset.id == requestItem.id);
-}
-
 function verifyRequestItemTarget(document, requestList, requestItem, aMethod,
                                  aUrl, aData = {}) {
   info("> Verifying: " + aMethod + " " + aUrl + " " + aData.toSource());
 
   let visibleIndex = requestList.indexOf(requestItem);
 
   info("Visible index of item: " + visibleIndex);
 
--- a/devtools/client/themes/netmonitor.css
+++ b/devtools/client/themes/netmonitor.css
@@ -133,30 +133,27 @@
   height: 19px !important;
 }
 
 .requests-menu-contents {
   display: flex;
   flex-direction: column;
   overflow-x: hidden;
   overflow-y: auto;
-
   --timings-scale: 1;
   --timings-rev-scale: 1;
-
-  /* Devtools panel view height - tabbar height - toolbar height */
-  height: calc(100vh - 48px);
 }
 
 .requests-menu-subitem {
   display: flex;
   flex: none;
   box-sizing: border-box;
   align-items: center;
   padding: 3px;
+  cursor: default;
 }
 
 .subitem-label {
   white-space: nowrap;
   overflow: hidden;
   text-overflow: ellipsis;
 }
 
@@ -855,36 +852,46 @@
   border-bottom: none;
   border-inline-start: none;
 }
 
 .statistics-panel .splitter {
   border-color: rgba(0,0,0,0.2);
   cursor: default;
   pointer-events: none;
-  height: 100vh;
+  height: 100%;
 }
 
 .statistics-panel .charts-container {
   display: flex;
   width: 100%;
 }
 
 .statistics-panel .charts,
 .statistics-panel .pie-table-chart-container {
   width: 100%;
   height: 100%;
 }
 
+.pie-table-chart-container {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
 .statistics-panel .pie-chart-container {
   margin-inline-start: 3vw;
   margin-inline-end: 1vw;
 }
 
 .statistics-panel .table-chart-container {
+  display: flex;
+  flex-direction: column;
+  flex: 1 1 auto;
+  min-width: 240px;
   margin-inline-start: 1vw;
   margin-inline-end: 3vw;
 }
 
 .chart-colored-blob[name=html] {
   fill: var(--theme-highlight-bluegrey);
   background: var(--theme-highlight-bluegrey);
 }
@@ -919,35 +926,47 @@
   background: var(--theme-highlight-green);
 }
 
 .chart-colored-blob[name=flash] {
   fill: var(--theme-highlight-red);
   background: var(--theme-highlight-red);
 }
 
+.table-chart-row {
+  display: flex;
+}
+
 .table-chart-row-label[name=cached] {
   display: none;
 }
 
 .table-chart-row-label[name=count] {
   width: 3em;
   text-align: end;
 }
 
 .table-chart-row-label[name=label] {
   width: 7em;
+  text-align: start;
 }
 
 .table-chart-row-label[name=size] {
   width: 7em;
+  text-align: start;
 }
 
 .table-chart-row-label[name=time] {
   width: 7em;
+  text-align: start;
+}
+
+.table-chart-totals {
+  display: flex;
+  flex-direction: column;
 }
 
 /* Firebug theme support for network charts */
 
 .theme-firebug .chart-colored-blob[name=html] {
   fill: rgba(94, 136, 176, 0.8); /* Blue-Grey highlight */
   background: rgba(94, 136, 176, 0.8);
 }
@@ -1036,21 +1055,23 @@
   }
 
   .requests-menu-waterfall {
     display: none;
   }
 
   .statistics-panel .charts-container {
     flex-direction: column;
+    /* Minus 4em for statistics back button width */
+    width: calc(100% - 4em);
   }
 
   .statistics-panel .splitter {
-    width: 100vw;
-    height: 0;
+    width: 100%;
+    height: 1px;
   }
 }
 
 /* Platform overrides (copied in from the old platform specific files) */
 :root[platform="win"] .requests-menu-header-button > .button-box {
   padding: 0;
 }
 
@@ -1250,28 +1271,28 @@
 .editor-container,
 .editor-mount,
 .panel-container iframe {
   border: none;
   width: 100%;
   height: 100%;
 }
 
-.monitor-panel {
-  width: 100vw;
-  /* Devtools panel height - devtools toolbar height */
-  height: calc(100vh - 24px);
-}
-
 .network-details-panel {
   width: 100%;
   height: 100%;
   overflow: hidden;
 }
 
 .split-box {
-  width: 100vw;
+  overflow: hidden;
 }
 
-.network-monitor {
-  width: 100vw;
-  height: 100vh;
+html,
+body,
+.root,
+.network-monitor,
+.monitor-panel {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  overflow: hidden;
 }