зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1559347 - Implement generic search across all resources. r=Honza
Differential Revision: https://phabricator.services.mozilla.com/D36323 --HG-- extra : moz-landing-system : lando
This commit is contained in:
Родитель
b05ce1a78b
Коммит
10edd3b03b
|
@ -20,6 +20,7 @@ NetMonitorPanel.prototype = {
|
|||
await app.bootstrap({
|
||||
toolbox: this.toolbox,
|
||||
document: this.panelWin.document,
|
||||
win: this.panelWin,
|
||||
});
|
||||
|
||||
// Ready to go!
|
||||
|
|
|
@ -12,12 +12,14 @@ const sort = require("./sort");
|
|||
const timingMarkers = require("./timing-markers");
|
||||
const ui = require("./ui");
|
||||
const webSockets = require("./web-sockets");
|
||||
const search = require("./search");
|
||||
|
||||
Object.assign(
|
||||
exports,
|
||||
batching,
|
||||
filters,
|
||||
requests,
|
||||
search,
|
||||
selection,
|
||||
sort,
|
||||
timingMarkers,
|
||||
|
|
|
@ -7,6 +7,7 @@ DevToolsModules(
|
|||
'filters.js',
|
||||
'index.js',
|
||||
'requests.js',
|
||||
'search.js',
|
||||
'selection.js',
|
||||
'sort.js',
|
||||
'timing-markers.js',
|
||||
|
|
|
@ -0,0 +1,220 @@
|
|||
/* 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_SEARCH,
|
||||
CLOSE_SEARCH,
|
||||
UPDATE_SEARCH_STATUS,
|
||||
SEARCH_STATUS,
|
||||
} = require("../constants");
|
||||
|
||||
const {
|
||||
getDisplayedRequests,
|
||||
getOngoingSearch,
|
||||
getSearchStatus,
|
||||
getRequestById,
|
||||
} = require("../selectors/index");
|
||||
|
||||
const { fetchNetworkUpdatePacket } = require("../utils/request-utils");
|
||||
|
||||
const { searchInResource } = require("../workers/search/index");
|
||||
|
||||
/**
|
||||
* Search through all resources. This is the main action exported
|
||||
* from this module and consumed by Network panel UI.
|
||||
*/
|
||||
function search(connector, query) {
|
||||
let cancelled = 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 (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch all data for the resource.
|
||||
await loadResource(connector, request);
|
||||
|
||||
// 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 = () => {
|
||||
cancelled = true;
|
||||
};
|
||||
|
||||
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) => {
|
||||
// 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);
|
||||
|
||||
if (!result.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(addSearchResult(resource, result, query));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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: CLOSE_SEARCH });
|
||||
};
|
||||
}
|
||||
|
||||
function openSearch() {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: OPEN_SEARCH });
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle visibility of search panel in network panel
|
||||
*/
|
||||
function toggleSearchPanel() {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
state.search.panelOpen
|
||||
? dispatch({ type: CLOSE_SEARCH })
|
||||
: dispatch({ type: OPEN_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.CANCELLED));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
search,
|
||||
closeSearch,
|
||||
openSearch,
|
||||
clearSearchResults,
|
||||
addSearchQuery,
|
||||
toggleSearchPanel,
|
||||
};
|
|
@ -17,6 +17,10 @@ const { EVENTS } = require("./constants");
|
|||
|
||||
const { getDisplayedRequestById } = require("./selectors/index");
|
||||
|
||||
const SearchWorker = require("./workers/search/index");
|
||||
const SEARCH_WORKER_URL =
|
||||
"resource://devtools/client/netmonitor/src/workers/search/worker.js";
|
||||
|
||||
/**
|
||||
* Global App object for Network panel. This object depends
|
||||
* on the UI and can't be created independently.
|
||||
|
@ -31,7 +35,7 @@ function NetMonitorApp(api) {
|
|||
}
|
||||
|
||||
NetMonitorApp.prototype = {
|
||||
async bootstrap({ toolbox, document }) {
|
||||
async bootstrap({ toolbox, document, win }) {
|
||||
// Get the root element for mounting.
|
||||
this.mount = document.querySelector("#mount");
|
||||
|
||||
|
@ -50,6 +54,9 @@ NetMonitorApp.prototype = {
|
|||
});
|
||||
};
|
||||
|
||||
// Bootstrap search worker
|
||||
SearchWorker.start(SEARCH_WORKER_URL, win);
|
||||
|
||||
const { actions, connector, store } = this.api;
|
||||
|
||||
const sourceMapService = toolbox.sourceMapURLService;
|
||||
|
@ -71,6 +78,8 @@ NetMonitorApp.prototype = {
|
|||
destroy() {
|
||||
unmountComponentAtNode(this.mount);
|
||||
|
||||
SearchWorker.stop();
|
||||
|
||||
// Make sure to destroy the API object. It's usually destroyed
|
||||
// in the Toolbox destroy method, but we need it here for case
|
||||
// where the Network panel is initialized without the toolbox
|
||||
|
|
|
@ -88,6 +88,8 @@ class RequestListContent extends Component {
|
|||
selectedRequest: PropTypes.object,
|
||||
unblockSelectedRequestURL: PropTypes.func.isRequired,
|
||||
requestFilterTypes: PropTypes.object.isRequired,
|
||||
panelOpen: PropTypes.bool,
|
||||
toggleSearchPanel: PropTypes.func.isRequired,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -310,6 +312,8 @@ class RequestListContent extends Component {
|
|||
sendCustomRequest,
|
||||
openStatistics,
|
||||
unblockSelectedRequestURL,
|
||||
toggleSearchPanel,
|
||||
panelOpen,
|
||||
} = this.props;
|
||||
this.contextMenu = new RequestListContextMenu({
|
||||
blockSelectedRequestURL,
|
||||
|
@ -320,6 +324,8 @@ class RequestListContent extends Component {
|
|||
openStatistics,
|
||||
openRequestInTab: this.openRequestInTab,
|
||||
unblockSelectedRequestURL,
|
||||
toggleSearchPanel,
|
||||
panelOpen,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -410,6 +416,7 @@ module.exports = connect(
|
|||
selectedRequest: getSelectedRequest(state),
|
||||
scale: getWaterfallScale(state),
|
||||
requestFilterTypes: state.filters.requestFilterTypes,
|
||||
panelOpen: state.search.panelOpen,
|
||||
}),
|
||||
(dispatch, props) => ({
|
||||
blockSelectedRequestURL: clickedRequest => {
|
||||
|
@ -456,5 +463,6 @@ module.exports = connect(
|
|||
onWaterfallMouseDown: () => {
|
||||
dispatch(Actions.selectDetailsPanelTab("timings"));
|
||||
},
|
||||
toggleSearchPanel: () => dispatch(Actions.toggleSearchPanel()),
|
||||
})
|
||||
)(RequestListContent);
|
||||
|
|
|
@ -42,6 +42,24 @@ const actionTypes = {
|
|||
WS_SET_REQUEST_FILTER_TEXT: "WS_SET_REQUEST_FILTER_TEXT",
|
||||
WS_TOGGLE_COLUMN: "WS_TOGGLE_COLUMN",
|
||||
WS_RESET_COLUMNS: "WS_RESET_COLUMNS",
|
||||
|
||||
// Search
|
||||
ADD_SEARCH_QUERY: "ADD_SEARCH_QUERY",
|
||||
ADD_SEARCH_RESULT: "ADD_SEARCH_RESULT",
|
||||
CLEAR_SEARCH_RESULTS: "CLEAR_SEARCH_RESULTS",
|
||||
ADD_ONGOING_SEARCH: "ADD_ONGOING_SEARCH",
|
||||
OPEN_SEARCH: "OPEN_SEARCH",
|
||||
CLOSE_SEARCH: "CLOSE_SEARCH",
|
||||
UPDATE_SEARCH_STATUS: "UPDATE_SEARCH_STATUS",
|
||||
};
|
||||
|
||||
// Search status types
|
||||
const SEARCH_STATUS = {
|
||||
INITIAL: "INITIAL",
|
||||
FETCHING: "FETCHING",
|
||||
CANCELLED: "CANCELLED",
|
||||
DONE: "DONE",
|
||||
ERROR: "ERROR",
|
||||
};
|
||||
|
||||
// Descriptions for what this frontend is currently doing.
|
||||
|
@ -488,6 +506,7 @@ const general = {
|
|||
DEFAULT_COLUMN_WIDTH,
|
||||
SUPPORTED_HTTP_CODES,
|
||||
BLOCKED_REASON_MESSAGES,
|
||||
SEARCH_STATUS,
|
||||
};
|
||||
|
||||
// flatten constants
|
||||
|
|
|
@ -31,6 +31,7 @@ const {
|
|||
WebSockets,
|
||||
getWebSocketsDefaultColumnsState,
|
||||
} = require("./reducers/web-sockets");
|
||||
const { Search } = require("./reducers/search");
|
||||
|
||||
/**
|
||||
* Configure state and middleware for the Network monitor tool.
|
||||
|
@ -51,6 +52,7 @@ function configureStore(connector, telemetry) {
|
|||
webSockets: WebSockets({
|
||||
columns: getWebSocketsColumnState(),
|
||||
}),
|
||||
search: new Search(),
|
||||
};
|
||||
|
||||
// Prepare middleware.
|
||||
|
|
|
@ -12,6 +12,7 @@ DIRS += [
|
|||
'selectors',
|
||||
'utils',
|
||||
'widgets',
|
||||
'workers',
|
||||
]
|
||||
|
||||
DevToolsModules(
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
const { combineReducers } = require("devtools/client/shared/vendor/redux");
|
||||
const batchingReducer = require("./batching");
|
||||
const { requestsReducer } = require("./requests");
|
||||
const { search } = require("./search");
|
||||
const { sortReducer } = require("./sort");
|
||||
const { filters } = require("./filters");
|
||||
const { timingMarkers } = require("./timing-markers");
|
||||
|
@ -17,6 +18,7 @@ const networkThrottling = require("devtools/client/shared/components/throttling/
|
|||
module.exports = batchingReducer(
|
||||
combineReducers({
|
||||
requests: requestsReducer,
|
||||
search,
|
||||
sort: sortReducer,
|
||||
webSockets,
|
||||
filters,
|
||||
|
|
|
@ -7,6 +7,7 @@ DevToolsModules(
|
|||
'filters.js',
|
||||
'index.js',
|
||||
'requests.js',
|
||||
'search.js',
|
||||
'sort.js',
|
||||
'timing-markers.js',
|
||||
'ui.js',
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
/* 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_SEARCH,
|
||||
CLOSE_SEARCH,
|
||||
SEARCH_STATUS,
|
||||
UPDATE_SEARCH_STATUS,
|
||||
} = require("../constants");
|
||||
|
||||
/**
|
||||
* 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,
|
||||
panelOpen: false,
|
||||
},
|
||||
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 CLOSE_SEARCH:
|
||||
return onCloseSearch(state);
|
||||
case OPEN_SEARCH:
|
||||
return onOpenSearch(state);
|
||||
case UPDATE_SEARCH_STATUS:
|
||||
return onUpdateSearchStatus(state, action);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
function onAddSearchQuery(state, action) {
|
||||
return {
|
||||
...state,
|
||||
query: action.query,
|
||||
};
|
||||
}
|
||||
|
||||
function onAddSearchResult(state, action) {
|
||||
const results = state.results.slice();
|
||||
results.push({
|
||||
resource: action.resource,
|
||||
results: action.result,
|
||||
});
|
||||
|
||||
return {
|
||||
...state,
|
||||
results,
|
||||
};
|
||||
}
|
||||
|
||||
function onClearSearchResults(state) {
|
||||
return {
|
||||
...state,
|
||||
results: [],
|
||||
};
|
||||
}
|
||||
|
||||
function onAddOngoingSearch(state, action) {
|
||||
return {
|
||||
...state,
|
||||
ongoingSearch: action.ongoingSearch,
|
||||
};
|
||||
}
|
||||
|
||||
function onCloseSearch(state) {
|
||||
return {
|
||||
...state,
|
||||
panelOpen: false,
|
||||
};
|
||||
}
|
||||
|
||||
function onOpenSearch(state) {
|
||||
return {
|
||||
...state,
|
||||
panelOpen: true,
|
||||
};
|
||||
}
|
||||
|
||||
function onUpdateSearchStatus(state, action) {
|
||||
return {
|
||||
...state,
|
||||
status: action.status,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Search,
|
||||
search,
|
||||
};
|
|
@ -5,8 +5,9 @@
|
|||
"use strict";
|
||||
|
||||
const requests = require("./requests");
|
||||
const search = require("./search");
|
||||
const timingMarkers = require("./timing-markers");
|
||||
const ui = require("./ui");
|
||||
const webSockets = require("./web-sockets");
|
||||
|
||||
Object.assign(exports, requests, timingMarkers, ui, webSockets);
|
||||
Object.assign(exports, search, requests, timingMarkers, ui, webSockets);
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
DevToolsModules(
|
||||
'index.js',
|
||||
'requests.js',
|
||||
'search.js',
|
||||
'timing-markers.js',
|
||||
'ui.js',
|
||||
'web-sockets.js',
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
/* 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";
|
||||
|
||||
function getOngoingSearch(state) {
|
||||
return state.search.ongoingSearch;
|
||||
}
|
||||
|
||||
function getSearchStatus(state) {
|
||||
return state.search.status;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getOngoingSearch,
|
||||
getSearchStatus,
|
||||
};
|
|
@ -89,19 +89,22 @@ async function fetchHeaders(headers, getLongString) {
|
|||
* @param {array} updateTypes - a list of network event update types
|
||||
*/
|
||||
function fetchNetworkUpdatePacket(requestData, request, updateTypes) {
|
||||
const promises = [];
|
||||
updateTypes.forEach(updateType => {
|
||||
// Only stackTrace will be handled differently
|
||||
if (updateType === "stackTrace") {
|
||||
if (request.cause.stacktraceAvailable && !request.stacktrace) {
|
||||
requestData(request.id, updateType);
|
||||
promises.push(requestData(request.id, updateType));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (request[`${updateType}Available`] && !request[updateType]) {
|
||||
requestData(request.id, updateType);
|
||||
promises.push(requestData(request.id, updateType));
|
||||
}
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -340,12 +340,29 @@ class RequestListContextMenu {
|
|||
this.useAsFetch(id, url, method, requestHeaders, requestPostData),
|
||||
});
|
||||
|
||||
if (Services.prefs.getBoolPref("devtools.netmonitor.features.search")) {
|
||||
const { toggleSearchPanel, panelOpen } = this.props;
|
||||
|
||||
menu.push({
|
||||
type: "separator",
|
||||
});
|
||||
|
||||
menu.push({
|
||||
id: "request-list-context-search",
|
||||
label: "Search...", // TODO localization
|
||||
accesskey: "S", // TODO localization
|
||||
type: "checkbox",
|
||||
checked: panelOpen,
|
||||
visible: !!clickedRequest,
|
||||
click: () => toggleSearchPanel(),
|
||||
});
|
||||
}
|
||||
|
||||
showMenu(menu, {
|
||||
screenX: event.screenX,
|
||||
screenY: event.screenY,
|
||||
});
|
||||
}
|
||||
/* eslint-enable complexity */
|
||||
|
||||
/**
|
||||
* Opens selected item in the debugger
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
# vim: set filetype=python:
|
||||
# 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/.
|
||||
|
||||
DIRS += [
|
||||
'search',
|
||||
]
|
||||
|
||||
DevToolsModules(
|
||||
'worker-utils.js',
|
||||
)
|
|
@ -0,0 +1,21 @@
|
|||
/* 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 { WorkerDispatcher } = require("../worker-utils");
|
||||
|
||||
const dispatcher = new WorkerDispatcher();
|
||||
const start = dispatcher.start.bind(dispatcher);
|
||||
const stop = dispatcher.stop.bind(dispatcher);
|
||||
|
||||
// The search worker support just one task at this point,
|
||||
// which is searching through specified resource.
|
||||
const searchInResource = dispatcher.task("searchInResource");
|
||||
|
||||
module.exports = {
|
||||
start,
|
||||
stop,
|
||||
searchInResource,
|
||||
};
|
|
@ -0,0 +1,10 @@
|
|||
# vim: set filetype=python:
|
||||
# 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(
|
||||
'index.js',
|
||||
'search.js',
|
||||
'worker.js',
|
||||
)
|
|
@ -0,0 +1,306 @@
|
|||
/* 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/. */
|
||||
/* eslint-disable no-unused-vars */
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Search within specified resource. Note that this function runs
|
||||
* within a worker thread.
|
||||
*/
|
||||
function searchInResource(resource, query) {
|
||||
const results = [];
|
||||
|
||||
if (resource.url) {
|
||||
results.push(
|
||||
findMatches(resource, query, {
|
||||
key: "url",
|
||||
label: "Url",
|
||||
type: "url",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (resource.responseHeaders) {
|
||||
results.push(
|
||||
findMatches(resource, query, {
|
||||
key: "responseHeaders.headers",
|
||||
label: "Response Headers",
|
||||
type: "responseHeaders",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (resource.requestHeaders) {
|
||||
results.push(
|
||||
findMatches(resource, query, {
|
||||
key: "requestHeaders.headers",
|
||||
label: "Request Headers",
|
||||
type: "requestHeaders",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (resource.responseCookies) {
|
||||
let key = "responseCookies";
|
||||
|
||||
if (resource.responseCookies.cookies) {
|
||||
key = "responseCookies.cookies";
|
||||
}
|
||||
|
||||
results.push(
|
||||
findMatches(resource, query, {
|
||||
key,
|
||||
label: "Response Cookies",
|
||||
type: "responseCookies",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (resource.requestCookies) {
|
||||
let key = "requestCookies";
|
||||
|
||||
if (resource.requestCookies.cookies) {
|
||||
key = "requestCookies.cookies";
|
||||
}
|
||||
|
||||
results.push(
|
||||
findMatches(resource, query, {
|
||||
key,
|
||||
label: "Request Cookies",
|
||||
type: "requestCookies",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (resource.securityInfo) {
|
||||
results.push(
|
||||
findMatches(resource, query, {
|
||||
key: "securityInfo",
|
||||
label: "Security Information",
|
||||
type: "securityInfo",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (resource.responseContent) {
|
||||
results.push(
|
||||
findMatches(resource, query, {
|
||||
key: "responseContent.content.text",
|
||||
label: "Response Content",
|
||||
type: "responseContent",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (resource.requestPostData) {
|
||||
results.push(
|
||||
findMatches(resource, query, {
|
||||
key: "requestPostData.postData.text",
|
||||
label: "Request Post Data",
|
||||
type: "requestPostData",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return getResults(results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Concatenates all results
|
||||
* @param results
|
||||
* @returns {*[]}
|
||||
*/
|
||||
function getResults(results) {
|
||||
const tempResults = [].concat.apply([], results);
|
||||
|
||||
// Generate unique result keys
|
||||
tempResults.forEach((result, index) => {
|
||||
result.key = index;
|
||||
});
|
||||
|
||||
return tempResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find query matches in arrays, objects and strings.
|
||||
* @param resource
|
||||
* @param query
|
||||
* @param data
|
||||
* @returns {*}
|
||||
*/
|
||||
function findMatches(resource, query, data) {
|
||||
if (!resource || !query || !data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const resourceValue = getValue(data.key, resource);
|
||||
const resourceType = getType(resourceValue);
|
||||
|
||||
switch (resourceType) {
|
||||
case "string":
|
||||
return searchInText(query, resourceValue, data);
|
||||
case "array":
|
||||
return searchInArray(query, resourceValue, data);
|
||||
case "object":
|
||||
return searchInObject(query, resourceValue, data);
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get type of resource - deals with arrays as well.
|
||||
* @param resource
|
||||
* @returns {*}
|
||||
*/
|
||||
function getType(resource) {
|
||||
return Array.isArray(resource) ? "array" : typeof resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function returns the value of a key, included nested keys.
|
||||
* @param path
|
||||
* @param obj
|
||||
* @returns {*}
|
||||
*/
|
||||
function getValue(path, obj) {
|
||||
const properties = Array.isArray(path) ? path : path.split(".");
|
||||
return properties.reduce((prev, curr) => prev && prev[curr], obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search text for specific string and return all matches found
|
||||
* @param query
|
||||
* @param text
|
||||
* @param data
|
||||
* @returns {*}
|
||||
*/
|
||||
function searchInText(query, text, data) {
|
||||
const { type, label } = data;
|
||||
const lines = text.split(/\r\n|\r|\n/);
|
||||
const matches = [];
|
||||
|
||||
// iterate through each line
|
||||
lines.forEach((curr, i, arr) => {
|
||||
const regexQuery = RegExp(query, "gmi");
|
||||
let singleMatch;
|
||||
|
||||
while ((singleMatch = regexQuery.exec(lines[i])) !== null) {
|
||||
const startIndex = regexQuery.lastIndex;
|
||||
matches.push({
|
||||
type,
|
||||
label,
|
||||
line: i,
|
||||
value: getTruncatedValue(lines[i], query, startIndex),
|
||||
startIndex,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return matches.length === 0 ? [] : matches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for query in array.
|
||||
* Iterates through each array item and handles item based on type.
|
||||
* @param query
|
||||
* @param arr
|
||||
* @param data
|
||||
* @returns {*}
|
||||
*/
|
||||
function searchInArray(query, arr, data) {
|
||||
const { type, key, label } = data;
|
||||
|
||||
const matches = arr
|
||||
.filter(match => JSON.stringify(match).includes(query))
|
||||
.map((match, i) =>
|
||||
findMatches(match, query, {
|
||||
label,
|
||||
key: key + ".[" + i + "]",
|
||||
type,
|
||||
})
|
||||
);
|
||||
|
||||
return getResults(matches);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return query match and up to 100 characters on left and right.
|
||||
* (100) + [matched query] + (100)
|
||||
* @param value
|
||||
* @param query
|
||||
* @param startIndex
|
||||
* @returns {*}
|
||||
*/
|
||||
function getTruncatedValue(value, query, startIndex) {
|
||||
const valueSize = value.length;
|
||||
const indexEnd = startIndex + query.length;
|
||||
|
||||
if (valueSize < 200 + query.length) {
|
||||
return value;
|
||||
}
|
||||
|
||||
const start = value.substring(startIndex, startIndex - 100);
|
||||
const end = value.substring(indexEnd, indexEnd + 100);
|
||||
|
||||
return start + query + end;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates through object, including nested objects, returns all
|
||||
* found matches.
|
||||
* @param query
|
||||
* @param obj
|
||||
* @param data
|
||||
* @returns {*}
|
||||
*/
|
||||
function searchInObject(query, obj, data) {
|
||||
const { type, label } = data;
|
||||
const matches = data.hasOwnProperty("collector") ? data.collector : [];
|
||||
|
||||
for (const objectKey in obj) {
|
||||
if (obj.hasOwnProperty(objectKey)) {
|
||||
if (typeof obj[objectKey] === "object") {
|
||||
searchInObject(obj[objectKey], query, {
|
||||
collector: matches,
|
||||
type,
|
||||
label: objectKey,
|
||||
});
|
||||
}
|
||||
|
||||
let location;
|
||||
|
||||
if (objectKey) {
|
||||
if (objectKey.includes(query)) {
|
||||
location = "name";
|
||||
} else if (
|
||||
typeof obj[objectKey] === "string" &&
|
||||
obj[objectKey].includes(query)
|
||||
) {
|
||||
location = "value";
|
||||
}
|
||||
}
|
||||
|
||||
if (!location) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const match = {
|
||||
type,
|
||||
label: objectKey,
|
||||
};
|
||||
|
||||
const value = location === "name" ? objectKey : obj[objectKey];
|
||||
const startIndex = value.indexOf(query);
|
||||
|
||||
match.startIndex = startIndex;
|
||||
match.value = getTruncatedValue(value, query, startIndex);
|
||||
|
||||
matches.push(match);
|
||||
}
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
/* 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";
|
||||
|
||||
/* eslint-disable no-undef*/
|
||||
importScripts("./search.js", "../worker-utils.js");
|
||||
|
||||
// Implementation of search worker (runs in worker scope).
|
||||
self.onmessage = workerHandler({ searchInResource });
|
|
@ -0,0 +1,128 @@
|
|||
/* 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";
|
||||
|
||||
function WorkerDispatcher() {
|
||||
this.msgId = 1;
|
||||
this.worker = null;
|
||||
}
|
||||
|
||||
WorkerDispatcher.prototype = {
|
||||
start(url, win) {
|
||||
this.worker = new win.Worker(url);
|
||||
this.worker.onerror = () => {
|
||||
console.error(`Error in worker ${url}`);
|
||||
};
|
||||
},
|
||||
|
||||
stop() {
|
||||
if (!this.worker) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.worker.terminate();
|
||||
this.worker = null;
|
||||
},
|
||||
|
||||
task(method, { queue = false } = {}) {
|
||||
const calls = [];
|
||||
const push = args => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (queue && calls.length === 0) {
|
||||
Promise.resolve().then(flush);
|
||||
}
|
||||
|
||||
calls.push([args, resolve, reject]);
|
||||
|
||||
if (!queue) {
|
||||
flush();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const flush = () => {
|
||||
const items = calls.slice();
|
||||
calls.length = 0;
|
||||
|
||||
if (!this.worker) {
|
||||
return;
|
||||
}
|
||||
|
||||
const id = this.msgId++;
|
||||
this.worker.postMessage({
|
||||
id,
|
||||
method,
|
||||
calls: items.map(item => item[0]),
|
||||
});
|
||||
|
||||
const listener = ({ data: result }) => {
|
||||
if (result.id !== id) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.worker) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.worker.removeEventListener("message", listener);
|
||||
|
||||
result.results.forEach((resultData, i) => {
|
||||
const [, resolve, reject] = items[i];
|
||||
|
||||
if (resultData.error) {
|
||||
reject(resultData.error);
|
||||
} else {
|
||||
resolve(resultData.response);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
this.worker.addEventListener("message", listener);
|
||||
};
|
||||
|
||||
return (...args) => push(args);
|
||||
},
|
||||
|
||||
invoke(method, ...args) {
|
||||
return this.task(method)(...args);
|
||||
},
|
||||
};
|
||||
|
||||
function workerHandler(publicInterface) {
|
||||
return function(msg) {
|
||||
const { id, method, calls } = msg.data;
|
||||
|
||||
Promise.all(
|
||||
calls.map(args => {
|
||||
try {
|
||||
const response = publicInterface[method].apply(undefined, args);
|
||||
if (response instanceof Promise) {
|
||||
return response.then(
|
||||
val => ({ response: val }),
|
||||
// Error can't be sent via postMessage, so be sure to
|
||||
// convert to string.
|
||||
err => ({ error: err.toString() })
|
||||
);
|
||||
}
|
||||
return { response };
|
||||
} catch (error) {
|
||||
// Error can't be sent via postMessage, so be sure to convert to
|
||||
// string.
|
||||
return { error: error.toString() };
|
||||
}
|
||||
})
|
||||
).then(results => {
|
||||
self.postMessage({ id, results });
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// Might be loaded within a worker thread where `module` isn't available.
|
||||
if (typeof module !== "undefined") {
|
||||
module.exports = {
|
||||
WorkerDispatcher,
|
||||
workerHandler,
|
||||
};
|
||||
}
|
|
@ -163,6 +163,9 @@ pref("devtools.serviceWorkers.testing.enabled", false);
|
|||
// Enable the Network Monitor
|
||||
pref("devtools.netmonitor.enabled", true);
|
||||
|
||||
// Enable Network Search
|
||||
pref("devtools.netmonitor.features.search", false);
|
||||
|
||||
// Enable the Application panel
|
||||
pref("devtools.application.enabled", false);
|
||||
|
||||
|
|
|
@ -139,7 +139,7 @@ class SearchBox extends PureComponent {
|
|||
|
||||
onKeyDown(e) {
|
||||
if (this.props.onKeyDown) {
|
||||
this.props.onKeyDown();
|
||||
this.props.onKeyDown(e);
|
||||
}
|
||||
|
||||
const autocomplete = this.autocompleteRef.current;
|
||||
|
|
Загрузка…
Ссылка в новой задаче