summaryrefslogtreecommitdiffstats
path: root/devtools/client/netmonitor/src/components/search
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/netmonitor/src/components/search')
-rw-r--r--devtools/client/netmonitor/src/components/search/SearchPanel.js264
-rw-r--r--devtools/client/netmonitor/src/components/search/StatusBar.js97
-rw-r--r--devtools/client/netmonitor/src/components/search/Toolbar.js160
-rw-r--r--devtools/client/netmonitor/src/components/search/moz.build10
-rw-r--r--devtools/client/netmonitor/src/components/search/search-provider.js91
5 files changed, 622 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);
diff --git a/devtools/client/netmonitor/src/components/search/StatusBar.js b/devtools/client/netmonitor/src/components/search/StatusBar.js
new file mode 100644
index 0000000000..31ecdeffe9
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/search/StatusBar.js
@@ -0,0 +1,97 @@
+/* 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,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const {
+ connect,
+} = require("resource://devtools/client/shared/redux/visibility-handler-connect.js");
+const {
+ getSearchStatus,
+ getSearchResultCount,
+ getSearchResourceCount,
+} = require("resource://devtools/client/netmonitor/src/selectors/index.js");
+const { PluralForm } = require("resource://devtools/shared/plural-form.js");
+const {
+ L10N,
+} = require("resource://devtools/client/netmonitor/src/utils/l10n.js");
+const { div, span } = dom;
+const {
+ SEARCH_STATUS,
+} = require("resource://devtools/client/netmonitor/src/constants.js");
+
+/**
+ * Displays the number of lines found for results and resource count (files)
+ */
+class StatusBar extends Component {
+ static get propTypes() {
+ return {
+ status: PropTypes.string,
+ resultsCount: PropTypes.string,
+ resourceCount: PropTypes.string,
+ };
+ }
+
+ getSearchStatusDoneLabel(lines, files) {
+ const matchingLines = PluralForm.get(
+ lines,
+ L10N.getStr("netmonitor.search.status.labels.matchingLines")
+ ).replace("#1", lines);
+ const matchingFiles = PluralForm.get(
+ files,
+ L10N.getStr("netmonitor.search.status.labels.fileCount")
+ ).replace("#1", files);
+
+ return L10N.getFormatStr(
+ "netmonitor.search.status.labels.done",
+ matchingLines,
+ matchingFiles
+ );
+ }
+
+ renderStatus() {
+ const { status, resultsCount, resourceCount } = this.props;
+
+ switch (status) {
+ case SEARCH_STATUS.FETCHING:
+ return L10N.getStr("netmonitor.search.status.labels.fetching");
+ case SEARCH_STATUS.DONE:
+ return this.getSearchStatusDoneLabel(resultsCount, resourceCount);
+ case SEARCH_STATUS.ERROR:
+ return L10N.getStr("netmonitor.search.status.labels.error");
+ case SEARCH_STATUS.CANCELED:
+ return L10N.getStr("netmonitor.search.status.labels.canceled");
+ default:
+ return "";
+ }
+ }
+
+ render() {
+ const { status } = this.props;
+ return div(
+ { className: "devtools-toolbar devtools-toolbar-bottom" },
+ div(
+ {
+ className: "status-bar-label",
+ title: this.renderStatus(),
+ },
+ this.renderStatus(),
+ status === SEARCH_STATUS.FETCHING
+ ? span({ className: "img loader" })
+ : ""
+ )
+ );
+ }
+}
+
+module.exports = connect(state => ({
+ status: getSearchStatus(state),
+ resultsCount: getSearchResultCount(state),
+ resourceCount: getSearchResourceCount(state),
+}))(StatusBar);
diff --git a/devtools/client/netmonitor/src/components/search/Toolbar.js b/devtools/client/netmonitor/src/components/search/Toolbar.js
new file mode 100644
index 0000000000..0f39cfa17a
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/search/Toolbar.js
@@ -0,0 +1,160 @@
+/* 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("resource://devtools/client/shared/vendor/react.js");
+const {
+ connect,
+} = require("resource://devtools/client/shared/redux/visibility-handler-connect.js");
+const {
+ FILTER_SEARCH_DELAY,
+} = require("resource://devtools/client/netmonitor/src/constants.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const Actions = require("resource://devtools/client/netmonitor/src/actions/index.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+const {
+ L10N,
+} = require("resource://devtools/client/netmonitor/src/utils/l10n.js");
+const { button, span, div } = dom;
+
+// Components
+const SearchBox = createFactory(
+ require("resource://devtools/client/shared/components/SearchBox.js")
+);
+
+/**
+ * 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,
+ clearSearchResultAndCancel: PropTypes.func.isRequired,
+ caseSensitive: PropTypes.bool.isRequired,
+ toggleCaseSensitiveSearch: PropTypes.func.isRequired,
+ connector: PropTypes.object.isRequired,
+ query: PropTypes.string,
+ };
+ }
+
+ /**
+ * 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;
+ }
+ }
+
+ renderModifiers() {
+ return div(
+ { className: "search-modifiers" },
+ span({ className: "pipe-divider" }),
+ this.renderCaseSensitiveButton()
+ );
+ }
+
+ /**
+ * 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 the case sensitive search modifier button
+ */
+ renderCaseSensitiveButton() {
+ const { caseSensitive, toggleCaseSensitiveSearch } = this.props;
+ const active = caseSensitive ? "checked" : "";
+
+ return button({
+ id: "devtools-network-search-caseSensitive",
+ className: `devtools-button ${active}`,
+ title: L10N.getStr("netmonitor.search.toolbar.caseSensitive"),
+ onClick: toggleCaseSensitiveSearch,
+ });
+ }
+
+ /**
+ * Render Search box.
+ */
+ renderFilterBox() {
+ const { addSearchQuery, clearSearchResultAndCancel, connector, query } =
+ 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,
+ value: query,
+ onClearButtonClick: () => clearSearchResultAndCancel(),
+ onChange: newQuery => addSearchQuery(newQuery),
+ onKeyDown: event => this.onKeyDown(event, connector),
+ });
+ }
+
+ render() {
+ return div(
+ {
+ id: "netmonitor-toolbar-container",
+ className: "devtools-toolbar devtools-input-toolbar",
+ },
+ this.renderFilterBox(),
+ this.renderModifiers()
+ );
+ }
+}
+
+module.exports = connect(
+ state => ({
+ caseSensitive: state.search.caseSensitive,
+ query: state.search.query,
+ }),
+ dispatch => ({
+ closeSearch: () => dispatch(Actions.closeSearch()),
+ openSearch: () => dispatch(Actions.openSearch()),
+ clearSearchResultAndCancel: () =>
+ dispatch(Actions.clearSearchResultAndCancel()),
+ toggleCaseSensitiveSearch: () =>
+ dispatch(Actions.toggleCaseSensitiveSearch()),
+ search: (connector, query) => dispatch(Actions.search(connector, query)),
+ addSearchQuery: query => dispatch(Actions.addSearchQuery(query)),
+ })
+)(Toolbar);
diff --git a/devtools/client/netmonitor/src/components/search/moz.build b/devtools/client/netmonitor/src/components/search/moz.build
new file mode 100644
index 0000000000..7c48392d10
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/search/moz.build
@@ -0,0 +1,10 @@
+# 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",
+ "StatusBar.js",
+ "Toolbar.js",
+)
diff --git a/devtools/client/netmonitor/src/components/search/search-provider.js b/devtools/client/netmonitor/src/components/search/search-provider.js
new file mode 100644
index 0000000000..d076f35856
--- /dev/null
+++ b/devtools/client/netmonitor/src/components/search/search-provider.js
@@ -0,0 +1,91 @@
+/* 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 {
+ ObjectProvider,
+} = require("resource://devtools/client/shared/components/tree/ObjectProvider.js");
+const {
+ getFileName,
+} = require("resource://devtools/client/netmonitor/src/utils/request-utils.js");
+
+/**
+ * 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;
+ },
+
+ getLabel(object) {
+ if (object.resource) {
+ return this.getResourceLabel(object);
+ } else if (object.label) {
+ return object.label;
+ }
+ return ObjectProvider.getLabel(object);
+ },
+
+ getValue(object) {
+ if (object.resource) {
+ return "";
+ } 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);
+ },
+
+ getResourceTooltipLabel(object) {
+ const { resource } = object;
+ if (resource.urlDetails?.url) {
+ return resource.urlDetails.url;
+ }
+
+ return this.getResourceLabel(object);
+ },
+
+ getResourceLabel(object) {
+ return (
+ getFileName(object.resource.urlDetails.baseNameWithQuery) ||
+ object.resource.urlDetails.host
+ );
+ },
+};
+
+module.exports = {
+ SearchProvider,
+};