Bug 1309194 - Implement summary info in Net Panel Toolbar r=Honza,jsnajdr
authorRicky Chien <ricky060709@gmail.com>
Mon, 07 Nov 2016 11:49:47 +0800
changeset 439627 014fd4d5443d308b0a3826ee67506eddbcce0fcd
parent 439114 85a9d908e91a7071bbb1f554b275fb1b4e796e34
child 439628 57317a080c9998ff884aa357be58207bc3be6836
push id36064
push userrthijssen@mozilla.com
push dateWed, 16 Nov 2016 13:38:27 +0000
reviewersHonza, jsnajdr
bugs1309194
milestone53.0a1
Bug 1309194 - Implement summary info in Net Panel Toolbar r=Honza,jsnajdr MozReview-Commit-ID: AqV4glBUHW1
devtools/client/netmonitor/actions/index.js
devtools/client/netmonitor/actions/moz.build
devtools/client/netmonitor/actions/requests.js
devtools/client/netmonitor/actions/sidebar.js
devtools/client/netmonitor/actions/ui.js
devtools/client/netmonitor/components/moz.build
devtools/client/netmonitor/components/summary-button.js
devtools/client/netmonitor/components/toggle-button.js
devtools/client/netmonitor/constants.js
devtools/client/netmonitor/netmonitor-view.js
devtools/client/netmonitor/netmonitor.xul
devtools/client/netmonitor/reducers/index.js
devtools/client/netmonitor/reducers/moz.build
devtools/client/netmonitor/reducers/requests.js
devtools/client/netmonitor/reducers/sidebar.js
devtools/client/netmonitor/reducers/ui.js
devtools/client/netmonitor/requests-menu-view.js
devtools/client/netmonitor/selectors/index.js
devtools/client/netmonitor/test/browser_net_footer-summary.js
devtools/client/netmonitor/toolbar-view.js
devtools/client/themes/common.css
devtools/client/themes/netmonitor.css
--- a/devtools/client/netmonitor/actions/index.js
+++ b/devtools/client/netmonitor/actions/index.js
@@ -1,9 +1,10 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 const filters = require("./filters");
-const sidebar = require("./sidebar");
+const requests = require("./requests");
+const ui = require("./ui");
 
-module.exports = Object.assign({}, filters, sidebar);
+module.exports = Object.assign({}, filters, requests, ui);
--- a/devtools/client/netmonitor/actions/moz.build
+++ b/devtools/client/netmonitor/actions/moz.build
@@ -1,10 +1,11 @@
 # vim: set filetype=python:
 # 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(
     'filters.js',
     'index.js',
-    'sidebar.js',
+    'requests.js',
+    'ui.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/actions/requests.js
@@ -0,0 +1,25 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+  UPDATE_REQUESTS,
+} = require("../constants");
+
+/**
+ * Update request items
+ *
+ * @param {array} requests - visible request items
+ */
+function updateRequests(items) {
+  return {
+    type: UPDATE_REQUESTS,
+    items,
+  };
+}
+
+module.exports = {
+  updateRequests,
+};
deleted file mode 100644
--- a/devtools/client/netmonitor/actions/sidebar.js
+++ /dev/null
@@ -1,49 +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 {
-  DISABLE_TOGGLE_BUTTON,
-  SHOW_SIDEBAR,
-  TOGGLE_SIDEBAR,
-} = require("../constants");
-
-/**
- * Change ToggleButton disabled state.
- *
- * @param {boolean} disabled - expected button disabled state
- */
-function disableToggleButton(disabled) {
-  return {
-    type: DISABLE_TOGGLE_BUTTON,
-    disabled: disabled,
-  };
-}
-
-/**
- * Change sidebar visible state.
- *
- * @param {boolean} visible - expected sidebar visible state
- */
-function showSidebar(visible) {
-  return {
-    type: SHOW_SIDEBAR,
-    visible: visible,
-  };
-}
-
-/**
- * Toggle to show/hide sidebar.
- */
-function toggleSidebar() {
-  return {
-    type: TOGGLE_SIDEBAR,
-  };
-}
-
-module.exports = {
-  disableToggleButton,
-  showSidebar,
-  toggleSidebar,
-};
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/actions/ui.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 {
+  OPEN_SIDEBAR,
+  TOGGLE_SIDEBAR,
+} = require("../constants");
+
+/**
+ * Change sidebar open state.
+ *
+ * @param {boolean} open - open state
+ */
+function openSidebar(open) {
+  return {
+    type: OPEN_SIDEBAR,
+    open,
+  };
+}
+
+/**
+ * Toggle sidebar open state.
+ */
+function toggleSidebar() {
+  return {
+    type: TOGGLE_SIDEBAR,
+  };
+}
+
+module.exports = {
+  openSidebar,
+  toggleSidebar,
+};
--- a/devtools/client/netmonitor/components/moz.build
+++ b/devtools/client/netmonitor/components/moz.build
@@ -1,10 +1,11 @@
 # vim: set filetype=python:
 # 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(
     'filter-buttons.js',
     'search-box.js',
+    'summary-button.js',
     'toggle-button.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/components/summary-button.js
@@ -0,0 +1,57 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* globals NetMonitorView */
+
+"use strict";
+
+const {
+  CONTENT_SIZE_DECIMALS,
+  REQUEST_TIME_DECIMALS,
+} = require("../constants");
+const { 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 { L10N } = require("../l10n");
+const { getSummary } = require("../selectors/index");
+
+const { button, span } = DOM;
+
+function SummaryButton({
+  summary,
+  triggerSummary,
+}) {
+  let { count, totalBytes, totalMillis } = summary;
+  const text = (count === 0) ? L10N.getStr("networkMenu.empty") :
+    PluralForm.get(count, L10N.getStr("networkMenu.summary"))
+    .replace("#1", count)
+    .replace("#2", L10N.numberWithDecimals(totalBytes / 1024,
+      CONTENT_SIZE_DECIMALS))
+    .replace("#3", L10N.numberWithDecimals(totalMillis / 1000,
+      REQUEST_TIME_DECIMALS));
+
+  return button({
+    id: "requests-menu-network-summary-button",
+    className: "devtools-button",
+    title: count ? text : L10N.getStr("netmonitor.toolbar.perf"),
+    onClick: triggerSummary,
+  },
+  span({ className: "summary-info-icon" }),
+  span({ className: "summary-info-text" }, text));
+}
+
+SummaryButton.propTypes = {
+  summary: PropTypes.object.isRequired,
+};
+
+module.exports = connect(
+  (state) => ({
+    summary: getSummary(state),
+  }),
+  (dispatch) => ({
+    triggerSummary: () => {
+      NetMonitorView.toggleFrontendMode();
+    },
+  })
+)(SummaryButton);
--- a/devtools/client/netmonitor/components/toggle-button.js
+++ b/devtools/client/netmonitor/components/toggle-button.js
@@ -1,62 +1,58 @@
-/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
-/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
 /* globals NetMonitorView */
+
 "use strict";
 
 const { DOM, PropTypes } = require("devtools/client/shared/vendor/react");
 const { connect } = require("devtools/client/shared/vendor/react-redux");
 const { L10N } = require("../l10n");
 const Actions = require("../actions/index");
 
-// Shortcuts
 const { button } = DOM;
 
-/**
- * Button used to toggle sidebar
- */
 function ToggleButton({
   disabled,
-  onToggle,
-  visible,
+  open,
+  triggerSidebar,
 }) {
   let className = ["devtools-button"];
-  if (!visible) {
+  if (!open) {
     className.push("pane-collapsed");
   }
-  let titleMsg = visible ? L10N.getStr("collapseDetailsPane") :
-                           L10N.getStr("expandDetailsPane");
+
+  const title = open ? L10N.getStr("collapseDetailsPane") :
+                       L10N.getStr("expandDetailsPane");
 
   return button({
     id: "details-pane-toggle",
     className: className.join(" "),
-    title: titleMsg,
-    disabled: disabled,
+    title,
+    disabled,
     tabIndex: "0",
-    onMouseDown: onToggle,
+    onMouseDown: triggerSidebar,
   });
 }
 
 ToggleButton.propTypes = {
   disabled: PropTypes.bool.isRequired,
-  onToggle: PropTypes.func.isRequired,
-  visible: PropTypes.bool.isRequired,
+  triggerSidebar: PropTypes.func.isRequired,
 };
 
 module.exports = connect(
   (state) => ({
-    disabled: state.sidebar.toggleButtonDisabled,
-    visible: state.sidebar.visible,
+    disabled: state.requests.items.length === 0,
+    open: state.ui.sidebar.open,
   }),
   (dispatch) => ({
-    onToggle: () => {
+    triggerSidebar: () => {
       dispatch(Actions.toggleSidebar());
 
       let requestsMenu = NetMonitorView.RequestsMenu;
       let selectedIndex = requestsMenu.selectedIndex;
 
       // Make sure there's a selection if the button is pressed, to avoid
       // showing an empty network details pane.
       if (selectedIndex == -1 && requestsMenu.itemCount) {
--- a/devtools/client/netmonitor/constants.js
+++ b/devtools/client/netmonitor/constants.js
@@ -1,19 +1,21 @@
 /* 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 general = {
   FREETEXT_FILTER_SEARCH_DELAY: 200,
+  CONTENT_SIZE_DECIMALS: 2,
+  REQUEST_TIME_DECIMALS: 2,
 };
 
 const actionTypes = {
   TOGGLE_FILTER_TYPE: "TOGGLE_FILTER_TYPE",
   ENABLE_FILTER_TYPE_ONLY: "ENABLE_FILTER_TYPE_ONLY",
+  SET_FILTER_TEXT: "SET_FILTER_TEXT",
+  OPEN_SIDEBAR: "OPEN_SIDEBAR",
   TOGGLE_SIDEBAR: "TOGGLE_SIDEBAR",
-  SHOW_SIDEBAR: "SHOW_SIDEBAR",
-  DISABLE_TOGGLE_BUTTON: "DISABLE_TOGGLE_BUTTON",
-  SET_FILTER_TEXT: "SET_FILTER_TEXT",
+  UPDATE_REQUESTS: "UPDATE_REQUESTS",
 };
 
 module.exports = Object.assign({}, general, actionTypes);
--- a/devtools/client/netmonitor/netmonitor-view.js
+++ b/devtools/client/netmonitor/netmonitor-view.js
@@ -120,17 +120,16 @@ var NetMonitorView = {
     this._detailsPane.setAttribute("width", Prefs.networkDetailsWidth);
     this._detailsPane.setAttribute("height", Prefs.networkDetailsHeight);
     this.toggleDetailsPane({ visible: false });
 
     // Disable the performance statistics mode.
     if (!Prefs.statistics) {
       $("#request-menu-context-perf").hidden = true;
       $("#notice-perf-message").hidden = true;
-      $("#requests-menu-network-summary-button").hidden = true;
     }
   },
 
   /**
    * Destroys the UI for all the displayed panes.
    */
   _destroyPanes: Task.async(function* () {
     dumpn("Destroying the NetMonitorView panes");
@@ -166,20 +165,20 @@ var NetMonitorView = {
    * @param number tabIndex [optional]
    *        The index of the intended selected tab in the details pane.
    */
   toggleDetailsPane: function (flags, tabIndex) {
     ViewHelpers.togglePane(flags, this._detailsPane);
 
     if (flags.visible) {
       this._body.classList.remove("pane-collapsed");
-      gStore.dispatch(Actions.showSidebar(true));
+      gStore.dispatch(Actions.openSidebar(true));
     } else {
       this._body.classList.add("pane-collapsed");
-      gStore.dispatch(Actions.showSidebar(false));
+      gStore.dispatch(Actions.openSidebar(false));
     }
 
     if (tabIndex !== undefined) {
       $("#event-details-pane").selectedIndex = tabIndex;
     }
   },
 
   /**
--- a/devtools/client/netmonitor/netmonitor.xul
+++ b/devtools/client/netmonitor/netmonitor.xul
@@ -23,19 +23,18 @@
     <vbox id="network-inspector-view" flex="1">
       <hbox id="netmonitor-toolbar" class="devtools-toolbar">
         <html:div xmlns="http://www.w3.org/1999/xhtml"
                   id="react-clear-button-hook"/>
         <html:div xmlns="http://www.w3.org/1999/xhtml"
                   id="react-filter-buttons-hook"/>
         <spacer id="requests-menu-spacer"
                 flex="1"/>
-        <toolbarbutton id="requests-menu-network-summary-button"
-                       class="devtools-toolbarbutton icon-and-text"
-                       data-localization="tooltiptext=netmonitor.toolbar.perf"/>
+        <html:div xmlns="http://www.w3.org/1999/xhtml"
+                  id="react-summary-button-hook"/>
         <html:div xmlns="http://www.w3.org/1999/xhtml"
                   id="react-search-box-hook"/>
         <html:div xmlns="http://www.w3.org/1999/xhtml"
                   id="react-details-pane-toggle-hook"/>
       </hbox>
       <hbox id="network-table-and-sidebar"
             class="devtools-responsive-container"
             flex="1">
--- a/devtools/client/netmonitor/reducers/index.js
+++ b/devtools/client/netmonitor/reducers/index.js
@@ -1,13 +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 { combineReducers } = require("devtools/client/shared/vendor/redux");
 const filters = require("./filters");
-const sidebar = require("./sidebar");
+const requests = require("./requests");
+const ui = require("./ui");
 
 module.exports = combineReducers({
   filters,
-  sidebar,
+  requests,
+  ui,
 });
--- a/devtools/client/netmonitor/reducers/moz.build
+++ b/devtools/client/netmonitor/reducers/moz.build
@@ -1,10 +1,11 @@
 # vim: set filetype=python:
 # 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(
     'filters.js',
     'index.js',
-    'sidebar.js',
+    'requests.js',
+    'ui.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/reducers/requests.js
@@ -0,0 +1,29 @@
+/* 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 I = require("devtools/client/shared/vendor/immutable");
+const {
+  UPDATE_REQUESTS,
+} = require("../constants");
+
+const Requests = I.Record({
+  items: [],
+});
+
+function updateRequests(state, action) {
+  return state.set("items", action.items || state.items);
+}
+
+function requests(state = new Requests(), action) {
+  switch (action.type) {
+    case UPDATE_REQUESTS:
+      return updateRequests(state, action);
+    default:
+      return state;
+  }
+}
+
+module.exports = requests;
deleted file mode 100644
--- a/devtools/client/netmonitor/reducers/sidebar.js
+++ /dev/null
@@ -1,43 +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 I = require("devtools/client/shared/vendor/immutable");
-const {
-  DISABLE_TOGGLE_BUTTON,
-  SHOW_SIDEBAR,
-  TOGGLE_SIDEBAR,
-} = require("../constants");
-
-const SidebarState = I.Record({
-  toggleButtonDisabled: true,
-  visible: false,
-});
-
-function disableToggleButton(state, action) {
-  return state.set("toggleButtonDisabled", action.disabled);
-}
-
-function showSidebar(state, action) {
-  return state.set("visible", action.visible);
-}
-
-function toggleSidebar(state, action) {
-  return state.set("visible", !state.visible);
-}
-
-function sidebar(state = new SidebarState(), action) {
-  switch (action.type) {
-    case DISABLE_TOGGLE_BUTTON:
-      return disableToggleButton(state, action);
-    case SHOW_SIDEBAR:
-      return showSidebar(state, action);
-    case TOGGLE_SIDEBAR:
-      return toggleSidebar(state, action);
-    default:
-      return state;
-  }
-}
-
-module.exports = sidebar;
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/reducers/ui.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 I = require("devtools/client/shared/vendor/immutable");
+const {
+  OPEN_SIDEBAR,
+  TOGGLE_SIDEBAR,
+} = require("../constants");
+
+const Sidebar = I.Record({
+  open: false,
+});
+
+const UI = I.Record({
+  sidebar: new Sidebar(),
+});
+
+function openSidebar(state, action) {
+  return state.setIn(["sidebar", "open"], action.open);
+}
+
+function toggleSidebar(state, action) {
+  return state.setIn(["sidebar", "open"], !state.sidebar.open);
+}
+
+function ui(state = new UI(), action) {
+  switch (action.type) {
+    case OPEN_SIDEBAR:
+      return openSidebar(state, action);
+    case TOGGLE_SIDEBAR:
+      return toggleSidebar(state, action);
+    default:
+      return state;
+  }
+}
+
+module.exports = ui;
--- a/devtools/client/netmonitor/requests-menu-view.js
+++ b/devtools/client/netmonitor/requests-menu-view.js
@@ -14,17 +14,16 @@ const {DeferredTask} = Cu.import("resour
 /* eslint-disable mozilla/reject-some-requires */
 const {SideMenuWidget} = require("resource://devtools/client/shared/widgets/SideMenuWidget.jsm");
 const {HTMLTooltip} = require("devtools/client/shared/widgets/tooltip/HTMLTooltip");
 const {setImageTooltip, getImageDimensions} =
   require("devtools/client/shared/widgets/tooltip/ImageTooltipHelper");
 const {Heritage, WidgetMethods, setNamedTimeout} =
   require("devtools/client/shared/widgets/view-helpers");
 const {CurlUtils} = require("devtools/client/shared/curl");
-const {PluralForm} = require("devtools/shared/plural-form");
 const {Filters, isFreetextMatch} = require("./filter-predicates");
 const {Sorters} = require("./sort-predicates");
 const {L10N, WEBCONSOLE_L10N} = require("./l10n");
 const {formDataURI,
        writeHeaderText,
        getKeyWithEvent,
        getAbbreviatedMimeType,
        getUriNameWithQuery,
@@ -85,20 +84,21 @@ const CONTENT_MIME_TYPE_ABBREVIATIONS = 
   "x-javascript": "js"
 };
 
 // A smart store watcher to notify store changes as necessary
 function storeWatcher(initialValue, reduceValue, onChange) {
   let currentValue = initialValue;
 
   return () => {
+    const oldValue = currentValue;
     const newValue = reduceValue(currentValue);
-    if (newValue !== currentValue) {
-      onChange(newValue, currentValue);
+    if (newValue !== oldValue) {
       currentValue = newValue;
+      onChange(newValue, oldValue);
     }
   };
 }
 
 /**
  * Functions handling the requests menu (containing details about each request,
  * like status, method, file, domain, as well as a waterfall representing
  * timing imformation).
@@ -124,18 +124,16 @@ RequestsMenuView.prototype = Heritage.ex
 
     this.store = store;
 
     this.contextMenu = new RequestListContextMenu();
 
     let widgetParentEl = $("#requests-menu-contents");
     this.widget = new SideMenuWidget(widgetParentEl);
     this._splitter = $("#network-inspector-view-splitter");
-    this._summary = $("#requests-menu-network-summary-button");
-    this._summary.setAttribute("label", L10N.getStr("networkMenu.empty"));
 
     // Create a tooltip for the newly appended network request item.
     this.tooltip = new HTMLTooltip(NetMonitorController._toolbox.doc, { type: "arrow" });
     this.tooltip.startTogglingOnHover(widgetParentEl, this._onHover, {
       toggleDelay: REQUESTS_TOOLTIP_TOGGLE_DELAY,
       interactive: true
     });
 
@@ -206,23 +204,20 @@ RequestsMenuView.prototype = Heritage.ex
         this.cloneSelectedRequestEvent, false);
     } else {
       $("#headers-summary-resend").hidden = true;
     }
 
     if (NetMonitorController.supportsPerfStats) {
       $("#requests-menu-perf-notice-button").addEventListener("command",
         this._onContextPerfCommand, false);
-      $("#requests-menu-network-summary-button").addEventListener("command",
-        this._onContextPerfCommand, false);
       $("#network-statistics-back-button").addEventListener("command",
         this._onContextPerfCommand, false);
     } else {
       $("#notice-perf-message").hidden = true;
-      $("#requests-menu-network-summary-button").hidden = true;
     }
 
     if (!NetMonitorController.supportsTransferredResponseSize) {
       $("#requests-menu-transferred-header-box").hidden = true;
       $("#requests-menu-item-template .requests-menu-transferred")
         .hidden = true;
     }
   },
@@ -252,18 +247,16 @@ RequestsMenuView.prototype = Heritage.ex
       this.requestsMenuSortKeyboardEvent, false);
 
     this._flushRequestsTask.disarm();
 
     $("#requests-menu-reload-notice-button").removeEventListener("command",
       this._onReloadCommand, false);
     $("#requests-menu-perf-notice-button").removeEventListener("command",
       this._onContextPerfCommand, false);
-    $("#requests-menu-network-summary-button").removeEventListener("command",
-      this._onContextPerfCommand, false);
     $("#network-statistics-back-button").removeEventListener("command",
       this._onContextPerfCommand, false);
 
     $("#custom-request-send-button").removeEventListener("click",
       this.sendCustomRequestEvent, false);
     $("#custom-request-close-button").removeEventListener("click",
       this.closeCustomRequestEvent, false);
     $("#headers-summary-resend").removeEventListener("click",
@@ -417,17 +410,17 @@ RequestsMenuView.prototype = Heritage.ex
     }
   },
 
   /**
    * Refreshes the view contents with the newly selected filters
    */
   reFilterRequests: function () {
     this.filterContents(this._filterPredicate);
-    this.refreshSummary();
+    this.updateRequests();
     this.refreshZebra();
   },
 
   /**
    * Returns a predicate that can be used to test if a request matches any of
    * the active filters.
    */
   get _filterPredicate() {
@@ -536,62 +529,38 @@ RequestsMenuView.prototype = Heritage.ex
         if (direction == "ascending") {
           this.sortContents((a, b) => Sorters.waterfall(a.attachment, b.attachment));
         } else {
           this.sortContents((a, b) => -Sorters.waterfall(a.attachment, b.attachment));
         }
         break;
     }
 
-    this.refreshSummary();
+    this.updateRequests();
     this.refreshZebra();
   },
 
   /**
    * Removes all network requests and closes the sidebar if open.
    */
   clear: function () {
     NetMonitorController.NetworkEventsHandler.clearMarkers();
     NetMonitorView.Sidebar.toggle(false);
 
-    this.store.dispatch(Actions.disableToggleButton(true));
     $("#requests-menu-empty-notice").hidden = false;
 
     this.empty();
-    this.refreshSummary();
+    this.updateRequests();
   },
 
   /**
-   * Refreshes the status displayed in this container's footer, providing
-   * concise information about all requests.
+   * Update store request itmes and trigger related UI update
    */
-  refreshSummary: function () {
-    let visibleItems = this.visibleItems;
-    let visibleRequestsCount = visibleItems.length;
-    if (!visibleRequestsCount) {
-      this._summary.setAttribute("label", L10N.getStr("networkMenu.empty"));
-      return;
-    }
-
-    let totalBytes = this._getTotalBytesOfRequests(visibleItems);
-    let totalMillis =
-      this._getNewestRequest(visibleItems).attachment.endedMillis -
-      this._getOldestRequest(visibleItems).attachment.startedMillis;
-
-    // https://developer.mozilla.org/en-US/docs/Localization_and_Plurals
-    let str = PluralForm.get(visibleRequestsCount,
-      L10N.getStr("networkMenu.summary"));
-
-    this._summary.setAttribute("label", str
-      .replace("#1", visibleRequestsCount)
-      .replace("#2", L10N.numberWithDecimals((totalBytes || 0) / 1024,
-        CONTENT_SIZE_DECIMALS))
-      .replace("#3", L10N.numberWithDecimals((totalMillis || 0) / 1000,
-        REQUEST_TIME_DECIMALS))
-    );
+  updateRequests: function () {
+    this.store.dispatch(Actions.updateRequests(this.visibleItems));
   },
 
   /**
    * Adds odd/even attributes to all the visible items in this container.
    */
   refreshZebra: function () {
     let visibleItems = this.visibleItems;
 
@@ -860,27 +829,26 @@ RequestsMenuView.prototype = Heritage.ex
         NetMonitorView.NetworkDetails.populate(selectedItem.attachment);
       }
     }
 
     // We're done flushing all the requests, clear the update queue.
     this._updateQueue = [];
     this._addQueue = [];
 
-    this.store.dispatch(Actions.disableToggleButton(!this.itemCount));
     $("#requests-menu-empty-notice").hidden = !!this.itemCount;
 
     // Make sure all the requests are sorted and filtered.
     // Freshly added requests may not yet contain all the information required
     // for sorting and filtering predicates, so this is done each time the
     // network requests table is flushed (don't worry, events are drained first
     // so this doesn't happen once per network event update).
     this.sortContents();
     this.filterContents();
-    this.refreshSummary();
+    this.updateRequests();
     this.refreshZebra();
 
     // Rescale all the waterfalls so that everything is visible at once.
     this._flushWaterfallViews();
   },
 
   /**
    * Customization function for creating an item's UI.
@@ -1554,69 +1522,16 @@ RequestsMenuView.prototype = Heritage.ex
    */
   _registerLastRequestEnd: function (unixTime) {
     if (this._lastRequestEndedMillis < unixTime) {
       this._lastRequestEndedMillis = unixTime;
     }
   },
 
   /**
-   * Gets the total number of bytes representing the cumulated content size of
-   * a set of requests. Returns 0 for an empty set.
-   *
-   * @param array itemsArray
-   * @return number
-   */
-  _getTotalBytesOfRequests: function (itemsArray) {
-    if (!itemsArray.length) {
-      return 0;
-    }
-
-    let result = 0;
-    itemsArray.forEach(item => {
-      let size = item.attachment.contentSize;
-      result += (typeof size == "number") ? size : 0;
-    });
-
-    return result;
-  },
-
-  /**
-   * Gets the oldest (first performed) request in a set. Returns null for an
-   * empty set.
-   *
-   * @param array itemsArray
-   * @return object
-   */
-  _getOldestRequest: function (itemsArray) {
-    if (!itemsArray.length) {
-      return null;
-    }
-    return itemsArray.reduce((prev, curr) =>
-      prev.attachment.startedMillis < curr.attachment.startedMillis ?
-        prev : curr);
-  },
-
-  /**
-   * Gets the newest (latest performed) request in a set. Returns null for an
-   * empty set.
-   *
-   * @param array itemsArray
-   * @return object
-   */
-  _getNewestRequest: function (itemsArray) {
-    if (!itemsArray.length) {
-      return null;
-    }
-    return itemsArray.reduce((prev, curr) =>
-      prev.attachment.startedMillis > curr.attachment.startedMillis ?
-        prev : curr);
-  },
-
-  /**
    * Gets the available waterfall width in this container.
    * @return number
    */
   get _waterfallWidth() {
     if (this._cachedWaterfallWidth == 0) {
       let container = $("#requests-menu-toolbar");
       let waterfall = $("#requests-menu-waterfall-header-box");
       let containerBounds = container.getBoundingClientRect();
--- a/devtools/client/netmonitor/selectors/index.js
+++ b/devtools/client/netmonitor/selectors/index.js
@@ -1,8 +1,63 @@
 /* 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 { createSelector } = require("devtools/client/shared/vendor/reselect");
+
+/**
+ * Gets the total number of bytes representing the cumulated content size of
+ * a set of requests. Returns 0 for an empty set.
+ *
+ * @param {array} items - an array of request items
+ * @return {number} total bytes of requests
+ */
+function getTotalBytesOfRequests(items) {
+  if (!items.length) {
+    return 0;
+  }
+
+  let result = 0;
+  items.forEach((item) => {
+    let size = item.attachment.contentSize;
+    result += (typeof size == "number") ? size : 0;
+  });
+
+  return result;
+}
+
+/**
+ * Gets the total milliseconds for all requests. Returns null for an
+ * empty set.
+ *
+ * @param {array} items - an array of request items
+ * @return {object} total milliseconds for all requests
+ */
+function getTotalMillisOfRequests(items) {
+  if (!items.length) {
+    return null;
+  }
+
+  const oldest = items.reduce((prev, curr) =>
+    prev.attachment.startedMillis < curr.attachment.startedMillis ?
+      prev : curr);
+  const newest = items.reduce((prev, curr) =>
+    prev.attachment.startedMillis > curr.attachment.startedMillis ?
+      prev : curr);
+
+  return newest.attachment.endedMillis - oldest.attachment.startedMillis;
+}
+
+const getSummary = createSelector(
+  (state) => state.requests.items,
+  (requests) => ({
+    count: requests.length,
+    totalBytes: getTotalBytesOfRequests(requests),
+    totalMillis: getTotalMillisOfRequests(requests),
+  })
+);
+
 module.exports = {
-  // selectors...
+  getSummary,
 };
--- a/devtools/client/netmonitor/test/browser_net_footer-summary.js
+++ b/devtools/client/netmonitor/test/browser_net_footer-summary.js
@@ -5,23 +5,24 @@
 
 /**
  * Test if the summary text displayed in the network requests menu footer
  * is correct.
  */
 
 add_task(function* () {
   requestLongerTimeout(2);
+  let { getSummary } = require("devtools/client/netmonitor/selectors/index");
   let { L10N } = require("devtools/client/netmonitor/l10n");
   let { PluralForm } = require("devtools/shared/plural-form");
 
   let { tab, monitor } = yield initNetMonitor(FILTERING_URL);
   info("Starting test... ");
 
-  let { $, NetMonitorView } = monitor.panelWin;
+  let { $, NetMonitorView, gStore } = monitor.panelWin;
   let { RequestsMenu } = NetMonitorView;
 
   RequestsMenu.lazyUpdate = false;
   testStatus();
 
   for (let i = 0; i < 2; i++) {
     info(`Performing requests in batch #${i}`);
     let wait = waitForNetworkEvents(monitor, 8);
@@ -38,38 +39,31 @@ add_task(function* () {
       EventUtils.sendMouseEvent({ type: "click" }, buttonEl);
       testStatus();
     }
   }
 
   yield teardown(monitor);
 
   function testStatus() {
-    let summary = $("#requests-menu-network-summary-button");
-    let value = summary.getAttribute("label");
+    const { count, totalBytes, totalMillis } = getSummary(gStore.getState());
+    let value = $("#requests-menu-network-summary-button").textContent;
     info("Current summary: " + value);
 
-    let visibleItems = RequestsMenu.visibleItems;
-    let visibleRequestsCount = visibleItems.length;
     let totalRequestsCount = RequestsMenu.itemCount;
-    info("Current requests: " + visibleRequestsCount + " of " + totalRequestsCount + ".");
+    info("Current requests: " + count + " of " + totalRequestsCount + ".");
 
-    if (!totalRequestsCount || !visibleRequestsCount) {
+    if (!totalRequestsCount || !count) {
       is(value, L10N.getStr("networkMenu.empty"),
         "The current summary text is incorrect, expected an 'empty' label.");
       return;
     }
 
-    let totalBytes = RequestsMenu._getTotalBytesOfRequests(visibleItems);
-    let totalMillis =
-      RequestsMenu._getNewestRequest(visibleItems).attachment.endedMillis -
-      RequestsMenu._getOldestRequest(visibleItems).attachment.startedMillis;
-
     info("Computed total bytes: " + totalBytes);
     info("Computed total millis: " + totalMillis);
 
-    is(value, PluralForm.get(visibleRequestsCount, L10N.getStr("networkMenu.summary"))
-      .replace("#1", visibleRequestsCount)
+    is(value, PluralForm.get(count, L10N.getStr("networkMenu.summary"))
+      .replace("#1", count)
       .replace("#2", L10N.numberWithDecimals((totalBytes || 0) / 1024, 2))
       .replace("#3", L10N.numberWithDecimals((totalMillis || 0) / 1000, 2))
     , "The current summary text is incorrect.");
   }
 });
--- a/devtools/client/netmonitor/toolbar-view.js
+++ b/devtools/client/netmonitor/toolbar-view.js
@@ -2,16 +2,17 @@
 "use strict";
 
 const { createFactory, DOM } = require("devtools/client/shared/vendor/react");
 const ReactDOM = require("devtools/client/shared/vendor/react-dom");
 const Provider = createFactory(require("devtools/client/shared/vendor/react-redux").Provider);
 const FilterButtons = createFactory(require("./components/filter-buttons"));
 const ToggleButton = createFactory(require("./components/toggle-button"));
 const SearchBox = createFactory(require("./components/search-box"));
+const SummaryButton = createFactory(require("./components/summary-button"));
 const { L10N } = require("./l10n");
 
 // Shortcuts
 const { button } = DOM;
 
 /**
  * Functions handling the toolbar view: expand/collapse button etc.
  */
@@ -23,18 +24,19 @@ ToolbarView.prototype = {
   /**
    * Initialization function, called when the debugger is started.
    */
   initialize: function (store) {
     dumpn("Initializing the ToolbarView");
 
     this._clearContainerNode = $("#react-clear-button-hook");
     this._filterContainerNode = $("#react-filter-buttons-hook");
+    this._summaryContainerNode = $("#react-summary-button-hook");
+    this._searchContainerNode = $("#react-search-box-hook");
     this._toggleContainerNode = $("#react-details-pane-toggle-hook");
-    this._searchContainerNode = $("#react-search-box-hook");
 
     // clear button
     ReactDOM.render(button({
       id: "requests-menu-clear-button",
       className: "devtools-button devtools-clear-icon",
       title: L10N.getStr("netmonitor.toolbar.clear"),
       onClick: () => {
         NetMonitorView.RequestsMenu.clear();
@@ -42,16 +44,22 @@ ToolbarView.prototype = {
     }), this._clearContainerNode);
 
     // filter button
     ReactDOM.render(Provider(
       { store },
       FilterButtons()
     ), this._filterContainerNode);
 
+    // summary button
+    ReactDOM.render(Provider(
+      { store },
+      SummaryButton()
+    ), this._summaryContainerNode);
+
     // search box
     ReactDOM.render(Provider(
       { store },
       SearchBox()
     ), this._searchContainerNode);
 
     // details pane toggle button
     ReactDOM.render(Provider(
@@ -63,15 +71,16 @@ ToolbarView.prototype = {
   /**
    * Destruction function, called when the debugger is closed.
    */
   destroy: function () {
     dumpn("Destroying the ToolbarView");
 
     ReactDOM.unmountComponentAtNode(this._clearContainerNode);
     ReactDOM.unmountComponentAtNode(this._filterContainerNode);
+    ReactDOM.unmountComponentAtNode(this._summaryContainerNode);
+    ReactDOM.unmountComponentAtNode(this._searchContainerNode);
     ReactDOM.unmountComponentAtNode(this._toggleContainerNode);
-    ReactDOM.unmountComponentAtNode(this._searchContainerNode);
   }
 
 };
 
 exports.ToolbarView = ToolbarView;
--- a/devtools/client/themes/common.css
+++ b/devtools/client/themes/common.css
@@ -1,13 +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/. */
 
+
 @import url("resource://devtools/client/themes/splitters.css");
+@namespace html url("http://www.w3.org/1999/xhtml");
 
 :root {
   font: message-box;
 }
 
 :root[platform="mac"] {
   --monospace-font-family: Menlo, monospace;
 }
@@ -28,16 +30,20 @@
 .devtools-monospace {
   font-family: var(--monospace-font-family);
 }
 
 :root[platform="linux"] .devtools-monospace {
   font-size: 80%;
 }
 
+/* Override wrong system font from forms.css */
+html|button, html|select {
+  font: message-box;
+}
 
 /* Autocomplete Popup */
 
 .devtools-autocomplete-popup {
   box-shadow: 0 1px 0 hsla(209,29%,72%,.25) inset;
   background-color: transparent;
   border-radius: 3px;
   overflow-x: hidden;
--- a/devtools/client/themes/netmonitor.css
+++ b/devtools/client/themes/netmonitor.css
@@ -3,16 +3,17 @@
  * 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/. */
 
 #toolbar-labels {
   overflow: hidden;
 }
 
 #react-clear-button-hook,
+#react-summary-button-hook,
 #react-details-pane-toggle-hook {
   display: flex;
 }
 
 /**
  * Collapsed details pane needs to be truly hidden to prevent both accessibility
  * tools and keyboard from accessing its contents.
  */
@@ -41,17 +42,17 @@
   text-overflow: ellipsis;
 }
 
 /* Responsive sidebar */
 @media (max-width: 700px) {
   #toolbar-spacer,
   #details-pane-toggle,
   .requests-menu-waterfall,
-  #requests-menu-network-summary-button > .toolbarbutton-text {
+  #requests-menu-network-summary-button > .summary-info-text {
     display: none;
   }
 }
 
 :root.theme-dark {
   --table-splitter-color: rgba(255,255,255,0.15);
   --table-zebra-background: rgba(255,255,255,0.05);
 
@@ -738,26 +739,38 @@
 
 #custom-method-value {
   width: 4.5em;
 }
 
 /* Performance analysis buttons */
 
 #requests-menu-network-summary-button {
+  display: flex;
+  align-items: center;
   background: none;
   box-shadow: none;
   border-color: transparent;
-  list-style-image: url(images/profiler-stopwatch.svg);
   padding-inline-end: 0;
   cursor: pointer;
   margin-inline-end: 1em;
   min-width: 0;
 }
 
+#requests-menu-network-summary-button > .summary-info-icon {
+  background-image: url(images/profiler-stopwatch.svg);
+  filter: var(--icon-filter);
+  width: 16px;
+  height: 16px;
+}
+
+#requests-menu-network-summary-button > .summary-info-text {
+  margin-inline-start: 0.5em;
+}
+
 /* Performance analysis view */
 
 #network-statistics-toolbar {
   border: none;
   margin: 0;
   padding: 0;
 }