Bug 1454312 - Display search results when searching across all network resources. r=Honza,nchevobbe

Differential Revision: https://phabricator.services.mozilla.com/D38164

--HG--
extra : moz-landing-system : lando
This commit is contained in:
lloan 2019-08-09 12:18:54 +00:00
Родитель 4939b826f9
Коммит 9abd5ad6da
10 изменённых файлов: 553 добавлений и 1 удалений

Просмотреть файл

@ -240,6 +240,7 @@ devtools.jar:
content/netmonitor/index.html (netmonitor/index.html)
content/netmonitor/src/assets/styles/StatusCode.css (netmonitor/src/assets/styles/StatusCode.css)
content/netmonitor/src/assets/styles/websockets.css (netmonitor/src/assets/styles/websockets.css)
content/netmonitor/src/assets/styles/search.css (netmonitor/src/assets/styles/search.css)
# Application panel
content/application/index.html (application/index.html)

Просмотреть файл

@ -739,6 +739,26 @@ netmonitor.ws.time.format=%1$S.%2$S
# in the messages panel identifying the raw data.
netmonitor.ws.rawData.header=Raw Data (%S)
# LOCALIZATION NOTE (netmonitor.search.toolbar.inputPlaceholder): This is the label
# displayed in the search toolbar for the search input as the placeholder.
netmonitor.search.toolbar.inputPlaceholder=Find in resources…
# LOCALIZATION NOTE (netmonitor.search.toolbar.close): This is the label
# displayed in the search toolbar to close the search panel.
netmonitor.search.toolbar.close=Close Search Panel
# LOCALIZATION NOTE (netmonitor.search.toolbar.clear): This is the label
# displayed in the search toolbar to clear the search panel.
netmonitor.search.toolbar.clear=Clear Search Results
# LOCALIZATION NOTE (netmonitor.search.labels.responseHeaders): This is the label
# displayed in the search results as the label for the response headers
netmonitor.search.labels.responseHeaders=Response Header
# LOCALIZATION NOTE (netmonitor.search.labels.requestHeaders): This is the label
# displayed in the search results as the label for the request headers
netmonitor.search.labels.requestHeaders=Request Header
# LOCALIZATION NOTE (netmonitor.tab.headers): This is the label displayed
# in the network details pane identifying the headers tab.
netmonitor.tab.headers=Headers

Просмотреть файл

@ -23,6 +23,7 @@
@import "chrome://devtools/content/netmonitor/src/assets/styles/CustomRequestPanel.css";
@import "chrome://devtools/content/netmonitor/src/assets/styles/StatusCode.css";
@import "chrome://devtools/content/netmonitor/src/assets/styles/websockets.css";
@import "chrome://devtools/content/netmonitor/src/assets/styles/search.css";
/* General */

Просмотреть файл

@ -0,0 +1,68 @@
/* 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/. */
.network-monitor .monitor-panel .request-list-container .requests-list-scroll {
width: 100% !important;
}
.search-panel {
display: flex;
flex-direction: column;
overflow: hidden;
}
.search-panel-content {
width: 100%;
overflow: auto;
}
/* Custom tree styles for the Search results panel*/
.search-panel .treeTable .treeLabelCell::after {
content: "";
}
/* Color for resource label */
.search-panel .resourceCell {
color: var(--theme-text-color-inactive);
}
/* Color for search result label */
.search-panel .resultCell {
color: var(--table-text-color);
text-overflow: ellipsis;
}
/* Color for search result label */
.search-panel .treeLabel.resultLabel {
color: var(--theme-text-color-inactive);
}
/* Break the column layout and make the search result output more compact */
.search-panel .treeTable tr {
display: block;
}
.search-panel .treeTable {
width: 100%;
}
#devtools-network-search-close::before {
background-image: url("chrome://devtools/skin/images/close.svg");
}
#devtools-network-search-close > button {
margin: 0 !important;
border-radius: 0 !important;
position: relative;
min-width: 26px;
}
/* Color for query matches */
.search-panel .resultCell .query-match {
background-color: var(--theme-selection-background);
color: white;
padding: 1px 4px;
margin: 0 2px 0 2px;
border-radius: 2px;
}

Просмотреть файл

@ -34,6 +34,10 @@ loader.lazyGetter(this, "NetworkDetailsPanel", function() {
return createFactory(require("./NetworkDetailsPanel"));
});
loader.lazyGetter(this, "SearchPanel", function() {
return createFactory(require("./search/SearchPanel"));
});
// MediaQueryList object responsible for switching sidebar splitter
// between landscape and portrait mode (depending on browser window size).
const MediaQueryVert = window.matchMedia("(min-width: 700px)");
@ -62,6 +66,7 @@ class MonitorPanel extends Component {
sourceMapService: PropTypes.object,
openLink: PropTypes.func,
updateRequest: PropTypes.func.isRequired,
panelOpen: PropTypes.bool.isRequired,
};
}
@ -80,6 +85,8 @@ class MonitorPanel extends Component {
componentDidMount() {
MediaQuerySingleRow.addListener(this.onLayoutChange);
MediaQueryVert.addListener(this.onLayoutChange);
this.persistDetailsPanelSize();
this.persistSearchPanelSize();
}
componentWillReceiveProps(nextProps) {
@ -96,7 +103,11 @@ class MonitorPanel extends Component {
componentWillUnmount() {
MediaQuerySingleRow.removeListener(this.onLayoutChange);
MediaQueryVert.removeListener(this.onLayoutChange);
this.persistDetailsPanelSize();
this.persistSearchPanelSize();
}
persistDetailsPanelSize() {
const { clientWidth, clientHeight } = findDOMNode(this.refs.endPanel) || {};
if (this.state.isVerticalSpliter && clientWidth) {
@ -113,6 +124,23 @@ class MonitorPanel extends Component {
}
}
persistSearchPanelSize() {
const { clientWidth, clientHeight } =
findDOMNode(this.refs.searchPanel) || {};
if (clientWidth) {
Services.prefs.setIntPref(
"devtools.netmonitor.panes-search-width",
clientWidth
);
}
if (clientHeight) {
Services.prefs.setIntPref(
"devtools.netmonitor.panes-search-height",
clientHeight
);
}
}
onLayoutChange() {
this.setState({
isSingleRow: MediaQuerySingleRow.matches,
@ -130,6 +158,32 @@ class MonitorPanel extends Component {
);
}
renderSearchPanel(connector) {
const { isEmpty } = this.props;
const initialWidth = Services.prefs.getIntPref(
"devtools.netmonitor.panes-search-width"
);
const initialHeight = Services.prefs.getIntPref(
"devtools.netmonitor.panes-search-height"
);
return SplitBox({
className: "devtools-responsive-container",
initialWidth,
initialHeight,
minSize: "50px",
maxSize: "80%",
splitterSize: 1,
startPanel: SearchPanel({
ref: "searchPanel",
connector,
}),
endPanel: RequestList({ isEmpty, connector }),
endPanelControl: false,
vert: true,
onControlledPanelResized: () => {},
});
}
render() {
const {
actions,
@ -139,15 +193,21 @@ class MonitorPanel extends Component {
openLink,
openSplitConsole,
sourceMapService,
panelOpen,
} = this.props;
const initialWidth = Services.prefs.getIntPref(
"devtools.netmonitor.panes-network-details-width"
);
const initialHeight = Services.prefs.getIntPref(
"devtools.netmonitor.panes-network-details-height"
);
const startPanel = panelOpen
? this.renderSearchPanel(connector)
: RequestList({ isEmpty, connector });
return div(
{ className: "monitor-panel" },
Toolbar({
@ -163,7 +223,7 @@ class MonitorPanel extends Component {
minSize: "50px",
maxSize: "80%",
splitterSize: networkDetailsOpen ? 1 : 0,
startPanel: RequestList({ isEmpty, connector }),
startPanel,
endPanel:
networkDetailsOpen &&
NetworkDetailsPanel({
@ -185,6 +245,7 @@ module.exports = connect(
state => ({
isEmpty: state.requests.requests.size == 0,
networkDetailsOpen: state.ui.networkDetailsOpen,
panelOpen: state.search.panelOpen,
request: getSelectedRequest(state),
selectedRequestVisible: isSelectedRequestVisible(state),
}),

Просмотреть файл

@ -3,6 +3,7 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
DIRS += [
'search',
'websockets',
]

Просмотреть файл

@ -0,0 +1,136 @@
/* 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 {
Component,
createRef,
createFactory,
} = require("devtools/client/shared/vendor/react");
const dom = require("devtools/client/shared/vendor/react-dom-factories");
const { div, span } = dom;
const Actions = require("devtools/client/netmonitor/src/actions/index");
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
const {
connect,
} = require("devtools/client/shared/redux/visibility-handler-connect");
const TreeViewClass = require("devtools/client/shared/components/tree/TreeView");
const TreeView = createFactory(TreeViewClass);
const { SearchProvider } = require("./search-provider");
const Toolbar = createFactory(require("./Toolbar"));
/**
* This component is responsible for rendering all search results
* coming from the current search.
*/
class SearchPanel extends Component {
static get propTypes() {
return {
clearSearchResults: PropTypes.func.isRequired,
openSearch: PropTypes.func.isRequired,
closeSearch: PropTypes.func.isRequired,
search: PropTypes.func.isRequired,
connector: PropTypes.object.isRequired,
addSearchQuery: PropTypes.func.isRequired,
query: PropTypes.string.isRequired,
results: PropTypes.array,
};
}
constructor(props) {
super(props);
this.searchboxRef = createRef();
this.renderValue = this.renderValue.bind(this);
}
renderTree() {
const { results } = this.props;
return TreeView({
object: results,
provider: SearchProvider,
expandableStrings: false,
renderValue: this.renderValue,
columns: [
{
id: "value",
width: "100%",
},
],
});
}
/**
* Custom tree value rendering. This method is responsible for
* rendering highlighted query string within the search result.
*/
renderValue(props) {
const member = props.member;
/**
* Handle only second level (zero based) that displays
* the search result. Find the query string inside the
* search result value (`props.object`) and render it
* within a span element with proper class name.
*
* level 0 = resource name
*/
if (member.level === 1) {
const { query } = this.props;
const indexStart = props.object.indexOf(query);
const indexEnd = indexStart + query.length;
return span(
{},
span({}, props.object.substring(0, indexStart)),
span({ className: "query-match" }, query),
span({}, props.object.substring(indexEnd, props.object.length))
);
}
return props.object;
}
render() {
const {
openSearch,
closeSearch,
clearSearchResults,
connector,
addSearchQuery,
search,
} = this.props;
return div(
{ className: "search-panel", style: { width: "100%" } },
Toolbar({
searchboxRef: this.searchboxRef,
openSearch,
closeSearch,
clearSearchResults,
addSearchQuery,
search,
connector,
}),
div(
{ className: "search-panel-content", style: { width: "100%" } },
this.renderTree()
)
);
}
}
module.exports = connect(
state => ({
query: state.search.query,
results: state.search.results,
ongoingSearch: state.search.ongoingSearch,
status: state.search.status,
}),
dispatch => ({
closeSearch: () => dispatch(Actions.closeSearch()),
openSearch: () => dispatch(Actions.openSearch()),
search: () => dispatch(Actions.search()),
clearSearchResults: () => dispatch(Actions.clearSearchResults()),
addSearchQuery: query => dispatch(Actions.addSearchQuery(query)),
})
)(SearchPanel);

Просмотреть файл

@ -0,0 +1,132 @@
/* 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 {
Component,
createFactory,
} = require("devtools/client/shared/vendor/react");
const {
connect,
} = require("devtools/client/shared/redux/visibility-handler-connect");
const { FILTER_SEARCH_DELAY } = require("../../constants");
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
const Actions = require("devtools/client/netmonitor/src/actions/index");
const dom = require("devtools/client/shared/vendor/react-dom-factories");
const { L10N } = require("devtools/client/netmonitor/src/utils/l10n.js");
const { button, span, div } = dom;
// Components
const SearchBox = createFactory(
require("devtools/client/shared/components/SearchBox")
);
/**
* Network Search toolbar component.
*
* Provides tools for greater control over search.
*/
class Toolbar extends Component {
static get propTypes() {
return {
searchboxRef: PropTypes.object.isRequired,
clearSearchResults: PropTypes.func.isRequired,
search: PropTypes.func.isRequired,
closeSearch: PropTypes.func.isRequired,
addSearchQuery: PropTypes.func.isRequired,
connector: PropTypes.object.isRequired,
};
}
/**
* Render a separator.
*/
renderSeparator() {
return span({ className: "devtools-separator" });
}
/**
* Handles what we do when key is pressed in search input.
* @param event
* @param conn
*/
onKeyDown(event, connector) {
switch (event.key) {
case "Escape":
event.preventDefault();
this.props.closeSearch();
break;
case "Enter":
event.preventDefault();
this.props.addSearchQuery(event.target.value);
this.props.search(connector, event.target.value);
break;
}
}
renderCloseButton() {
const { closeSearch } = this.props;
return button({
id: "devtools-network-search-close",
className: "devtools-button",
title: L10N.getStr("netmonitor.search.toolbar.close"),
onClick: () => closeSearch(),
});
}
/**
* Render a clear button to clear search results.
*/
renderClearButton() {
return button({
className:
"devtools-button devtools-clear-icon ws-frames-list-clear-button",
title: L10N.getStr("netmonitor.search.toolbar.clear"),
onClick: () => {
this.props.clearSearchResults();
},
});
}
/**
* Render filter Search box.
*/
renderFilterBox() {
const { addSearchQuery, connector } = this.props;
return SearchBox({
keyShortcut: "CmdOrCtrl+Shift+F",
placeholder: L10N.getStr("netmonitor.search.toolbar.inputPlaceholder"),
type: "search",
delay: FILTER_SEARCH_DELAY,
ref: this.props.searchboxRef,
onChange: query => addSearchQuery(query),
onKeyDown: event => this.onKeyDown(event, connector),
});
}
render() {
return div(
{
id: "netmonitor-toolbar-container",
className: "devtools-toolbar devtools-input-toolbar",
},
this.renderClearButton(),
this.renderSeparator(),
this.renderFilterBox(),
this.renderCloseButton()
);
}
}
module.exports = connect(
state => ({}),
dispatch => ({
closeSearch: () => dispatch(Actions.closeSearch()),
openSearch: () => dispatch(Actions.openSearch()),
clearSearchResults: () => dispatch(Actions.clearSearchResults()),
search: (connector, query) => dispatch(Actions.search(connector, query)),
addSearchQuery: query => dispatch(Actions.addSearchQuery(query)),
})
)(Toolbar);

Просмотреть файл

@ -0,0 +1,9 @@
# 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(
'search-provider.js',
'SearchPanel.js',
'Toolbar.js',
)

Просмотреть файл

@ -0,0 +1,123 @@
/* 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 { L10N } = require("devtools/client/netmonitor/src/utils/l10n");
const {
ObjectProvider,
} = require("devtools/client/shared/components/tree/ObjectProvider");
const {
getFileName,
} = require("devtools/client/netmonitor/src/utils/request-utils");
/**
* This provider is responsible for providing data from the
* search reducer to the SearchPanel.
*/
const SearchProvider = {
...ObjectProvider,
getChildren(object) {
if (Array.isArray(object)) {
return object;
} else if (object.resource) {
return object.results;
} else if (object.type) {
return [];
}
return ObjectProvider.getLabel(object);
},
hasChildren(object) {
return this.getChildren(object).length > 0;
},
getLabel(object) {
if (object.resource) {
return this.getResourceLabel(object);
} else if (object.type) {
switch (object.type) {
case "url":
return this.getUrlLabel(object);
case "responseContent":
return this.getResponseContent(object);
case "requestCookies":
return this.getRequestCookies();
case "responseCookies":
return this.getResponseCookies();
case "requestHeaders":
return this.getRequestHeaders();
case "responseHeaders":
return this.getResponseHeaders();
}
}
return ObjectProvider.getLabel(object);
},
getValue(object) {
if (object.resource) {
return object.resource.url;
} else if (object.type) {
return object.value;
}
return ObjectProvider.getValue(object);
},
getKey(object) {
if (object.resource) {
return object.resource.id;
} else if (object.type) {
return object.key;
}
return ObjectProvider.getKey(object);
},
getType(object) {
if (object.resource) {
return "resource";
} else if (object.type) {
return "result";
}
return ObjectProvider.getType(object);
},
getResourceLabel(object) {
const resourceLabel =
getFileName(object.resource.urlDetails.baseNameWithQuery) ||
object.resource.urlDetails.host;
if (resourceLabel.length > 30) {
return resourceLabel.substring(0, 30) + "…";
}
return resourceLabel;
},
getUrlLabel(object) {
return object.label.substring(0, 100);
},
getResponseContent(object) {
return object.line + "";
},
getRequestCookies() {
return "Set-Cookie";
},
getResponseCookies() {
return "Cookie";
},
getRequestHeaders() {
return L10N.getStr("netmonitor.search.labels.requestHeaders");
},
getResponseHeaders() {
return L10N.getStr("netmonitor.search.labels.responseHeaders");
},
};
module.exports = {
SearchProvider,
};