summaryrefslogtreecommitdiffstats
path: root/devtools/client/netmonitor/src/components/search/SearchPanel.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/netmonitor/src/components/search/SearchPanel.js')
-rw-r--r--devtools/client/netmonitor/src/components/search/SearchPanel.js264
1 files changed, 264 insertions, 0 deletions
diff --git a/devtools/client/netmonitor/src/components/search/SearchPanel.js b/devtools/client/netmonitor/src/components/search/SearchPanel.js
new file mode 100644
index 0000000000..11301a7d9c
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/search/SearchPanel.js
@@ -0,0 +1,264 @@
+/* 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("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);