summaryrefslogtreecommitdiffstats
path: root/devtools/client/netmonitor/src/reducers
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /devtools/client/netmonitor/src/reducers
parentInitial commit. (diff)
downloadthunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz
thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/netmonitor/src/reducers')
-rw-r--r--devtools/client/netmonitor/src/reducers/batching.js27
-rw-r--r--devtools/client/netmonitor/src/reducers/filters.js102
-rw-r--r--devtools/client/netmonitor/src/reducers/index.js47
-rw-r--r--devtools/client/netmonitor/src/reducers/messages.js335
-rw-r--r--devtools/client/netmonitor/src/reducers/moz.build16
-rw-r--r--devtools/client/netmonitor/src/reducers/request-blocking.js187
-rw-r--r--devtools/client/netmonitor/src/reducers/requests.js313
-rw-r--r--devtools/client/netmonitor/src/reducers/search.js118
-rw-r--r--devtools/client/netmonitor/src/reducers/sort.js48
-rw-r--r--devtools/client/netmonitor/src/reducers/timing-markers.js78
-rw-r--r--devtools/client/netmonitor/src/reducers/ui.js260
11 files changed, 1531 insertions, 0 deletions
diff --git a/devtools/client/netmonitor/src/reducers/batching.js b/devtools/client/netmonitor/src/reducers/batching.js
new file mode 100644
index 0000000000..bc4f02beb2
--- /dev/null
+++ b/devtools/client/netmonitor/src/reducers/batching.js
@@ -0,0 +1,27 @@
+/* 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 {
+ BATCH_ACTIONS,
+} = require("resource://devtools/client/netmonitor/src/constants.js");
+
+/**
+ * A reducer to handle batched actions. For each action in the BATCH_ACTIONS array,
+ * the reducer is called successively on the array of batched actions, resulting in
+ * only one state update.
+ */
+function batchingReducer(nextReducer) {
+ return function reducer(state, action) {
+ switch (action.type) {
+ case BATCH_ACTIONS:
+ return action.actions.reduce(reducer, state);
+ default:
+ return nextReducer(state, action);
+ }
+ };
+}
+
+module.exports = batchingReducer;
diff --git a/devtools/client/netmonitor/src/reducers/filters.js b/devtools/client/netmonitor/src/reducers/filters.js
new file mode 100644
index 0000000000..4635f6f94a
--- /dev/null
+++ b/devtools/client/netmonitor/src/reducers/filters.js
@@ -0,0 +1,102 @@
+/* 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 {
+ ENABLE_REQUEST_FILTER_TYPE_ONLY,
+ TOGGLE_REQUEST_FILTER_TYPE,
+ SET_REQUEST_FILTER_TEXT,
+ FILTER_TAGS,
+} = require("resource://devtools/client/netmonitor/src/constants.js");
+
+function FilterTypes(overrideParams = {}) {
+ const allFilterTypes = ["all"].concat(FILTER_TAGS);
+ // filter only those keys which are valid filter tags
+ overrideParams = Object.keys(overrideParams)
+ .filter(key => allFilterTypes.includes(key))
+ .reduce((obj, key) => {
+ obj[key] = overrideParams[key];
+ return obj;
+ }, {});
+ const filterTypes = allFilterTypes.reduce(
+ (o, tag) => Object.assign(o, { [tag]: false }),
+ {}
+ );
+ return Object.assign({}, filterTypes, overrideParams);
+}
+
+function Filters(overrideParams = {}) {
+ return Object.assign(
+ {
+ requestFilterTypes: new FilterTypes({ all: true }),
+ requestFilterText: "",
+ },
+ overrideParams
+ );
+}
+
+function toggleRequestFilterType(state, action) {
+ const { filter } = action;
+ let newState;
+
+ // Ignore unknown filter type
+ if (!state.hasOwnProperty(filter)) {
+ return state;
+ }
+ if (filter === "all") {
+ return new FilterTypes({ all: true });
+ }
+
+ newState = { ...state };
+ newState.all = false;
+ newState[filter] = !state[filter];
+
+ if (!Object.values(newState).includes(true)) {
+ newState = new FilterTypes({ all: true });
+ }
+
+ return newState;
+}
+
+function enableRequestFilterTypeOnly(state, action) {
+ const { filter } = action;
+
+ // Ignore unknown filter type
+ if (!state.hasOwnProperty(filter)) {
+ return state;
+ }
+
+ return new FilterTypes({ [filter]: true });
+}
+
+function filters(state = new Filters(), action) {
+ state = { ...state };
+ switch (action.type) {
+ case ENABLE_REQUEST_FILTER_TYPE_ONLY:
+ state.requestFilterTypes = enableRequestFilterTypeOnly(
+ state.requestFilterTypes,
+ action
+ );
+ break;
+ case TOGGLE_REQUEST_FILTER_TYPE:
+ state.requestFilterTypes = toggleRequestFilterType(
+ state.requestFilterTypes,
+ action
+ );
+ break;
+ case SET_REQUEST_FILTER_TEXT:
+ state.requestFilterText = action.text;
+ break;
+ default:
+ break;
+ }
+ return state;
+}
+
+module.exports = {
+ FilterTypes,
+ Filters,
+ filters,
+};
diff --git a/devtools/client/netmonitor/src/reducers/index.js b/devtools/client/netmonitor/src/reducers/index.js
new file mode 100644
index 0000000000..d73765f795
--- /dev/null
+++ b/devtools/client/netmonitor/src/reducers/index.js
@@ -0,0 +1,47 @@
+/* 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("resource://devtools/client/shared/vendor/redux.js");
+const batchingReducer = require("resource://devtools/client/netmonitor/src/reducers/batching.js");
+const requestBlockingReducer = require("resource://devtools/client/netmonitor/src/reducers/request-blocking.js");
+const {
+ requestsReducer,
+} = require("resource://devtools/client/netmonitor/src/reducers/requests.js");
+const {
+ search,
+} = require("resource://devtools/client/netmonitor/src/reducers/search.js");
+const {
+ sortReducer,
+} = require("resource://devtools/client/netmonitor/src/reducers/sort.js");
+const {
+ filters,
+} = require("resource://devtools/client/netmonitor/src/reducers/filters.js");
+const {
+ timingMarkers,
+} = require("resource://devtools/client/netmonitor/src/reducers/timing-markers.js");
+const {
+ ui,
+} = require("resource://devtools/client/netmonitor/src/reducers/ui.js");
+const {
+ messages,
+} = require("resource://devtools/client/netmonitor/src/reducers/messages.js");
+const networkThrottling = require("resource://devtools/client/shared/components/throttling/reducer.js");
+
+module.exports = batchingReducer(
+ combineReducers({
+ filters,
+ messages,
+ networkThrottling,
+ requestBlocking: requestBlockingReducer,
+ requests: requestsReducer,
+ search,
+ sort: sortReducer,
+ timingMarkers,
+ ui,
+ })
+);
diff --git a/devtools/client/netmonitor/src/reducers/messages.js b/devtools/client/netmonitor/src/reducers/messages.js
new file mode 100644
index 0000000000..27d7da28c7
--- /dev/null
+++ b/devtools/client/netmonitor/src/reducers/messages.js
@@ -0,0 +1,335 @@
+/* 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 {
+ SELECT_REQUEST,
+ MSG_ADD,
+ MSG_SELECT,
+ MSG_OPEN_DETAILS,
+ MSG_CLEAR,
+ MSG_TOGGLE_FILTER_TYPE,
+ MSG_TOGGLE_CONTROL,
+ MSG_SET_FILTER_TEXT,
+ MSG_TOGGLE_COLUMN,
+ MSG_RESET_COLUMNS,
+ MSG_CLOSE_CONNECTION,
+ CHANNEL_TYPE,
+ SET_EVENT_STREAM_FLAG,
+} = require("resource://devtools/client/netmonitor/src/constants.js");
+
+/**
+ * The default column states for the MessageListItem component.
+ */
+const defaultColumnsState = {
+ data: true,
+ size: false,
+ time: true,
+};
+
+const defaultWSColumnsState = {
+ ...defaultColumnsState,
+ opCode: false,
+ maskBit: false,
+ finBit: false,
+};
+
+const defaultSSEColumnsState = {
+ ...defaultColumnsState,
+ eventName: false,
+ lastEventId: false,
+ retry: false,
+};
+
+/**
+ * Returns a new object of default cols.
+ */
+function getMessageDefaultColumnsState(channelType) {
+ let columnsState = defaultColumnsState;
+ const { EVENT_STREAM, WEB_SOCKET } = CHANNEL_TYPE;
+
+ if (channelType === WEB_SOCKET) {
+ columnsState = defaultWSColumnsState;
+ } else if (channelType === EVENT_STREAM) {
+ columnsState = defaultSSEColumnsState;
+ }
+
+ return Object.assign({}, columnsState);
+}
+
+/**
+ * This structure stores list of all WebSocket and EventSource messages received
+ * from the backend.
+ */
+function Messages(initialState = {}) {
+ const { EVENT_STREAM, WEB_SOCKET } = CHANNEL_TYPE;
+
+ return {
+ // Map with all requests (key = resourceId, value = array of message objects)
+ messages: new Map(),
+ messageFilterText: "",
+ // Default filter type is "all",
+ messageFilterType: "all",
+ showControlFrames: false,
+ selectedMessage: null,
+ messageDetailsOpen: false,
+ currentChannelId: null,
+ currentChannelType: null,
+ currentRequestId: null,
+ closedConnections: new Map(),
+ columns: null,
+ sseColumns: getMessageDefaultColumnsState(EVENT_STREAM),
+ wsColumns: getMessageDefaultColumnsState(WEB_SOCKET),
+ ...initialState,
+ };
+}
+
+/**
+ * When a network request is selected,
+ * set the current resourceId affiliated with the connection.
+ */
+function setCurrentChannel(state, action) {
+ if (!action.request) {
+ return state;
+ }
+
+ const { id, cause, resourceId, isEventStream } = action.request;
+ const { EVENT_STREAM, WEB_SOCKET } = CHANNEL_TYPE;
+ let currentChannelType = null;
+ let columnsKey = "columns";
+ if (cause.type === "websocket") {
+ currentChannelType = WEB_SOCKET;
+ columnsKey = "wsColumns";
+ } else if (isEventStream) {
+ currentChannelType = EVENT_STREAM;
+ columnsKey = "sseColumns";
+ }
+
+ return {
+ ...state,
+ columns:
+ currentChannelType === state.currentChannelType
+ ? { ...state.columns }
+ : { ...state[columnsKey] },
+ currentChannelId: resourceId,
+ currentChannelType,
+ currentRequestId: id,
+ // Default filter text is empty string for a new connection
+ messageFilterText: "",
+ };
+}
+
+/**
+ * If the request is already selected and isEventStream flag
+ * is added later, we need to update currentChannelType & columns.
+ */
+function updateCurrentChannel(state, action) {
+ if (state.currentRequestId === action.id) {
+ const currentChannelType = CHANNEL_TYPE.EVENT_STREAM;
+ return {
+ ...state,
+ columns: { ...state.sseColumns },
+ currentChannelType,
+ };
+ }
+ return state;
+}
+
+/**
+ * Appending new message into the map.
+ */
+function addMessage(state, action) {
+ const { httpChannelId } = action;
+ const nextState = { ...state };
+
+ const newMessage = {
+ httpChannelId,
+ ...action.data,
+ };
+
+ nextState.messages = mapSet(
+ nextState.messages,
+ newMessage.httpChannelId,
+ newMessage
+ );
+
+ return nextState;
+}
+
+/**
+ * Select specific message.
+ */
+function selectMessage(state, action) {
+ return {
+ ...state,
+ selectedMessage: action.message,
+ messageDetailsOpen: action.open,
+ };
+}
+
+/**
+ * Shows/Hides the MessagePayload component.
+ */
+function openMessageDetails(state, action) {
+ return {
+ ...state,
+ messageDetailsOpen: action.open,
+ };
+}
+
+/**
+ * Clear messages of the request from the state.
+ */
+function clearMessages(state) {
+ const nextState = { ...state };
+ const defaultState = Messages();
+ nextState.messages = new Map(state.messages);
+ nextState.messages.delete(nextState.currentChannelId);
+
+ // Reset fields to default state.
+ nextState.selectedMessage = defaultState.selectedMessage;
+ nextState.messageDetailsOpen = defaultState.messageDetailsOpen;
+
+ return nextState;
+}
+
+/**
+ * Toggle the message filter type of the connection.
+ */
+function toggleMessageFilterType(state, action) {
+ return {
+ ...state,
+ messageFilterType: action.filter,
+ };
+}
+
+/**
+ * Toggle control frames for the WebSocket connection.
+ */
+function toggleControlFrames(state, action) {
+ return {
+ ...state,
+ showControlFrames: !state.showControlFrames,
+ };
+}
+
+/**
+ * Set the filter text of the current channelId.
+ */
+function setMessageFilterText(state, action) {
+ return {
+ ...state,
+ messageFilterText: action.text,
+ };
+}
+
+/**
+ * Toggle the user specified column view state.
+ */
+function toggleColumn(state, action) {
+ const { column } = action;
+ let columnsKey = null;
+ if (state.currentChannelType === CHANNEL_TYPE.WEB_SOCKET) {
+ columnsKey = "wsColumns";
+ } else {
+ columnsKey = "sseColumns";
+ }
+ const newColumnsState = {
+ ...state[columnsKey],
+ [column]: !state[columnsKey][column],
+ };
+ return {
+ ...state,
+ columns: newColumnsState,
+ [columnsKey]: newColumnsState,
+ };
+}
+
+/**
+ * Reset back to default columns view state.
+ */
+function resetColumns(state) {
+ let columnsKey = null;
+ if (state.currentChannelType === CHANNEL_TYPE.WEB_SOCKET) {
+ columnsKey = "wsColumns";
+ } else {
+ columnsKey = "sseColumns";
+ }
+ const newColumnsState = getMessageDefaultColumnsState(
+ state.currentChannelType
+ );
+ return {
+ ...state,
+ [columnsKey]: newColumnsState,
+ columns: newColumnsState,
+ };
+}
+
+function closeConnection(state, action) {
+ const { httpChannelId, code, reason } = action;
+ const nextState = { ...state };
+
+ nextState.closedConnections.set(httpChannelId, {
+ code,
+ reason,
+ });
+
+ return nextState;
+}
+
+/**
+ * Append new item into existing map and return new map.
+ */
+function mapSet(map, key, value) {
+ const newMap = new Map(map);
+ if (newMap.has(key)) {
+ const messagesArray = [...newMap.get(key)];
+ messagesArray.push(value);
+ newMap.set(key, messagesArray);
+ return newMap;
+ }
+ return newMap.set(key, [value]);
+}
+
+/**
+ * This reducer is responsible for maintaining list of
+ * messages within the Network panel.
+ */
+function messages(state = Messages(), action) {
+ switch (action.type) {
+ case SELECT_REQUEST:
+ return setCurrentChannel(state, action);
+ case SET_EVENT_STREAM_FLAG:
+ return updateCurrentChannel(state, action);
+ case MSG_ADD:
+ return addMessage(state, action);
+ case MSG_SELECT:
+ return selectMessage(state, action);
+ case MSG_OPEN_DETAILS:
+ return openMessageDetails(state, action);
+ case MSG_CLEAR:
+ return clearMessages(state);
+ case MSG_TOGGLE_FILTER_TYPE:
+ return toggleMessageFilterType(state, action);
+ case MSG_TOGGLE_CONTROL:
+ return toggleControlFrames(state, action);
+ case MSG_SET_FILTER_TEXT:
+ return setMessageFilterText(state, action);
+ case MSG_TOGGLE_COLUMN:
+ return toggleColumn(state, action);
+ case MSG_RESET_COLUMNS:
+ return resetColumns(state);
+ case MSG_CLOSE_CONNECTION:
+ return closeConnection(state, action);
+ default:
+ return state;
+ }
+}
+
+module.exports = {
+ Messages,
+ messages,
+ getMessageDefaultColumnsState,
+};
diff --git a/devtools/client/netmonitor/src/reducers/moz.build b/devtools/client/netmonitor/src/reducers/moz.build
new file mode 100644
index 0000000000..bd9d238761
--- /dev/null
+++ b/devtools/client/netmonitor/src/reducers/moz.build
@@ -0,0 +1,16 @@
+# 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",
+ "filters.js",
+ "index.js",
+ "messages.js",
+ "request-blocking.js",
+ "requests.js",
+ "search.js",
+ "sort.js",
+ "timing-markers.js",
+ "ui.js",
+)
diff --git a/devtools/client/netmonitor/src/reducers/request-blocking.js b/devtools/client/netmonitor/src/reducers/request-blocking.js
new file mode 100644
index 0000000000..ffd0d8c97a
--- /dev/null
+++ b/devtools/client/netmonitor/src/reducers/request-blocking.js
@@ -0,0 +1,187 @@
+/* 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 {
+ ADD_BLOCKED_URL,
+ DISABLE_MATCHING_URLS,
+ TOGGLE_BLOCKED_URL,
+ UPDATE_BLOCKED_URL,
+ REMOVE_BLOCKED_URL,
+ REMOVE_ALL_BLOCKED_URLS,
+ ENABLE_ALL_BLOCKED_URLS,
+ DISABLE_ALL_BLOCKED_URLS,
+ TOGGLE_BLOCKING_ENABLED,
+ SYNCED_BLOCKED_URLS,
+} = require("resource://devtools/client/netmonitor/src/constants.js");
+
+function RequestBlocking() {
+ return {
+ blockedUrls: [],
+ blockingSynced: false,
+ blockingEnabled: true,
+ };
+}
+
+function requestBlockingReducer(state = RequestBlocking(), action) {
+ switch (action.type) {
+ case ADD_BLOCKED_URL:
+ return addBlockedUrl(state, action);
+ case REMOVE_BLOCKED_URL:
+ return removeBlockedUrl(state, action);
+ case REMOVE_ALL_BLOCKED_URLS:
+ return removeAllBlockedUrls(state, action);
+ case UPDATE_BLOCKED_URL:
+ return updateBlockedUrl(state, action);
+ case TOGGLE_BLOCKED_URL:
+ return toggleBlockedUrl(state, action);
+ case TOGGLE_BLOCKING_ENABLED:
+ return toggleBlockingEnabled(state, action);
+ case ENABLE_ALL_BLOCKED_URLS:
+ return enableAllBlockedUrls(state, action);
+ case DISABLE_ALL_BLOCKED_URLS:
+ return disableAllBlockedUrls(state, action);
+ case DISABLE_MATCHING_URLS:
+ return disableOrRemoveMatchingUrls(state, action);
+ case SYNCED_BLOCKED_URLS:
+ return syncedBlockedUrls(state, action);
+ default:
+ return state;
+ }
+}
+
+function syncedBlockedUrls(state, action) {
+ // Indicates whether the blocked url has been synced
+ // with the server once. We don't need to do it once netmonitor is open.
+ return {
+ ...state,
+ blockingSynced: action.synced,
+ };
+}
+
+function toggleBlockingEnabled(state, action) {
+ return {
+ ...state,
+ blockingEnabled: action.enabled,
+ };
+}
+
+function addBlockedUrl(state, action) {
+ // The user can paste in a list of URLS so we need to cleanse the input
+ // Pasting a list turns new lines into spaces
+ const uniqueUrls = [...new Set(action.url.split(" "))].map(url => url.trim());
+
+ const newUrls = uniqueUrls
+ // Ensure the URL isn't already blocked
+ .filter(url => url && !state.blockedUrls.some(item => item.url === url))
+ // Add new URLs as enabled by default
+ .map(url => ({ url, enabled: true }));
+
+ // If the user is trying to block a URL that's currently in the list but disabled,
+ // re-enable the old item
+ const currentBlockedUrls = state.blockedUrls.map(item =>
+ uniqueUrls.includes(item.url) ? { url: item.url, enabled: true } : item
+ );
+
+ const blockedUrls = [...currentBlockedUrls, ...newUrls];
+ return {
+ ...state,
+ blockedUrls,
+ };
+}
+
+function removeBlockedUrl(state, action) {
+ return {
+ ...state,
+ blockedUrls: state.blockedUrls.filter(item => item.url != action.url),
+ };
+}
+
+function removeAllBlockedUrls(state, action) {
+ return {
+ ...state,
+ blockedUrls: [],
+ };
+}
+
+function enableAllBlockedUrls(state, action) {
+ const blockedUrls = state.blockedUrls.map(item => ({
+ ...item,
+ enabled: true,
+ }));
+ return {
+ ...state,
+ blockedUrls,
+ };
+}
+
+function disableAllBlockedUrls(state, action) {
+ const blockedUrls = state.blockedUrls.map(item => ({
+ ...item,
+ enabled: false,
+ }));
+ return {
+ ...state,
+ blockedUrls,
+ };
+}
+
+function updateBlockedUrl(state, action) {
+ const { oldUrl, newUrl } = action;
+ let { blockedUrls } = state;
+
+ if (!blockedUrls.find(item => item.url === newUrl)) {
+ blockedUrls = blockedUrls.map(item => {
+ if (item.url === oldUrl) {
+ return { ...item, url: newUrl };
+ }
+ return item;
+ });
+ } else {
+ blockedUrls = blockedUrls.filter(item => item.url != oldUrl);
+ }
+
+ return {
+ ...state,
+ blockedUrls,
+ };
+}
+
+function toggleBlockedUrl(state, action) {
+ const blockedUrls = state.blockedUrls.map(item => {
+ if (item.url === action.url) {
+ return { ...item, enabled: !item.enabled };
+ }
+ return item;
+ });
+
+ return {
+ ...state,
+ blockedUrls,
+ };
+}
+
+function disableOrRemoveMatchingUrls(state, action) {
+ const blockedUrls = state.blockedUrls
+ .map(item => {
+ // If the url matches exactly, remove the entry
+ if (action.url === item.url) {
+ return null;
+ }
+ // If just a partial match, disable the entry
+ if (action.url.includes(item.url)) {
+ return { ...item, enabled: false };
+ }
+ return item;
+ })
+ .filter(Boolean);
+
+ return {
+ ...state,
+ blockedUrls,
+ };
+}
+
+module.exports = requestBlockingReducer;
diff --git a/devtools/client/netmonitor/src/reducers/requests.js b/devtools/client/netmonitor/src/reducers/requests.js
new file mode 100644
index 0000000000..0bc503727e
--- /dev/null
+++ b/devtools/client/netmonitor/src/reducers/requests.js
@@ -0,0 +1,313 @@
+/* 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 {
+ processNetworkUpdates,
+} = require("resource://devtools/client/netmonitor/src/utils/request-utils.js");
+const {
+ ADD_REQUEST,
+ SET_EVENT_STREAM_FLAG,
+ CLEAR_REQUESTS,
+ CLONE_REQUEST,
+ CLONE_SELECTED_REQUEST,
+ OPEN_NETWORK_DETAILS,
+ REMOVE_SELECTED_CUSTOM_REQUEST,
+ RIGHT_CLICK_REQUEST,
+ SELECT_REQUEST,
+ PRESELECT_REQUEST,
+ SEND_CUSTOM_REQUEST,
+ SET_RECORDING_STATE,
+ UPDATE_REQUEST,
+} = require("resource://devtools/client/netmonitor/src/constants.js");
+
+/**
+ * This structure stores list of all HTTP requests received
+ * from the backend. It's using plain JS structures to store
+ * data instead of ImmutableJS, which is performance expensive.
+ */
+function Requests() {
+ return {
+ // Map with all requests (key = actor ID, value = request object)
+ requests: [],
+ // Selected request ID
+ selectedId: null,
+ // Right click request represents the last request that was clicked
+ clickedRequestId: null,
+ // @backward-compact { version 85 } The preselectedId can either be
+ // the actor id on old servers, or the resourceId on new ones.
+ preselectedId: null,
+ // True if the monitor is recording HTTP traffic
+ recording: true,
+ // Auxiliary fields to hold requests stats
+ firstStartedMs: +Infinity,
+ lastEndedMs: -Infinity,
+ };
+}
+
+/**
+ * This reducer is responsible for maintaining list of request
+ * within the Network panel.
+ */
+function requestsReducer(state = Requests(), action) {
+ switch (action.type) {
+ // Appending new request into the list/map.
+ case ADD_REQUEST: {
+ return addRequest(state, action);
+ }
+
+ // Update an existing request (with received data).
+ case UPDATE_REQUEST: {
+ return updateRequest(state, action);
+ }
+
+ // Add isEventStream flag to a request.
+ case SET_EVENT_STREAM_FLAG: {
+ return setEventStreamFlag(state, action);
+ }
+
+ // Remove all requests in the list. Create fresh new state
+ // object, but keep value of the `recording` field.
+ case CLEAR_REQUESTS: {
+ return {
+ ...Requests(),
+ recording: state.recording,
+ };
+ }
+
+ // Select specific request.
+ case SELECT_REQUEST: {
+ return {
+ ...state,
+ clickedRequestId: action.id,
+ selectedId: action.id,
+ };
+ }
+
+ // Clone selected request for re-send.
+ case CLONE_REQUEST: {
+ return cloneRequest(state, action.id);
+ }
+
+ case CLONE_SELECTED_REQUEST: {
+ return cloneRequest(state, state.selectedId);
+ }
+
+ case RIGHT_CLICK_REQUEST: {
+ return {
+ ...state,
+ clickedRequestId: action.id,
+ };
+ }
+
+ case PRESELECT_REQUEST: {
+ return {
+ ...state,
+ preselectedId: action.id,
+ };
+ }
+
+ // Removing temporary cloned request (created for re-send, but canceled).
+ case REMOVE_SELECTED_CUSTOM_REQUEST: {
+ return closeCustomRequest(state);
+ }
+
+ // Re-sending an existing request.
+ 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, preselectedId: action.id });
+ }
+
+ // Pause/resume button clicked.
+ case SET_RECORDING_STATE: {
+ return {
+ ...state,
+ recording: action.recording,
+ };
+ }
+
+ // Side bar with request details opened.
+ case OPEN_NETWORK_DETAILS: {
+ const nextState = { ...state };
+ if (!action.open) {
+ nextState.selectedId = null;
+ return nextState;
+ }
+
+ if (!state.selectedId && action.defaultSelectedId) {
+ nextState.selectedId = action.defaultSelectedId;
+ return nextState;
+ }
+
+ return state;
+ }
+
+ default:
+ return state;
+ }
+}
+
+// Helpers
+
+function addRequest(state, action) {
+ const nextState = { ...state };
+ // The target front is not used and cannot be serialized by redux
+ // eslint-disable-next-line no-unused-vars
+ const { targetFront, ...requestData } = action.data;
+ const newRequest = {
+ id: action.id,
+ ...requestData,
+ };
+
+ nextState.requests = [...state.requests, newRequest];
+
+ // Update the started/ended timestamps.
+ const { startedMs } = action.data;
+ if (startedMs < state.firstStartedMs) {
+ nextState.firstStartedMs = startedMs;
+ }
+ if (startedMs > state.lastEndedMs) {
+ nextState.lastEndedMs = startedMs;
+ }
+
+ // Select the request if it was preselected and there is no other selection.
+ if (state.preselectedId) {
+ if (state.preselectedId === action.id) {
+ nextState.selectedId = state.selectedId || state.preselectedId;
+ }
+ // @backward-compact { version 85 } The preselectedId can be resourceId
+ // instead of actor id when a custom request is created, and could not be
+ // selected immediately because it was not yet in the request map.
+ else if (state.preselectedId === newRequest.resourceId) {
+ nextState.selectedId = action.id;
+ }
+ nextState.preselectedId = null;
+ }
+
+ return nextState;
+}
+
+function updateRequest(state, action) {
+ const { requests, lastEndedMs } = state;
+
+ const { id } = action;
+ const index = requests.findIndex(needle => needle.id === id);
+ if (index === -1) {
+ return state;
+ }
+ const request = requests[index];
+
+ const nextRequest = {
+ ...request,
+ ...processNetworkUpdates(action.data),
+ };
+ const requestEndTime =
+ nextRequest.startedMs +
+ (nextRequest.eventTimings ? nextRequest.eventTimings.totalTime : 0);
+
+ const nextRequests = [...requests];
+ nextRequests[index] = nextRequest;
+ return {
+ ...state,
+ requests: nextRequests,
+ lastEndedMs: requestEndTime > lastEndedMs ? requestEndTime : lastEndedMs,
+ };
+}
+
+function setEventStreamFlag(state, action) {
+ const { requests } = state;
+ const { id } = action;
+ const index = requests.findIndex(needle => needle.id === id);
+ if (index === -1) {
+ return state;
+ }
+
+ const request = requests[index];
+
+ const nextRequest = {
+ ...request,
+ isEventStream: true,
+ };
+
+ const nextRequests = [...requests];
+ nextRequests[index] = nextRequest;
+ return {
+ ...state,
+ requests: nextRequests,
+ };
+}
+
+function cloneRequest(state, id) {
+ const { requests } = state;
+
+ if (!id) {
+ return state;
+ }
+
+ const clonedRequest = requests.find(needle => needle.id === id);
+ if (!clonedRequest) {
+ return state;
+ }
+
+ const newRequest = {
+ id: clonedRequest.id + "-clone",
+ method: clonedRequest.method,
+ cause: clonedRequest.cause,
+ url: clonedRequest.url,
+ urlDetails: clonedRequest.urlDetails,
+ requestHeaders: clonedRequest.requestHeaders,
+ requestPostData: clonedRequest.requestPostData,
+ requestPostDataAvailable: clonedRequest.requestPostDataAvailable,
+ requestHeadersAvailable: clonedRequest.requestHeadersAvailable,
+ isCustom: true,
+ };
+
+ return {
+ ...state,
+ requests: [...requests, newRequest],
+ selectedId: newRequest.id,
+ preselectedId: id,
+ };
+}
+
+/**
+ * Remove the currently selected custom request.
+ */
+function closeCustomRequest(state) {
+ const { requests, selectedId, preselectedId } = state;
+
+ if (!selectedId) {
+ return state;
+ }
+
+ // Find the cloned requests to be removed
+ const removedRequest = requests.find(needle => needle.id === selectedId);
+
+ // If the custom request is already in the Map, select it immediately,
+ // and reset `preselectedId` attribute.
+ // @backward-compact { version 85 } The preselectId can also be a resourceId
+ // or an actor id.
+ const customRequest = requests.find(
+ needle => needle.id === preselectedId || needle.resourceId === preselectedId
+ );
+ const hasPreselectedId = preselectedId && customRequest;
+
+ return {
+ ...state,
+ // Only custom requests can be removed
+ [removedRequest?.isCustom && "requests"]: requests.filter(
+ item => item.id !== selectedId
+ ),
+ preselectedId: hasPreselectedId ? null : preselectedId,
+ selectedId: hasPreselectedId ? customRequest.id : null,
+ };
+}
+
+module.exports = {
+ Requests,
+ requestsReducer,
+};
diff --git a/devtools/client/netmonitor/src/reducers/search.js b/devtools/client/netmonitor/src/reducers/search.js
new file mode 100644
index 0000000000..91e843e18c
--- /dev/null
+++ b/devtools/client/netmonitor/src/reducers/search.js
@@ -0,0 +1,118 @@
+/* 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 {
+ ADD_SEARCH_QUERY,
+ ADD_SEARCH_RESULT,
+ CLEAR_SEARCH_RESULTS,
+ ADD_ONGOING_SEARCH,
+ SEARCH_STATUS,
+ TOGGLE_SEARCH_CASE_SENSITIVE_SEARCH,
+ UPDATE_SEARCH_STATUS,
+ SET_TARGET_SEARCH_RESULT,
+} = require("resource://devtools/client/netmonitor/src/constants.js");
+
+/**
+ * Search reducer stores the following data:
+ * - query [String]: the string the user is looking for
+ * - results [Object]: the list of search results
+ * - ongoingSearch [Object]: the object representing the current search
+ * - status [String]: status of the current search (see constants.js)
+ */
+function Search(overrideParams = {}) {
+ return Object.assign(
+ {
+ query: "",
+ results: [],
+ ongoingSearch: null,
+ status: SEARCH_STATUS.INITIAL,
+ caseSensitive: false,
+ targetSearchResult: null,
+ },
+ overrideParams
+ );
+}
+
+function search(state = new Search(), action) {
+ switch (action.type) {
+ case ADD_SEARCH_QUERY:
+ return onAddSearchQuery(state, action);
+ case ADD_SEARCH_RESULT:
+ return onAddSearchResult(state, action);
+ case CLEAR_SEARCH_RESULTS:
+ return onClearSearchResults(state);
+ case ADD_ONGOING_SEARCH:
+ return onAddOngoingSearch(state, action);
+ case TOGGLE_SEARCH_CASE_SENSITIVE_SEARCH:
+ return onToggleCaseSensitiveSearch(state);
+ case UPDATE_SEARCH_STATUS:
+ return onUpdateSearchStatus(state, action);
+ case SET_TARGET_SEARCH_RESULT:
+ return onSetTargetSearchResult(state, action);
+ }
+ return state;
+}
+
+function onAddSearchQuery(state, action) {
+ return {
+ ...state,
+ query: action.query,
+ };
+}
+
+function onAddSearchResult(state, action) {
+ const { resource } = action;
+ const results = state.results.slice();
+ results.push({
+ resource,
+ results: action.result,
+ });
+
+ return {
+ ...state,
+ results,
+ };
+}
+
+function onClearSearchResults(state) {
+ return {
+ ...state,
+ results: [],
+ };
+}
+
+function onAddOngoingSearch(state, action) {
+ return {
+ ...state,
+ ongoingSearch: action.ongoingSearch,
+ };
+}
+
+function onToggleCaseSensitiveSearch(state) {
+ return {
+ ...state,
+ caseSensitive: !state.caseSensitive,
+ };
+}
+
+function onUpdateSearchStatus(state, action) {
+ return {
+ ...state,
+ status: action.status,
+ };
+}
+
+function onSetTargetSearchResult(state, action) {
+ return {
+ ...state,
+ targetSearchResult: action.searchResult,
+ };
+}
+
+module.exports = {
+ Search,
+ search,
+};
diff --git a/devtools/client/netmonitor/src/reducers/sort.js b/devtools/client/netmonitor/src/reducers/sort.js
new file mode 100644
index 0000000000..0d3486f57b
--- /dev/null
+++ b/devtools/client/netmonitor/src/reducers/sort.js
@@ -0,0 +1,48 @@
+/* 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 {
+ SORT_BY,
+ RESET_COLUMNS,
+} = require("resource://devtools/client/netmonitor/src/constants.js");
+
+function Sort() {
+ return {
+ // null means: sort by "waterfall", but don't highlight the table header
+ type: null,
+ ascending: true,
+ };
+}
+
+function sortReducer(state = new Sort(), action) {
+ switch (action.type) {
+ case SORT_BY: {
+ state = { ...state };
+ if (action.sortType != null && action.sortType == state.type) {
+ state.ascending = !state.ascending;
+ } else {
+ state.type = action.sortType;
+ state.ascending = true;
+ }
+ return state;
+ }
+
+ case RESET_COLUMNS: {
+ state = { ...state };
+ state.type = null;
+ state.ascending = true;
+ return state;
+ }
+
+ default:
+ return state;
+ }
+}
+
+module.exports = {
+ Sort,
+ sortReducer,
+};
diff --git a/devtools/client/netmonitor/src/reducers/timing-markers.js b/devtools/client/netmonitor/src/reducers/timing-markers.js
new file mode 100644
index 0000000000..4a41f9b495
--- /dev/null
+++ b/devtools/client/netmonitor/src/reducers/timing-markers.js
@@ -0,0 +1,78 @@
+/* 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 {
+ ADD_REQUEST,
+ ADD_TIMING_MARKER,
+ CLEAR_TIMING_MARKERS,
+ CLEAR_REQUESTS,
+} = require("resource://devtools/client/netmonitor/src/constants.js");
+
+function TimingMarkers() {
+ return {
+ firstDocumentDOMContentLoadedTimestamp: -1,
+ firstDocumentLoadTimestamp: -1,
+ firstDocumentRequestStartTimestamp: +Infinity,
+ };
+}
+
+function addRequest(state, action) {
+ const nextState = { ...state };
+ const { startedMs } = action.data;
+ if (startedMs < state.firstDocumentRequestStartTimestamp) {
+ nextState.firstDocumentRequestStartTimestamp = startedMs;
+ }
+
+ return nextState;
+}
+
+function addTimingMarker(state, action) {
+ state = { ...state };
+
+ if (
+ action.marker.name === "dom-interactive" &&
+ state.firstDocumentDOMContentLoadedTimestamp === -1
+ ) {
+ state.firstDocumentDOMContentLoadedTimestamp = action.marker.time;
+ return state;
+ }
+
+ if (
+ action.marker.name === "dom-complete" &&
+ state.firstDocumentLoadTimestamp === -1
+ ) {
+ state.firstDocumentLoadTimestamp = action.marker.time;
+ return state;
+ }
+
+ return state;
+}
+
+function clearTimingMarkers(state) {
+ return new TimingMarkers();
+}
+
+function timingMarkers(state = new TimingMarkers(), action) {
+ switch (action.type) {
+ case ADD_REQUEST:
+ return addRequest(state, action);
+
+ case ADD_TIMING_MARKER:
+ return addTimingMarker(state, action);
+
+ case CLEAR_REQUESTS:
+ case CLEAR_TIMING_MARKERS:
+ return clearTimingMarkers(state);
+
+ default:
+ return state;
+ }
+}
+
+module.exports = {
+ TimingMarkers,
+ timingMarkers,
+};
diff --git a/devtools/client/netmonitor/src/reducers/ui.js b/devtools/client/netmonitor/src/reducers/ui.js
new file mode 100644
index 0000000000..d1b405f033
--- /dev/null
+++ b/devtools/client/netmonitor/src/reducers/ui.js
@@ -0,0 +1,260 @@
+/* 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 {
+ CLEAR_REQUESTS,
+ OPEN_NETWORK_DETAILS,
+ OPEN_ACTION_BAR,
+ RESIZE_NETWORK_DETAILS,
+ ENABLE_PERSISTENT_LOGS,
+ DISABLE_BROWSER_CACHE,
+ OPEN_STATISTICS,
+ REMOVE_SELECTED_CUSTOM_REQUEST,
+ RESET_COLUMNS,
+ RESPONSE_HEADERS,
+ SELECT_DETAILS_PANEL_TAB,
+ SELECT_ACTION_BAR_TAB,
+ SEND_CUSTOM_REQUEST,
+ SELECT_REQUEST,
+ TOGGLE_COLUMN,
+ WATERFALL_RESIZE,
+ PANELS,
+ MIN_COLUMN_WIDTH,
+ SET_COLUMNS_WIDTH,
+ SET_HEADERS_URL_PREVIEW_EXPANDED,
+} = require("resource://devtools/client/netmonitor/src/constants.js");
+
+const cols = {
+ status: true,
+ method: true,
+ domain: true,
+ file: true,
+ url: false,
+ protocol: false,
+ scheme: false,
+ remoteip: false,
+ initiator: true,
+ type: true,
+ cookies: false,
+ setCookies: false,
+ transferred: true,
+ contentSize: true,
+ priority: false,
+ startTime: false,
+ endTime: false,
+ responseTime: false,
+ duration: false,
+ latency: false,
+ waterfall: true,
+};
+
+function Columns() {
+ return Object.assign(
+ cols,
+ RESPONSE_HEADERS.reduce(
+ (acc, header) => Object.assign(acc, { [header]: false }),
+ {}
+ )
+ );
+}
+
+function ColumnsData() {
+ const defaultColumnsData = JSON.parse(
+ Services.prefs
+ .getDefaultBranch(null)
+ .getCharPref("devtools.netmonitor.columnsData")
+ );
+ return new Map(defaultColumnsData.map(i => [i.name, i]));
+}
+
+function UI(initialState = {}) {
+ return {
+ columns: Columns(),
+ columnsData: ColumnsData(),
+ detailsPanelSelectedTab: PANELS.HEADERS,
+ networkDetailsOpen: false,
+ networkDetailsWidth: null,
+ networkDetailsHeight: null,
+ persistentLogsEnabled: Services.prefs.getBoolPref(
+ "devtools.netmonitor.persistlog"
+ ),
+ browserCacheDisabled: Services.prefs.getBoolPref("devtools.cache.disabled"),
+ slowLimit: Services.prefs.getIntPref("devtools.netmonitor.audits.slow"),
+ statisticsOpen: false,
+ waterfallWidth: null,
+ networkActionOpen: false,
+ selectedActionBarTabId: null,
+ shouldExpandHeadersUrlPreview: false,
+ ...initialState,
+ };
+}
+
+function resetColumns(state) {
+ return {
+ ...state,
+ columns: Columns(),
+ columnsData: ColumnsData(),
+ };
+}
+
+function resizeWaterfall(state, action) {
+ return {
+ ...state,
+ waterfallWidth: action.width,
+ };
+}
+
+function openNetworkDetails(state, action) {
+ return {
+ ...state,
+ networkDetailsOpen: action.open,
+ };
+}
+
+function openNetworkAction(state, action) {
+ return {
+ ...state,
+ networkActionOpen: action.open,
+ };
+}
+
+function resizeNetworkDetails(state, action) {
+ return {
+ ...state,
+ networkDetailsWidth: action.width,
+ networkDetailsHeight: action.height,
+ };
+}
+
+function enablePersistentLogs(state, action) {
+ return {
+ ...state,
+ persistentLogsEnabled: action.enabled,
+ };
+}
+
+function disableBrowserCache(state, action) {
+ return {
+ ...state,
+ browserCacheDisabled: action.disabled,
+ };
+}
+
+function openStatistics(state, action) {
+ return {
+ ...state,
+ statisticsOpen: action.open,
+ };
+}
+
+function setDetailsPanelTab(state, action) {
+ return {
+ ...state,
+ detailsPanelSelectedTab: action.id,
+ };
+}
+
+function setActionBarTab(state, action) {
+ return {
+ ...state,
+ selectedActionBarTabId: action.id,
+ };
+}
+
+function setHeadersUrlPreviewExpanded(state, action) {
+ return {
+ ...state,
+ shouldExpandHeadersUrlPreview: action.expanded,
+ };
+}
+
+function toggleColumn(state, action) {
+ const { column } = action;
+
+ if (!state.columns.hasOwnProperty(column)) {
+ return state;
+ }
+
+ return {
+ ...state,
+ columns: {
+ ...state.columns,
+ [column]: !state.columns[column],
+ },
+ };
+}
+
+function setColumnsWidth(state, action) {
+ const { widths } = action;
+ const columnsData = new Map(state.columnsData);
+
+ widths.forEach(col => {
+ let data = columnsData.get(col.name);
+ if (!data) {
+ data = {
+ name: col.name,
+ minWidth: MIN_COLUMN_WIDTH,
+ };
+ }
+ columnsData.set(col.name, {
+ ...data,
+ width: col.width,
+ });
+ });
+
+ return {
+ ...state,
+ columnsData,
+ };
+}
+
+function ui(state = UI(), action) {
+ switch (action.type) {
+ case CLEAR_REQUESTS:
+ return openNetworkDetails(state, { open: false });
+ case OPEN_NETWORK_DETAILS:
+ return openNetworkDetails(state, action);
+ case RESIZE_NETWORK_DETAILS:
+ return resizeNetworkDetails(state, action);
+ case ENABLE_PERSISTENT_LOGS:
+ return enablePersistentLogs(state, action);
+ case DISABLE_BROWSER_CACHE:
+ return disableBrowserCache(state, action);
+ case OPEN_STATISTICS:
+ return openStatistics(state, action);
+ case RESET_COLUMNS:
+ return resetColumns(state);
+ case REMOVE_SELECTED_CUSTOM_REQUEST:
+ return openNetworkDetails(state, { open: true });
+ case SEND_CUSTOM_REQUEST:
+ return openNetworkDetails(state, { open: false });
+ case SELECT_DETAILS_PANEL_TAB:
+ return setDetailsPanelTab(state, action);
+ case SELECT_ACTION_BAR_TAB:
+ return setActionBarTab(state, action);
+ case SELECT_REQUEST:
+ return openNetworkDetails(state, { open: true });
+ case TOGGLE_COLUMN:
+ return toggleColumn(state, action);
+ case WATERFALL_RESIZE:
+ return resizeWaterfall(state, action);
+ case SET_COLUMNS_WIDTH:
+ return setColumnsWidth(state, action);
+ case OPEN_ACTION_BAR:
+ return openNetworkAction(state, action);
+ case SET_HEADERS_URL_PREVIEW_EXPANDED:
+ return setHeadersUrlPreviewExpanded(state, action);
+ default:
+ return state;
+ }
+}
+
+module.exports = {
+ Columns,
+ ColumnsData,
+ UI,
+ ui,
+};