From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- .../client/netmonitor/src/reducers/batching.js | 27 ++ devtools/client/netmonitor/src/reducers/filters.js | 102 +++++++ devtools/client/netmonitor/src/reducers/index.js | 47 +++ .../client/netmonitor/src/reducers/messages.js | 335 +++++++++++++++++++++ devtools/client/netmonitor/src/reducers/moz.build | 16 + .../netmonitor/src/reducers/request-blocking.js | 187 ++++++++++++ .../client/netmonitor/src/reducers/requests.js | 313 +++++++++++++++++++ devtools/client/netmonitor/src/reducers/search.js | 118 ++++++++ devtools/client/netmonitor/src/reducers/sort.js | 48 +++ .../netmonitor/src/reducers/timing-markers.js | 78 +++++ devtools/client/netmonitor/src/reducers/ui.js | 260 ++++++++++++++++ 11 files changed, 1531 insertions(+) create mode 100644 devtools/client/netmonitor/src/reducers/batching.js create mode 100644 devtools/client/netmonitor/src/reducers/filters.js create mode 100644 devtools/client/netmonitor/src/reducers/index.js create mode 100644 devtools/client/netmonitor/src/reducers/messages.js create mode 100644 devtools/client/netmonitor/src/reducers/moz.build create mode 100644 devtools/client/netmonitor/src/reducers/request-blocking.js create mode 100644 devtools/client/netmonitor/src/reducers/requests.js create mode 100644 devtools/client/netmonitor/src/reducers/search.js create mode 100644 devtools/client/netmonitor/src/reducers/sort.js create mode 100644 devtools/client/netmonitor/src/reducers/timing-markers.js create mode 100644 devtools/client/netmonitor/src/reducers/ui.js (limited to 'devtools/client/netmonitor/src/reducers') 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, +}; -- cgit v1.2.3