--- a/devtools/client/locales/en-US/netmonitor.properties
+++ b/devtools/client/locales/en-US/netmonitor.properties
@@ -643,16 +643,20 @@ netmonitor.toolbar.disableCache.label=Di
# LOCALIZATION NOTE (netmonitor.toolbar.disableCache.tooltip): This is the tooltip
# displayed for the checkbox for disabling browser cache.
netmonitor.toolbar.disableCache.tooltip=Disable HTTP cache
# LOCALIZATION NOTE (netmonitor.toolbar.clear): This is the label displayed
# in the network toolbar for the "Clear" button.
netmonitor.toolbar.clear=Clear
+# LOCALIZATION NOTE (netmonitor.toolbar.toggleRecording): This is the label displayed
+# in the network toolbar for the toggle recording button.
+netmonitor.toolbar.toggleRecording=Pause/Resume recording network log
+
# LOCALIZATION NOTE (netmonitor.toolbar.perf): This is the label displayed
# in the network toolbar for the performance analysis button.
netmonitor.toolbar.perf=Toggle performance analysis…
# LOCALIZATION NOTE (netmonitor.toolbar.resetColumns): This is the label
# displayed in the network table header context menu.
netmonitor.toolbar.resetColumns=Reset Columns
--- a/devtools/client/netmonitor/src/actions/requests.js
+++ b/devtools/client/netmonitor/src/actions/requests.js
@@ -6,16 +6,17 @@
const { sendHTTPRequest } = require("../connector/index");
const {
ADD_REQUEST,
CLEAR_REQUESTS,
CLONE_SELECTED_REQUEST,
REMOVE_SELECTED_CUSTOM_REQUEST,
SEND_CUSTOM_REQUEST,
+ TOGGLE_RECORDING,
UPDATE_REQUEST,
} = require("../constants");
const { getSelectedRequest } = require("../selectors/index");
function addRequest(id, data, batch) {
return {
type: ADD_REQUEST,
id,
@@ -87,16 +88,26 @@ function removeSelectedCustomRequest() {
}
function clearRequests() {
return {
type: CLEAR_REQUESTS
};
}
+/**
+ * Toggle monitoring
+ */
+function toggleRecording() {
+ return {
+ type: TOGGLE_RECORDING
+ };
+}
+
module.exports = {
addRequest,
clearRequests,
cloneSelectedRequest,
removeSelectedCustomRequest,
sendCustomRequest,
+ toggleRecording,
updateRequest,
};
--- a/devtools/client/netmonitor/src/assets/styles/netmonitor.css
+++ b/devtools/client/netmonitor/src/assets/styles/netmonitor.css
@@ -45,16 +45,22 @@
--sort-descending-image: url(chrome://devtools/skin/images/sort-descending-arrow.svg);
}
:root.theme-firebug {
--sort-ascending-image: url(chrome://devtools/skin/images/firebug/arrow-up.svg);
--sort-descending-image: url(chrome://devtools/skin/images/firebug/arrow-down.svg);
}
+/* Icons */
+:root {
+ --play-icon-url: url("chrome://devtools/skin/images/play.svg");
+ --pause-icon-url: url("chrome://devtools/skin/images/pause.svg");
+}
+
/* General */
* {
box-sizing: border-box;
}
html,
body,
@@ -92,16 +98,24 @@ body,
align-items: center;
}
.requests-list-filter-buttons {
display: flex;
flex-wrap: wrap;
}
+.devtools-button.devtools-pause-icon::before {
+ background-image: var(--pause-icon-url);
+}
+
+.devtools-button.devtools-play-icon::before {
+ background-image: var(--play-icon-url);
+}
+
/* Learn more links */
.learn-more-link::before {
background-image: url(chrome://devtools/skin/images/help.svg);
}
.tree-container .treeTable tr .learn-more-link {
position: absolute;
--- a/devtools/client/netmonitor/src/components/toolbar.js
+++ b/devtools/client/netmonitor/src/components/toolbar.js
@@ -11,93 +11,136 @@ const {
DOM,
PropTypes,
} = require("devtools/client/shared/vendor/react");
const { connect } = require("devtools/client/shared/vendor/react-redux");
const Actions = require("../actions/index");
const { FILTER_SEARCH_DELAY, FILTER_TAGS } = require("../constants");
const {
getDisplayedRequestsSummary,
+ getRecordingState,
getRequestFilterTypes,
getTypeFilteredRequests,
isNetworkDetailsToggleButtonDisabled,
} = require("../selectors/index");
const { autocompleteProvider } = require("../utils/filter-autocomplete-provider");
const { L10N } = require("../utils/l10n");
// Components
const SearchBox = createFactory(require("devtools/client/shared/components/SearchBox"));
const { button, div, input, label, span } = DOM;
-const COLLPASE_DETAILS_PANE = L10N.getStr("collapseDetailsPane");
+// Localization
+const COLLAPSE_DETAILS_PANE = L10N.getStr("collapseDetailsPane");
const EXPAND_DETAILS_PANE = L10N.getStr("expandDetailsPane");
const SEARCH_KEY_SHORTCUT = L10N.getStr("netmonitor.toolbar.filterFreetext.key");
const SEARCH_PLACE_HOLDER = L10N.getStr("netmonitor.toolbar.filterFreetext.label");
const TOOLBAR_CLEAR = L10N.getStr("netmonitor.toolbar.clear");
+const TOOLBAR_TOGGLE_RECORDING = L10N.getStr("netmonitor.toolbar.toggleRecording");
+// Preferences
const DEVTOOLS_DISABLE_CACHE_PREF = "devtools.cache.disabled";
const DEVTOOLS_ENABLE_PERSISTENT_LOG_PREF = "devtools.netmonitor.persistlog";
const TOOLBAR_FILTER_LABELS = FILTER_TAGS.concat("all").reduce((o, tag) =>
Object.assign(o, { [tag]: L10N.getStr(`netmonitor.toolbar.filter.${tag}`) }), {});
const ENABLE_PERSISTENT_LOGS_TOOLTIP =
L10N.getStr("netmonitor.toolbar.enablePersistentLogs.tooltip");
const ENABLE_PERSISTENT_LOGS_LABEL =
L10N.getStr("netmonitor.toolbar.enablePersistentLogs.label");
const DISABLE_CACHE_TOOLTIP = L10N.getStr("netmonitor.toolbar.disableCache.tooltip");
const DISABLE_CACHE_LABEL = L10N.getStr("netmonitor.toolbar.disableCache.label");
-/*
- * Network monitor toolbar component
+/**
+ * Network monitor toolbar component.
+ *
* Toolbar contains a set of useful tools to control network requests
+ * as well as set of filters for filtering the content.
*/
const Toolbar = createClass({
displayName: "Toolbar",
propTypes: {
+ toggleRecording: PropTypes.func.isRequired,
+ recording: PropTypes.bool.isRequired,
clearRequests: PropTypes.func.isRequired,
requestFilterTypes: PropTypes.array.isRequired,
setRequestFilterText: PropTypes.func.isRequired,
networkDetailsToggleDisabled: PropTypes.bool.isRequired,
networkDetailsOpen: PropTypes.bool.isRequired,
toggleNetworkDetails: PropTypes.func.isRequired,
enablePersistentLogs: PropTypes.func.isRequired,
togglePersistentLogs: PropTypes.func.isRequired,
persistentLogsEnabled: PropTypes.bool.isRequired,
disableBrowserCache: PropTypes.func.isRequired,
toggleBrowserCache: PropTypes.func.isRequired,
browserCacheDisabled: PropTypes.bool.isRequired,
toggleRequestFilterType: PropTypes.func.isRequired,
filteredRequests: PropTypes.object.isRequired,
},
+ componentDidMount() {
+ Services.prefs.addObserver(DEVTOOLS_ENABLE_PERSISTENT_LOG_PREF,
+ this.updatePersistentLogsEnabled);
+ Services.prefs.addObserver(DEVTOOLS_DISABLE_CACHE_PREF,
+ this.updateBrowserCacheDisabled);
+ },
+
+ componentWillUnmount() {
+ Services.prefs.removeObserver(DEVTOOLS_ENABLE_PERSISTENT_LOG_PREF,
+ this.updatePersistentLogsEnabled);
+ Services.prefs.removeObserver(DEVTOOLS_DISABLE_CACHE_PREF,
+ this.updateBrowserCacheDisabled);
+ },
+
+ toggleRequestFilterType(evt) {
+ if (evt.type === "keydown" && (evt.key !== "" || evt.key !== "Enter")) {
+ return;
+ }
+ this.props.toggleRequestFilterType(evt.target.dataset.key);
+ },
+
+ updatePersistentLogsEnabled() {
+ this.props.enablePersistentLogs(
+ Services.prefs.getBoolPref(DEVTOOLS_ENABLE_PERSISTENT_LOG_PREF));
+ },
+
+ updateBrowserCacheDisabled() {
+ this.props.disableBrowserCache(
+ Services.prefs.getBoolPref(DEVTOOLS_DISABLE_CACHE_PREF));
+ },
+
render() {
let {
+ toggleRecording,
clearRequests,
requestFilterTypes,
setRequestFilterText,
networkDetailsToggleDisabled,
networkDetailsOpen,
toggleNetworkDetails,
togglePersistentLogs,
persistentLogsEnabled,
toggleBrowserCache,
browserCacheDisabled,
filteredRequests,
+ recording,
} = this.props;
let toggleButtonClassName = [
"network-details-panel-toggle",
"devtools-button",
];
+
if (!networkDetailsOpen) {
toggleButtonClassName.push("pane-collapsed");
}
+ // Render list of filter-buttons.
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,
@@ -106,20 +149,33 @@ const Toolbar = createClass({
"aria-pressed": checked,
"data-key": type,
},
TOOLBAR_FILTER_LABELS[type]
)
);
});
+ // Calculate class-list for toggle recording button. The button
+ // has two states: pause/play.
+ let toggleButtonClassList = [
+ "devtools-button",
+ recording ? "devtools-pause-icon" : "devtools-play-icon",
+ ];
+
+ // Render the entire toolbar.
return (
span({ className: "devtools-toolbar devtools-toolbar-container" },
span({ className: "devtools-toolbar-group" },
button({
+ className: toggleButtonClassList.join(" "),
+ title: TOOLBAR_TOGGLE_RECORDING,
+ onClick: toggleRecording,
+ }),
+ button({
className: "devtools-button devtools-clear-icon requests-list-clear-button",
title: TOOLBAR_CLEAR,
onClick: clearRequests,
}),
div({ className: "requests-list-filter-buttons" }, buttons),
label(
{
className: "devtools-checkbox-label",
@@ -156,71 +212,42 @@ const Toolbar = createClass({
placeholder: SEARCH_PLACE_HOLDER,
type: "filter",
onChange: setRequestFilterText,
autocompleteProvider: filter =>
autocompleteProvider(filter, filteredRequests),
}),
button({
className: toggleButtonClassName.join(" "),
- title: networkDetailsOpen ? COLLPASE_DETAILS_PANE : EXPAND_DETAILS_PANE,
+ title: networkDetailsOpen ? COLLAPSE_DETAILS_PANE : EXPAND_DETAILS_PANE,
disabled: networkDetailsToggleDisabled,
tabIndex: "0",
onClick: toggleNetworkDetails,
}),
)
)
);
},
-
- componentDidMount() {
- Services.prefs.addObserver(DEVTOOLS_ENABLE_PERSISTENT_LOG_PREF,
- this.updatePersistentLogsEnabled);
- Services.prefs.addObserver(DEVTOOLS_DISABLE_CACHE_PREF,
- this.updateBrowserCacheDisabled);
- },
-
- componentWillUnmount() {
- Services.prefs.removeObserver(DEVTOOLS_ENABLE_PERSISTENT_LOG_PREF,
- this.updatePersistentLogsEnabled);
- Services.prefs.removeObserver(DEVTOOLS_DISABLE_CACHE_PREF,
- this.updateBrowserCacheDisabled);
- },
-
- toggleRequestFilterType(evt) {
- if (evt.type === "keydown" && (evt.key !== "" || evt.key !== "Enter")) {
- return;
- }
- this.props.toggleRequestFilterType(evt.target.dataset.key);
- },
-
- updatePersistentLogsEnabled() {
- this.props.enablePersistentLogs(
- Services.prefs.getBoolPref(DEVTOOLS_ENABLE_PERSISTENT_LOG_PREF));
- },
-
- updateBrowserCacheDisabled() {
- this.props.disableBrowserCache(
- Services.prefs.getBoolPref(DEVTOOLS_DISABLE_CACHE_PREF));
- }
});
module.exports = connect(
(state) => ({
networkDetailsToggleDisabled: isNetworkDetailsToggleButtonDisabled(state),
networkDetailsOpen: state.ui.networkDetailsOpen,
persistentLogsEnabled: state.ui.persistentLogsEnabled,
browserCacheDisabled: state.ui.browserCacheDisabled,
+ recording: getRecordingState(state),
requestFilterTypes: getRequestFilterTypes(state),
filteredRequests: getTypeFilteredRequests(state),
summary: getDisplayedRequestsSummary(state),
}),
(dispatch) => ({
clearRequests: () => dispatch(Actions.clearRequests()),
- setRequestFilterText: (text) => dispatch(Actions.setRequestFilterText(text)),
- toggleRequestFilterType: (type) => dispatch(Actions.toggleRequestFilterType(type)),
- toggleNetworkDetails: () => dispatch(Actions.toggleNetworkDetails()),
+ disableBrowserCache: (disabled) => dispatch(Actions.disableBrowserCache(disabled)),
enablePersistentLogs: (enabled) => dispatch(Actions.enablePersistentLogs(enabled)),
+ setRequestFilterText: (text) => dispatch(Actions.setRequestFilterText(text)),
+ toggleBrowserCache: () => dispatch(Actions.toggleBrowserCache()),
+ toggleNetworkDetails: () => dispatch(Actions.toggleNetworkDetails()),
+ toggleRecording: () => dispatch(Actions.toggleRecording()),
togglePersistentLogs: () => dispatch(Actions.togglePersistentLogs()),
- disableBrowserCache: (disabled) => dispatch(Actions.disableBrowserCache(disabled)),
- toggleBrowserCache: () => dispatch(Actions.toggleBrowserCache()),
+ toggleRequestFilterType: (type) => dispatch(Actions.toggleRequestFilterType(type)),
}),
)(Toolbar);
--- a/devtools/client/netmonitor/src/constants.js
+++ b/devtools/client/netmonitor/src/constants.js
@@ -20,16 +20,17 @@ const actionTypes = {
REMOVE_SELECTED_CUSTOM_REQUEST: "REMOVE_SELECTED_CUSTOM_REQUEST",
RESET_COLUMNS: "RESET_COLUMNS",
SELECT_REQUEST: "SELECT_REQUEST",
SELECT_DETAILS_PANEL_TAB: "SELECT_DETAILS_PANEL_TAB",
SEND_CUSTOM_REQUEST: "SEND_CUSTOM_REQUEST",
SET_REQUEST_FILTER_TEXT: "SET_REQUEST_FILTER_TEXT",
SORT_BY: "SORT_BY",
TOGGLE_COLUMN: "TOGGLE_COLUMN",
+ TOGGLE_RECORDING: "TOGGLE_RECORDING",
TOGGLE_REQUEST_FILTER_TYPE: "TOGGLE_REQUEST_FILTER_TYPE",
UPDATE_REQUEST: "UPDATE_REQUEST",
WATERFALL_RESIZE: "WATERFALL_RESIZE",
};
// Descriptions for what this frontend is currently doing.
const ACTIVITY_TYPE = {
// Standing by and handling requests normally.
--- a/devtools/client/netmonitor/src/middleware/moz.build
+++ b/devtools/client/netmonitor/src/middleware/moz.build
@@ -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/.
DevToolsModules(
'batching.js',
'prefs.js',
+ 'recording.js',
'thunk.js',
)
new file mode 100644
--- /dev/null
+++ b/devtools/client/netmonitor/src/middleware/recording.js
@@ -0,0 +1,24 @@
+/* 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 {
+ TOGGLE_RECORDING,
+} = require("../constants");
+
+/**
+ * Start/stop HTTP traffic recording.
+ */
+function recordingMiddleware(store) {
+ return next => action => {
+ const res = next(action);
+ if (action.type === TOGGLE_RECORDING) {
+ // TODO connect/disconnect the backend.
+ }
+ return res;
+ };
+}
+
+module.exports = recordingMiddleware;
--- a/devtools/client/netmonitor/src/reducers/requests.js
+++ b/devtools/client/netmonitor/src/reducers/requests.js
@@ -9,16 +9,17 @@ const { getUrlDetails } = require("../ut
const {
ADD_REQUEST,
CLEAR_REQUESTS,
CLONE_SELECTED_REQUEST,
OPEN_NETWORK_DETAILS,
REMOVE_SELECTED_CUSTOM_REQUEST,
SELECT_REQUEST,
SEND_CUSTOM_REQUEST,
+ TOGGLE_RECORDING,
UPDATE_REQUEST,
UPDATE_PROPS,
} = require("../constants");
const Request = I.Record({
id: null,
// Set to true in case of a request that's being edited as part of "edit and resend"
isCustom: false,
@@ -63,16 +64,18 @@ const Requests = I.Record({
// The collection of requests (keyed by id)
requests: I.Map(),
// Selection state
selectedId: null,
preselectedId: null,
// Auxiliary fields to hold requests stats
firstStartedMillis: +Infinity,
lastEndedMillis: -Infinity,
+ // Recording state
+ recording: true,
});
/**
* Remove the currently selected custom request.
*/
function closeCustomRequest(state) {
let { requests, selectedId } = state;
@@ -115,16 +118,74 @@ function requestsReducer(state = new Req
// Select the request if it was preselected and there is no other selection
if (st.preselectedId && st.preselectedId === action.id) {
st.selectedId = st.selectedId || st.preselectedId;
st.preselectedId = null;
}
});
}
+ case CLEAR_REQUESTS: {
+ return new Requests({
+ recording: state.recording
+ });
+ }
+ case CLONE_SELECTED_REQUEST: {
+ let { requests, selectedId } = state;
+
+ if (!selectedId) {
+ return state;
+ }
+
+ let clonedRequest = requests.get(selectedId);
+ if (!clonedRequest) {
+ return state;
+ }
+
+ let newRequest = new Request({
+ id: clonedRequest.id + "-clone",
+ method: clonedRequest.method,
+ url: clonedRequest.url,
+ urlDetails: clonedRequest.urlDetails,
+ requestHeaders: clonedRequest.requestHeaders,
+ requestPostData: clonedRequest.requestPostData,
+ isCustom: true
+ });
+
+ return state.withMutations(st => {
+ st.requests = requests.set(newRequest.id, newRequest);
+ st.selectedId = newRequest.id;
+ });
+ }
+ case OPEN_NETWORK_DETAILS: {
+ if (!action.open) {
+ return state.set("selectedId", null);
+ }
+
+ if (!state.selectedId && !state.requests.isEmpty()) {
+ return state.set("selectedId", state.requests.first().id);
+ }
+
+ return state;
+ }
+ case REMOVE_SELECTED_CUSTOM_REQUEST: {
+ return closeCustomRequest(state);
+ }
+ case SELECT_REQUEST: {
+ return state.set("selectedId", action.id);
+ }
+ case SEND_CUSTOM_REQUEST: {
+ // When a new request with a given id is added in future, select it immediately.
+ // where we know in advance the ID of the request, at a time when it
+ // wasn't sent yet.
+ return closeCustomRequest(state.set("preselectedId", action.id));
+ }
+ case TOGGLE_RECORDING: {
+ return state.set("recording", !state.recording);
+ }
case UPDATE_REQUEST: {
let { requests, lastEndedMillis } = state;
let updatedRequest = requests.get(action.id);
if (!updatedRequest) {
return state;
}
@@ -155,69 +216,16 @@ function requestsReducer(state = new Req
}
});
return state.withMutations(st => {
st.requests = requests.set(updatedRequest.id, updatedRequest);
st.lastEndedMillis = lastEndedMillis;
});
}
- case CLEAR_REQUESTS: {
- return new Requests();
- }
- case SELECT_REQUEST: {
- return state.set("selectedId", action.id);
- }
- case CLONE_SELECTED_REQUEST: {
- let { requests, selectedId } = state;
-
- if (!selectedId) {
- return state;
- }
-
- let clonedRequest = requests.get(selectedId);
- if (!clonedRequest) {
- return state;
- }
-
- let newRequest = new Request({
- id: clonedRequest.id + "-clone",
- method: clonedRequest.method,
- url: clonedRequest.url,
- urlDetails: clonedRequest.urlDetails,
- requestHeaders: clonedRequest.requestHeaders,
- requestPostData: clonedRequest.requestPostData,
- isCustom: true
- });
-
- return state.withMutations(st => {
- st.requests = requests.set(newRequest.id, newRequest);
- st.selectedId = newRequest.id;
- });
- }
- case REMOVE_SELECTED_CUSTOM_REQUEST: {
- return closeCustomRequest(state);
- }
- case SEND_CUSTOM_REQUEST: {
- // When a new request with a given id is added in future, select it immediately.
- // where we know in advance the ID of the request, at a time when it
- // wasn't sent yet.
- return closeCustomRequest(state.set("preselectedId", action.id));
- }
- case OPEN_NETWORK_DETAILS: {
- if (!action.open) {
- return state.set("selectedId", null);
- }
-
- if (!state.selectedId && !state.requests.isEmpty()) {
- return state.set("selectedId", state.requests.first().id);
- }
-
- return state;
- }
default:
return state;
}
}
module.exports = {
Requests,
--- a/devtools/client/netmonitor/src/selectors/requests.js
+++ b/devtools/client/netmonitor/src/selectors/requests.js
@@ -122,17 +122,26 @@ const getSelectedRequest = createSelecto
function getRequestById(state, id) {
return state.requests.requests.get(id);
}
function getDisplayedRequestById(state, id) {
return getDisplayedRequests(state).find(r => r.id === id);
}
+/**
+ * Returns the current recording boolean state (HTTP traffic is
+ * monitored or not monitored)
+ */
+function getRecordingState(state) {
+ return state.requests.recording;
+}
+
module.exports = {
getDisplayedRequestById,
getDisplayedRequests,
getDisplayedRequestsSummary,
+ getRecordingState,
getRequestById,
getSelectedRequest,
getSortedRequests,
getTypeFilteredRequests,
};