Source code
Revision control
Copy as Markdown
Other Tools
/* 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
"use strict";
const {
Component,
createRef,
createFactory,
} = require("resource://devtools/client/shared/vendor/react.js");
const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
const { div, span } = dom;
const Actions = require("resource://devtools/client/netmonitor/src/actions/index.js");
const {
PANELS,
} = require("resource://devtools/client/netmonitor/src/constants.js");
const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
const {
connect,
} = require("resource://devtools/client/shared/redux/visibility-handler-connect.js");
const TreeViewClass = require("resource://devtools/client/shared/components/tree/TreeView.js");
const TreeView = createFactory(TreeViewClass);
const LabelCell = createFactory(
require("resource://devtools/client/shared/components/tree/LabelCell.js")
);
const {
SearchProvider,
} = require("resource://devtools/client/netmonitor/src/components/search/search-provider.js");
const Toolbar = createFactory(
require("resource://devtools/client/netmonitor/src/components/search/Toolbar.js")
);
const StatusBar = createFactory(
require("resource://devtools/client/netmonitor/src/components/search/StatusBar.js")
);
const {
limitTooltipLength,
} = require("resource://devtools/client/netmonitor/src/utils/tooltips.js");
// There are two levels in the search panel tree hierarchy:
// 0: Resource - represents the source request object
// 1: Search Result - represents a match coming from the parent resource
const RESOURCE_LEVEL = 0;
const SEARCH_RESULT_LEVEL = 1;
/**
* 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,
caseSensitive: PropTypes.bool,
connector: PropTypes.object.isRequired,
addSearchQuery: PropTypes.func.isRequired,
query: PropTypes.string.isRequired,
results: PropTypes.array,
navigate: PropTypes.func.isRequired,
isDisplaying: PropTypes.bool.isRequired,
};
}
constructor(props) {
super(props);
this.searchboxRef = createRef();
this.renderValue = this.renderValue.bind(this);
this.renderLabel = this.renderLabel.bind(this);
this.onClickTreeRow = this.onClickTreeRow.bind(this);
this.provider = SearchProvider;
}
componentDidMount() {
if (this.searchboxRef) {
this.searchboxRef.current.focus();
}
}
componentDidUpdate(prevProps) {
if (this.props.isDisplaying && !prevProps.isDisplaying) {
this.searchboxRef.current.focus();
}
}
onClickTreeRow(path, event, member) {
if (member.object.parentResource) {
this.props.navigate(member.object);
}
}
/**
* Custom TreeView label rendering. The search result
* value isn't rendered in separate column, but in the
* same column as the label (to save space).
*/
renderLabel(props) {
const { member } = props;
const level = member.level || 0;
const className = level == RESOURCE_LEVEL ? "resourceCell" : "resultCell";
// Customize label rendering by adding a suffix/value
const renderSuffix = () => {
return dom.span(
{
className,
},
" ",
this.renderValue(props)
);
};
return LabelCell({
...props,
title:
member.level == 1
? limitTooltipLength(member.object.value)
: this.provider.getResourceTooltipLabel(member.object),
renderSuffix,
});
}
renderTree() {
const { results } = this.props;
return TreeView({
object: results,
provider: this.provider,
expandableStrings: false,
renderLabelCell: this.renderLabel,
columns: [],
onClickRow: this.onClickTreeRow,
});
}
/**
* Custom tree value rendering. This method is responsible for
* rendering highlighted query string within the search result
* result tree.
*/
renderValue(props) {
const { member } = props;
const { query, caseSensitive } = this.props;
// 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 === SEARCH_RESULT_LEVEL) {
const { object } = member;
// Handles multiple matches in a string
if (object.startIndex && object.startIndex.length > 1) {
let indexStart = 0;
const allMatches = object.startIndex.map((match, index) => {
if (index === 0) {
indexStart = match - 50;
}
const highlightedMatch = [
span(
{ key: "match-" + match },
object.value.substring(indexStart, match - query.length)
),
span(
{
className: "query-match",
key: "match-" + match + "-highlight",
},
object.value.substring(match - query.length, match)
),
];
indexStart = match;
return highlightedMatch;
});
return span(
{
title: limitTooltipLength(object.value),
},
allMatches
);
}
const indexStart = caseSensitive
? object.value.indexOf(query)
: object.value.toLowerCase().indexOf(query.toLowerCase());
const indexEnd = indexStart + query.length;
// Handles a match in a string
if (indexStart >= 0) {
return span(
{ title: limitTooltipLength(object.value) },
span({}, object.value.substring(0, indexStart)),
span(
{ className: "query-match" },
object.value.substring(indexStart, indexStart + query.length)
),
span({}, object.value.substring(indexEnd, object.value.length))
);
}
// Default for key:value matches where query might not
// be present in the value, but found in the key.
return span(
{ title: limitTooltipLength(object.value) },
span({}, object.value)
);
}
return this.provider.getValue(member.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()
),
StatusBar()
);
}
}
module.exports = connect(
state => ({
query: state.search.query,
caseSensitive: state.search.caseSensitive,
results: state.search.results,
ongoingSearch: state.search.ongoingSearch,
isDisplaying: state.ui.selectedActionBarTabId === PANELS.SEARCH,
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)),
navigate: searchResult => dispatch(Actions.navigate(searchResult)),
})
)(SearchPanel);