Bug 1350228 - Add network monitor status bar. r=Honza draft
authorTim Nguyen <ntim.bugs@gmail.com>
Mon, 03 Apr 2017 18:01:22 +0200
changeset 555067 ff6a8a4d89766ccda9dba3f980009ea39968fe73
parent 555012 a05f41d3ad1a86c71602df2da563d93e25aa179a
child 622525 5cfd658d6701348372045f4f7b7012b7754f67d2
push id52146
push userbmo:ntim.bugs@gmail.com
push dateMon, 03 Apr 2017 16:01:51 +0000
reviewersHonza
bugs1350228
milestone55.0a1
Bug 1350228 - Add network monitor status bar. r=Honza MozReview-Commit-ID: DEOllBeirq8
devtools/client/netmonitor/src/assets/styles/netmonitor.css
devtools/client/netmonitor/src/components/moz.build
devtools/client/netmonitor/src/components/request-list-empty-notice.js
devtools/client/netmonitor/src/components/request-list.js
devtools/client/netmonitor/src/components/status-bar.js
devtools/client/netmonitor/src/components/toolbar.js
devtools/client/netmonitor/src/selectors/index.js
devtools/client/netmonitor/src/selectors/moz.build
devtools/client/netmonitor/src/selectors/timing-markers.js
devtools/client/themes/toolbars.css
--- a/devtools/client/netmonitor/src/assets/styles/netmonitor.css
+++ b/devtools/client/netmonitor/src/assets/styles/netmonitor.css
@@ -66,18 +66,21 @@ body,
 }
 
 .toolbar-labels {
   overflow: hidden;
   display: flex;
   flex: auto;
 }
 
+.devtools-toolbar {
+  display: flex;
+}
+
 .devtools-toolbar-container {
-  display: flex;
   justify-content: space-between;
 }
 
 .devtools-toolbar-group {
   display: flex;
   flex: 0 0 auto;
   flex-wrap: nowrap;
   align-items: center;
@@ -87,29 +90,58 @@ body,
   overflow: auto;
 }
 
 .cropped-textbox .textbox-input {
   /* workaround for textbox not supporting the @crop attribute */
   text-overflow: ellipsis;
 }
 
+/* Status bar */
+
+.status-bar-label {
+  display: inline-flex;
+  align-content: stretch;
+  margin-inline-end: 10px;
+}
+
+.status-bar-label::before {
+  content: "";
+  display: inline-block;
+  margin-inline-end: 10px;
+  margin-top: 4px;
+  margin-bottom: 4px;
+  width: 1px;
+  background: var(--theme-splitter-color);
+}
+
+.status-bar-label.dom-content-loaded {
+  color: blue;
+}
+
+.status-bar-label.load {
+  color: red;
+}
+
 /* Request list */
 
 .request-list-container {
   display: flex;
   flex-direction: column;
   width: 100%;
   height: 100%;
+  overflow: hidden;
 }
 
 .request-list-empty-notice {
   margin: 0;
   padding: 12px;
   font-size: 120%;
+  flex: 1;
+  overflow: auto;
 }
 
 .notice-perf-message {
   margin-top: 2px;
 }
 
 .requests-list-perf-notice-button {
   min-width: 30px;
@@ -144,16 +176,17 @@ body,
   height: 19px !important;
 }
 
 .requests-list-contents {
   display: flex;
   flex-direction: column;
   overflow-x: hidden;
   overflow-y: auto;
+  flex: 1;
   --timings-scale: 1;
   --timings-rev-scale: 1;
 }
 
 .requests-list-subitem {
   display: flex;
   flex: none;
   box-sizing: border-box;
--- a/devtools/client/netmonitor/src/components/moz.build
+++ b/devtools/client/netmonitor/src/components/moz.build
@@ -18,12 +18,13 @@ DevToolsModules(
     'request-list-empty-notice.js',
     'request-list-header.js',
     'request-list-item.js',
     'request-list.js',
     'response-panel.js',
     'security-panel.js',
     'stack-trace-panel.js',
     'statistics-panel.js',
+    'status-bar.js',
     'tabbox-panel.js',
     'timings-panel.js',
     'toolbar.js',
 )
--- a/devtools/client/netmonitor/src/components/request-list-empty-notice.js
+++ b/devtools/client/netmonitor/src/components/request-list-empty-notice.js
@@ -32,17 +32,17 @@ const RequestListEmptyNotice = createCla
     return div(
       {
         className: "request-list-empty-notice",
       },
       div({ className: "notice-reload-message" },
         span(null, L10N.getStr("netmonitor.reloadNotice1")),
         button(
           {
-            className: "devtools-toolbarbutton requests-list-reload-notice-button",
+            className: "devtools-button requests-list-reload-notice-button",
             "data-standalone": true,
             onClick: this.props.onReloadClick,
           },
           L10N.getStr("netmonitor.reloadNotice2")
         ),
         span(null, L10N.getStr("netmonitor.reloadNotice3"))
       ),
       div({ className: "notice-perf-message" },
--- a/devtools/client/netmonitor/src/components/request-list.js
+++ b/devtools/client/netmonitor/src/components/request-list.js
@@ -9,27 +9,29 @@ const {
   DOM,
   PropTypes,
 } = require("devtools/client/shared/vendor/react");
 
 // Components
 const RequestListContent = createFactory(require("./request-list-content"));
 const RequestListEmptyNotice = createFactory(require("./request-list-empty-notice"));
 const RequestListHeader = createFactory(require("./request-list-header"));
+const StatusBar = createFactory(require("./status-bar"));
 
 const { div } = DOM;
 
 /**
  * Request panel component
  */
 function RequestList({ isEmpty }) {
   return (
     div({ className: "request-list-container" },
       RequestListHeader(),
       isEmpty ? RequestListEmptyNotice() : RequestListContent(),
+      StatusBar(),
     )
   );
 }
 
 RequestList.displayName = "RequestList";
 
 RequestList.propTypes = {
   isEmpty: PropTypes.bool.isRequired,
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/status-bar.js
@@ -0,0 +1,84 @@
+/* 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 { PluralForm } = require("devtools/shared/plural-form");
+
+const Actions = require("../actions/index");
+const {
+  getDisplayedRequestsSummary,
+  getDisplayedTimingMarker,
+} = require("../selectors/index");
+const {
+  getFormattedSize,
+  getFormattedTime
+} = require("../utils/format-utils");
+const { L10N } = require("../utils/l10n");
+
+// Components
+const { div, button, span } = DOM;
+
+function StatusBar({ summary, openStatistics, timingMarkers }) {
+  let { count, contentSize, transferredSize, millis } = summary;
+  let {
+    DOMContentLoaded,
+    load,
+  } = timingMarkers;
+
+  let text = (count === 0) ? L10N.getStr("networkMenu.empty") :
+    PluralForm.get(count, L10N.getStr("networkMenu.summary3"))
+    .replace("#1", count)
+    .replace("#2", getFormattedSize(contentSize))
+    .replace("#3", getFormattedSize(transferredSize))
+    .replace("#4", getFormattedTime(millis));
+
+  return (
+    div({ className: "devtools-toolbar devtools-toolbar-bottom" },
+      button({
+        className: "devtools-button requests-list-network-summary-button",
+        title: count ? text : L10N.getStr("netmonitor.toolbar.perf"),
+        onClick: openStatistics,
+      },
+        span({ className: "summary-info-icon" }),
+        span({ className: "summary-info-text" }, text),
+      ),
+
+      DOMContentLoaded > -1 &&
+      span({ className: "status-bar-label dom-content-loaded" },
+        `DOMContentLoaded: ${getFormattedTime(DOMContentLoaded)}`),
+
+      load > -1 &&
+      span({ className: "status-bar-label load" },
+        `load: ${getFormattedTime(load)}`),
+    )
+  );
+}
+
+StatusBar.displayName = "StatusBar";
+
+StatusBar.propTypes = {
+  openStatistics: PropTypes.func.isRequired,
+  summary: PropTypes.object.isRequired,
+  timingMarkers: PropTypes.object.isRequired,
+};
+
+module.exports = connect(
+  (state) => ({
+    summary: getDisplayedRequestsSummary(state),
+    timingMarkers: {
+      DOMContentLoaded:
+        getDisplayedTimingMarker(state, "firstDocumentDOMContentLoadedTimestamp"),
+      load: getDisplayedTimingMarker(state, "firstDocumentLoadTimestamp"),
+    },
+  }),
+  (dispatch) => ({
+    openStatistics: () => dispatch(Actions.openStatistics(true)),
+  }),
+)(StatusBar);
--- a/devtools/client/netmonitor/src/components/toolbar.js
+++ b/devtools/client/netmonitor/src/components/toolbar.js
@@ -6,28 +6,24 @@
 
 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 { FILTER_SEARCH_DELAY } = require("../constants");
 const {
   getDisplayedRequestsSummary,
   getRequestFilterTypes,
   isNetworkDetailsToggleButtonDisabled,
 } = require("../selectors/index");
-const {
-  getFormattedSize,
-  getFormattedTime
-} = require("../utils/format-utils");
+
 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");
@@ -40,61 +36,49 @@ const TOOLBAR_CLEAR = L10N.getStr("netmo
  * Network monitor toolbar component
  * Toolbar contains a set of useful tools to control network requests
  */
 const Toolbar = createClass({
   displayName: "Toolbar",
 
   propTypes: {
     clearRequests: PropTypes.func.isRequired,
-    openStatistics: PropTypes.func.isRequired,
     requestFilterTypes: PropTypes.array.isRequired,
     setRequestFilterText: PropTypes.func.isRequired,
     networkDetailsToggleDisabled: PropTypes.bool.isRequired,
     networkDetailsOpen: PropTypes.bool.isRequired,
-    summary: PropTypes.object.isRequired,
     toggleNetworkDetails: PropTypes.func.isRequired,
     toggleRequestFilterType: PropTypes.func.isRequired,
   },
 
   toggleRequestFilterType(evt) {
     if (evt.type === "keydown" && (evt.key !== "" || evt.key !== "Enter")) {
       return;
     }
     this.props.toggleRequestFilterType(evt.target.dataset.key);
   },
 
   render() {
     let {
       clearRequests,
-      openStatistics,
       requestFilterTypes,
       setRequestFilterText,
       networkDetailsToggleDisabled,
       networkDetailsOpen,
-      summary,
       toggleNetworkDetails,
     } = this.props;
 
     let toggleButtonClassName = [
       "network-details-panel-toggle",
       "devtools-button",
     ];
     if (!networkDetailsOpen) {
       toggleButtonClassName.push("pane-collapsed");
     }
 
-    let { count, contentSize, transferredSize, millis } = summary;
-    let text = (count === 0) ? L10N.getStr("networkMenu.empty") :
-      PluralForm.get(count, L10N.getStr("networkMenu.summary3"))
-      .replace("#1", count)
-      .replace("#2", getFormattedSize(contentSize))
-      .replace("#3", getFormattedSize(transferredSize))
-      .replace("#4", getFormattedTime(millis));
-
     let buttons = requestFilterTypes.map(([type, checked]) => {
       let classList = ["devtools-button", `requests-list-filter-${type}-button`];
       checked && classList.push("checked");
 
       return (
         button({
           className: classList.join(" "),
           key: type,
@@ -114,24 +98,16 @@ const Toolbar = createClass({
           button({
             className: "devtools-button devtools-clear-icon requests-list-clear-button",
             title: TOOLBAR_CLEAR,
             onClick: clearRequests,
           }),
           div({ className: "requests-list-filter-buttons" }, buttons),
         ),
         span({ className: "devtools-toolbar-group" },
-          button({
-            className: "devtools-button requests-list-network-summary-button",
-            title: count ? text : L10N.getStr("netmonitor.toolbar.perf"),
-            onClick: openStatistics,
-          },
-            span({ className: "summary-info-icon" }),
-            span({ className: "summary-info-text" }, text),
-          ),
           SearchBox({
             delay: FILTER_SEARCH_DELAY,
             keyShortcut: SEARCH_KEY_SHORTCUT,
             placeholder: SEARCH_PLACE_HOLDER,
             type: "filter",
             onChange: setRequestFilterText,
           }),
           button({
@@ -151,14 +127,13 @@ module.exports = connect(
   (state) => ({
     networkDetailsToggleDisabled: isNetworkDetailsToggleButtonDisabled(state),
     networkDetailsOpen: state.ui.networkDetailsOpen,
     requestFilterTypes: getRequestFilterTypes(state),
     summary: getDisplayedRequestsSummary(state),
   }),
   (dispatch) => ({
     clearRequests: () => dispatch(Actions.clearRequests()),
-    openStatistics: () => dispatch(Actions.openStatistics(true)),
     setRequestFilterText: (text) => dispatch(Actions.setRequestFilterText(text)),
     toggleRequestFilterType: (type) => dispatch(Actions.toggleRequestFilterType(type)),
     toggleNetworkDetails: () => dispatch(Actions.toggleNetworkDetails()),
   }),
 )(Toolbar);
--- a/devtools/client/netmonitor/src/selectors/index.js
+++ b/devtools/client/netmonitor/src/selectors/index.js
@@ -1,15 +1,17 @@
 /* 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 requests = require("./requests");
+const timingMarkers = require("./timing-markers");
 const ui = require("./ui");
 
 Object.assign(exports,
   filters,
   requests,
+  timingMarkers,
   ui
 );
--- a/devtools/client/netmonitor/src/selectors/moz.build
+++ b/devtools/client/netmonitor/src/selectors/moz.build
@@ -1,10 +1,11 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
     'filters.js',
     'index.js',
     'requests.js',
+    'timing-markers.js',
     'ui.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/selectors/timing-markers.js
@@ -0,0 +1,13 @@
+/* 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";
+
+function getDisplayedTimingMarker(state, marker) {
+  return state.timingMarkers.get(marker) - state.requests.get("firstStartedMillis");
+}
+
+module.exports = {
+  getDisplayedTimingMarker,
+};
--- a/devtools/client/themes/toolbars.css
+++ b/devtools/client/themes/toolbars.css
@@ -34,17 +34,17 @@
   --icon-filter: none;
   --checked-icon-filter: none;
 }
 
 
 /* Toolbars */
 .devtools-toolbar,
 .devtools-sidebar-tabs tabs {
-  -moz-appearance: none; appearance: none;
+  -moz-appearance: none;
   padding: 0;
   border-width: 0;
   border-bottom-width: 1px;
   border-style: solid;
   height: 24px;
   line-height: 24px;
   box-sizing: border-box;
 }
@@ -69,16 +69,21 @@
   border: none !important; /* overrides .checkbox-label-box from checkbox.css */
 }
 
 .devtools-toolbar checkbox .checkbox-label-box .checkbox-label {
   margin: 0 6px !important; /* overrides .checkbox-label from checkbox.css */
   padding: 0;
 }
 
+.devtools-toolbar-bottom {
+  border-top-width: 1px;
+  border-bottom: none;
+}
+
 .devtools-separator {
   margin: 0 2px;
   width: 2px;
   background-image: linear-gradient(transparent 15%, var(--theme-splitter-color) 15%, var(--theme-splitter-color) 85%, transparent 85%);
   background-size: 1px 100%;
   background-repeat: no-repeat;
   background-position: 0, 1px, 2px;
 }