diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /devtools/client/netmonitor/src/components/search | |
parent | Initial commit. (diff) | |
download | thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/netmonitor/src/components/search')
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, +}; |