/* 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, OPEN_ACTION_BAR, UPDATE_SEARCH_STATUS, SEARCH_STATUS, SET_TARGET_SEARCH_RESULT, SELECT_ACTION_BAR_TAB, TOGGLE_SEARCH_CASE_SENSITIVE_SEARCH, PANELS, } = require("resource://devtools/client/netmonitor/src/constants.js"); const { getDisplayedRequests, getOngoingSearch, getSearchStatus, getRequestById, } = require("resource://devtools/client/netmonitor/src/selectors/index.js"); const { selectRequest, } = require("resource://devtools/client/netmonitor/src/actions/selection.js"); const { selectDetailsPanelTab, } = require("resource://devtools/client/netmonitor/src/actions/ui.js"); const { fetchNetworkUpdatePacket, } = require("resource://devtools/client/netmonitor/src/utils/request-utils.js"); const { searchInResource, } = require("resource://devtools/client/netmonitor/src/workers/search/index.js"); /** * Search through all resources. This is the main action exported * from this module and consumed by Network panel UI. */ function search(connector, query) { let canceled = false; // Instantiate an `ongoingSearch` function/object. It's responsible // for triggering set of asynchronous steps like fetching // data from the backend and performing search over it. // This `ongoingSearch` is stored in the Search reducer, so it can // be canceled if needed (e.g. when new search is executed). const newOngoingSearch = async ({ dispatch, getState }) => { const state = getState(); dispatch(stopOngoingSearch()); await dispatch(addOngoingSearch(newOngoingSearch)); await dispatch(clearSearchResults()); await dispatch(addSearchQuery(query)); dispatch(updateSearchStatus(SEARCH_STATUS.FETCHING)); // Loop over all displayed resources (in the sorted order), // fetch all the details data and run search worker that // search through the resource structure. const requests = getDisplayedRequests(state); for (const request of requests) { if (canceled) { return; } // Fetch all data for the resource. await loadResource(connector, request); if (canceled) { return; } // The state changed, so make sure to get fresh new reference // to the updated resource object. const updatedResource = getRequestById(getState(), request.id); await dispatch(searchResource(updatedResource, query)); } dispatch(updateSearchStatus(SEARCH_STATUS.DONE)); }; // Implement support for canceling (used e.g. when a new search // is executed or the user stops the searching manually). newOngoingSearch.cancel = () => { canceled = true; }; newOngoingSearch.isCanceled = () => { return canceled; }; return newOngoingSearch; } /** * Fetch all data related to the specified resource from the backend. */ async function loadResource(connector, resource) { const updateTypes = [ "responseHeaders", "requestHeaders", "responseCookies", "requestCookies", "requestPostData", "responseContent", "responseCache", "stackTrace", "securityInfo", ]; return fetchNetworkUpdatePacket(connector.requestData, resource, updateTypes); } /** * Search through all data within the specified resource. */ function searchResource(resource, query) { return async ({ dispatch, getState }) => { const state = getState(); const ongoingSearch = getOngoingSearch(state); const modifiers = { caseSensitive: state.search.caseSensitive, }; // Run search in a worker and wait for the results. The return // value is an array with search occurrences. const result = await searchInResource(resource, query, modifiers); if (!result.length || ongoingSearch.isCanceled()) { return; } dispatch(addSearchResult(resource, result)); }; } /** * Add search query to the reducer. */ function addSearchResult(resource, result) { return { type: ADD_SEARCH_RESULT, resource, result, }; } /** * Add search query to the reducer. */ function addSearchQuery(query) { return { type: ADD_SEARCH_QUERY, query, }; } /** * Clear all search results. */ function clearSearchResults() { return { type: CLEAR_SEARCH_RESULTS, }; } /** * Used to clear and cancel an ongoing search. * @returns {Function} */ function clearSearchResultAndCancel() { return ({ dispatch, getState }) => { dispatch(stopOngoingSearch()); dispatch(clearSearchResults()); }; } /** * Update status of the current search. */ function updateSearchStatus(status) { return { type: UPDATE_SEARCH_STATUS, status, }; } /** * Close the entire search panel. */ function closeSearch() { return ({ dispatch, getState }) => { dispatch(stopOngoingSearch()); dispatch({ type: OPEN_ACTION_BAR, open: false }); }; } /** * Open the entire search panel * @returns {Function} */ function openSearch() { return ({ dispatch, getState }) => { dispatch({ type: OPEN_ACTION_BAR, open: true }); dispatch({ type: SELECT_ACTION_BAR_TAB, id: PANELS.SEARCH, }); }; } /** * Toggles case sensitive search * @returns {Function} */ function toggleCaseSensitiveSearch() { return ({ dispatch, getState }) => { dispatch({ type: TOGGLE_SEARCH_CASE_SENSITIVE_SEARCH }); }; } /** * Toggle visibility of search panel in network panel */ function toggleSearchPanel() { return ({ dispatch, getState }) => { const state = getState(); state.ui.networkActionOpen && state.ui.selectedActionBarTabId === PANELS.SEARCH ? dispatch({ type: OPEN_ACTION_BAR, open: false }) : dispatch({ type: OPEN_ACTION_BAR, open: true }); dispatch({ type: SELECT_ACTION_BAR_TAB, id: PANELS.SEARCH, }); }; } /** * Append new search object into the reducer. The search object * is cancellable and so, it implements `cancel` method. */ function addOngoingSearch(ongoingSearch) { return { type: ADD_ONGOING_SEARCH, ongoingSearch, }; } /** * Cancel the current ongoing search. */ function stopOngoingSearch() { return ({ dispatch, getState }) => { const state = getState(); const ongoingSearch = getOngoingSearch(state); const status = getSearchStatus(state); if (ongoingSearch && status !== SEARCH_STATUS.DONE) { ongoingSearch.cancel(); dispatch(updateSearchStatus(SEARCH_STATUS.CANCELED)); } }; } /** * This action is fired when the user selects a search result * within the Search panel. It opens the details side bar and * selects the right side panel to show the context of the * clicked search result. */ function navigate(searchResult) { return ({ dispatch, getState }) => { // Store target search result in Search reducer. It's used // for search result navigation within the side panels. dispatch(setTargetSearchResult(searchResult)); // Preselect the right side panel. dispatch(selectDetailsPanelTab(searchResult.panel)); // Select related request in the UI (it also opens the // right side bar automatically). dispatch(selectRequest(searchResult.parentResource.id)); }; } function setTargetSearchResult(searchResult) { return { type: SET_TARGET_SEARCH_RESULT, searchResult, }; } module.exports = { search, closeSearch, openSearch, clearSearchResults, addSearchQuery, toggleSearchPanel, navigate, setTargetSearchResult, toggleCaseSensitiveSearch, clearSearchResultAndCancel, stopOngoingSearch, };