Bug 1309194: Implement summary info in Net Panel Toolbar; Bug 1317205: CSS improvement for summary button

Required for: https://github.com/MoonchildProductions/moebius/pull/93
This commit is contained in:
janekptacijarabaci 2018-02-28 12:17:13 +01:00 committed by Roy Tam
commit 2f496e3f1e
22 changed files with 333 additions and 240 deletions

View file

@ -4,6 +4,7 @@
"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);

View file

@ -6,5 +6,6 @@
DevToolsModules(
'filters.js',
'index.js',
'sidebar.js',
'requests.js',
'ui.js',
)

View file

@ -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,
};

View file

@ -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,
};

View file

@ -0,0 +1,36 @@
+/* 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,
};

View file

@ -6,5 +6,6 @@
DevToolsModules(
'filter-buttons.js',
'search-box.js',
'summary-button.js',
'toggle-button.js',
)

View file

@ -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);

View file

@ -1,5 +1,3 @@
/* -*- 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/. */
@ -11,47 +9,43 @@ 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;

View file

@ -5,15 +5,17 @@
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",
TOGGLE_SIDEBAR: "TOGGLE_SIDEBAR",
SHOW_SIDEBAR: "SHOW_SIDEBAR",
DISABLE_TOGGLE_BUTTON: "DISABLE_TOGGLE_BUTTON",
SET_FILTER_TEXT: "SET_FILTER_TEXT",
OPEN_SIDEBAR: "OPEN_SIDEBAR",
TOGGLE_SIDEBAR: "TOGGLE_SIDEBAR",
UPDATE_REQUESTS: "UPDATE_REQUESTS",
};
module.exports = Object.assign({}, general, actionTypes);

View file

@ -125,7 +125,6 @@ var NetMonitorView = {
if (!Prefs.statistics) {
$("#request-menu-context-perf").hidden = true;
$("#notice-perf-message").hidden = true;
$("#requests-menu-network-summary-button").hidden = true;
}
},
@ -171,10 +170,10 @@ var NetMonitorView = {
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) {

View file

@ -28,9 +28,8 @@
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"

View file

@ -5,9 +5,11 @@
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,
});

View file

@ -6,5 +6,6 @@
DevToolsModules(
'filters.js',
'index.js',
'sidebar.js',
'requests.js',
'ui.js',
)

View file

@ -0,0 +1,28 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"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;

View file

@ -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;

View file

@ -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;

View file

@ -19,7 +19,6 @@ const {setImageTooltip, getImageDimensions} =
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");
@ -90,10 +89,11 @@ 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);
}
};
}
@ -129,8 +129,6 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
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" });
@ -211,13 +209,10 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
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) {
@ -257,8 +252,6 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
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);
@ -422,7 +415,7 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
*/
reFilterRequests: function () {
this.filterContents(this._filterPredicate);
this.refreshSummary();
this.updateRequests();
this.refreshZebra();
},
@ -541,7 +534,7 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
break;
}
this.refreshSummary();
this.updateRequests();
this.refreshZebra();
},
@ -552,41 +545,17 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
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));
},
/**
@ -865,7 +834,6 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
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.
@ -875,7 +843,7 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
// 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.
@ -1558,59 +1526,6 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
}
},
/**
* 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

View file

@ -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,
};

View file

@ -10,13 +10,14 @@
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;
@ -43,31 +44,24 @@ add_task(function* () {
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.");

View file

@ -7,6 +7,7 @@ const Provider = createFactory(require("devtools/client/shared/vendor/react-redu
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
@ -28,8 +29,9 @@ ToolbarView.prototype = {
this._clearContainerNode = $("#react-clear-button-hook");
this._filterContainerNode = $("#react-filter-buttons-hook");
this._toggleContainerNode = $("#react-details-pane-toggle-hook");
this._summaryContainerNode = $("#react-summary-button-hook");
this._searchContainerNode = $("#react-search-box-hook");
this._toggleContainerNode = $("#react-details-pane-toggle-hook");
// clear button
ReactDOM.render(button({
@ -47,6 +49,12 @@ ToolbarView.prototype = {
FilterButtons()
), this._filterContainerNode);
// summary button
ReactDOM.render(Provider(
{ store },
SummaryButton()
), this._summaryContainerNode);
// search box
ReactDOM.render(Provider(
{ store },
@ -68,8 +76,9 @@ ToolbarView.prototype = {
ReactDOM.unmountComponentAtNode(this._clearContainerNode);
ReactDOM.unmountComponentAtNode(this._filterContainerNode);
ReactDOM.unmountComponentAtNode(this._toggleContainerNode);
ReactDOM.unmountComponentAtNode(this._summaryContainerNode);
ReactDOM.unmountComponentAtNode(this._searchContainerNode);
ReactDOM.unmountComponentAtNode(this._toggleContainerNode);
}
};