SearchPanel.js (12906B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 const { 8 Component, 9 createRef, 10 createFactory, 11 } = require("resource://devtools/client/shared/vendor/react.mjs"); 12 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); 13 const { div, span } = dom; 14 const Actions = require("resource://devtools/client/netmonitor/src/actions/index.js"); 15 const { 16 PANELS, 17 } = require("resource://devtools/client/netmonitor/src/constants.js"); 18 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs"); 19 const { 20 connect, 21 } = require("resource://devtools/client/shared/vendor/react-redux.js"); 22 const TreeViewClass = ChromeUtils.importESModule( 23 "resource://devtools/client/shared/components/tree/TreeView.mjs" 24 ).default; 25 const TreeView = createFactory(TreeViewClass); 26 const LabelCell = createFactory( 27 ChromeUtils.importESModule( 28 "resource://devtools/client/shared/components/tree/LabelCell.mjs" 29 ).default 30 ); 31 const { 32 SearchProvider, 33 } = require("resource://devtools/client/netmonitor/src/components/search/search-provider.js"); 34 const Toolbar = createFactory( 35 require("resource://devtools/client/netmonitor/src/components/search/Toolbar.js") 36 ); 37 const StatusBar = createFactory( 38 require("resource://devtools/client/netmonitor/src/components/search/StatusBar.js") 39 ); 40 const { 41 limitTooltipLength, 42 } = require("resource://devtools/client/netmonitor/src/utils/tooltips.js"); 43 const { 44 L10N, 45 } = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); 46 loader.lazyRequireGetter( 47 this, 48 "showMenu", 49 "resource://devtools/client/shared/components/menu/utils.js", 50 true 51 ); 52 53 const PropertiesViewContextMenu = require("resource://devtools/client/netmonitor/src/widgets/PropertiesViewContextMenu.js"); 54 const RequestListContextMenu = require("resource://devtools/client/netmonitor/src/widgets/RequestListContextMenu.js"); 55 56 // There are two levels in the search panel tree hierarchy: 57 // 0: Resource - represents the source request object 58 // 1: Search Result - represents a match coming from the parent resource 59 const RESOURCE_LEVEL = 0; 60 const SEARCH_RESULT_LEVEL = 1; 61 62 /** 63 * This component is responsible for rendering all search results 64 * coming from the current search. 65 */ 66 class SearchPanel extends Component { 67 static get propTypes() { 68 return { 69 clearSearchResults: PropTypes.func.isRequired, 70 openSearch: PropTypes.func.isRequired, 71 closeSearch: PropTypes.func.isRequired, 72 search: PropTypes.func.isRequired, 73 caseSensitive: PropTypes.bool, 74 connector: PropTypes.object.isRequired, 75 addSearchQuery: PropTypes.func.isRequired, 76 query: PropTypes.string.isRequired, 77 results: PropTypes.array, 78 navigate: PropTypes.func.isRequired, 79 isDisplaying: PropTypes.bool.isRequired, 80 blockedUrls: PropTypes.array.isRequired, 81 requests: PropTypes.array.isRequired, 82 cloneRequest: PropTypes.func.isRequired, 83 openDetailsPanelTab: PropTypes.func.isRequired, 84 openHTTPCustomRequestTab: PropTypes.func.isRequired, 85 closeHTTPCustomRequestTab: PropTypes.func.isRequired, 86 sendCustomRequest: PropTypes.func.isRequired, 87 sendHTTPCustomRequest: PropTypes.func.isRequired, 88 openStatistics: PropTypes.func.isRequired, 89 openRequestBlockingAndAddUrl: PropTypes.func.isRequired, 90 openRequestBlockingAndDisableUrls: PropTypes.func.isRequired, 91 removeBlockedUrl: PropTypes.func.isRequired, 92 }; 93 } 94 95 constructor(props) { 96 super(props); 97 98 this.searchboxRef = createRef(); 99 this.renderValue = this.renderValue.bind(this); 100 this.renderLabel = this.renderLabel.bind(this); 101 this.onClickTreeRow = this.onClickTreeRow.bind(this); 102 this.onContextMenuTreeRow = this.onContextMenuTreeRow.bind(this); 103 this.provider = SearchProvider; 104 this.expandedNodes = new Set(); 105 } 106 107 componentDidMount() { 108 if (this.searchboxRef) { 109 this.searchboxRef.current.focus(); 110 } 111 } 112 113 componentDidUpdate(prevProps) { 114 if (this.props.isDisplaying && !prevProps.isDisplaying) { 115 this.searchboxRef.current.focus(); 116 } 117 } 118 119 onClickTreeRow(path, event, member) { 120 if (member.object.parentResource) { 121 this.props.navigate(member.object); 122 } 123 } 124 125 /** 126 * Custom TreeView label rendering. The search result 127 * value isn't rendered in separate column, but in the 128 * same column as the label (to save space). 129 */ 130 renderLabel(props) { 131 const { member } = props; 132 const level = member.level || 0; 133 const className = level == RESOURCE_LEVEL ? "resourceCell" : "resultCell"; 134 135 // Customize label rendering by adding a suffix/value 136 const renderSuffix = () => { 137 return dom.span( 138 { 139 className, 140 }, 141 " ", 142 this.renderValue(props) 143 ); 144 }; 145 146 return LabelCell({ 147 ...props, 148 title: 149 member.level == 1 150 ? limitTooltipLength(member.object.value) 151 : this.provider.getResourceTooltipLabel(member.object), 152 renderSuffix, 153 }); 154 } 155 156 onContextMenuTreeRow(member, evt) { 157 evt.preventDefault(); 158 159 // if selected item is associated to a request --> show suitable contextmenu 160 const request = member?.object?.resource; 161 if (typeof request === "object") { 162 // test if request is still available: 163 const requestId = request.id; 164 const storedRequest = this.props.requests.find( 165 currentStoredRequest => requestId === currentStoredRequest?.id 166 ); 167 168 if (typeof storedRequest === "object") { 169 // request is in cache --> open full context menu: 170 this.openRequestListContextMenu(evt, request); 171 } else { 172 // request is not in the cache anymore --> open context menu with note about it: 173 const menuItems = [ 174 { 175 id: "simple-view-context-menu-request-not-available-anymore", 176 label: L10N.getStr("netmonitor.context.hintRequestNotAvailable"), 177 disabled: true, 178 }, 179 ]; 180 181 showMenu(menuItems, { 182 screenX: evt.screenX, 183 screenY: evt.screenY, 184 }); 185 } 186 } else { 187 // for other content -> open simple context menu with copy only 188 if (!this.contextMenuSimple) { 189 this.contextMenuSimple = new PropertiesViewContextMenu(); 190 } 191 this.contextMenuSimple.open(evt, window.getSelection(), { 192 member, 193 }); 194 } 195 } 196 197 openRequestListContextMenu(evt, request) { 198 // reuse context menu of request list: 199 if (!this.contextMenuRequest) { 200 const { 201 connector, 202 cloneRequest, 203 openDetailsPanelTab, 204 openHTTPCustomRequestTab, 205 closeHTTPCustomRequestTab, 206 sendCustomRequest, 207 sendHTTPCustomRequest, 208 openStatistics, 209 openRequestBlockingAndAddUrl, 210 openRequestBlockingAndDisableUrls, 211 removeBlockedUrl, 212 } = this.props; 213 this.contextMenuRequest = new RequestListContextMenu({ 214 connector, 215 cloneRequest, 216 openDetailsPanelTab, 217 openHTTPCustomRequestTab, 218 closeHTTPCustomRequestTab, 219 sendCustomRequest, 220 sendHTTPCustomRequest, 221 openStatistics, 222 openRequestBlockingAndAddUrl, 223 openRequestBlockingAndDisableUrls, 224 removeBlockedUrl, 225 }); 226 } 227 228 const { blockedUrls, results } = this.props; 229 const allRequestsInResults = results.map(r => r.resource); 230 231 this.contextMenuRequest.open( 232 evt, 233 request, 234 allRequestsInResults, 235 blockedUrls 236 ); 237 } 238 239 renderTree() { 240 const { results } = this.props; 241 return TreeView({ 242 object: results, 243 provider: this.provider, 244 expandableStrings: false, 245 // Ensure passing a stable expanded Set, 246 // so that TreeView doesn't reset to default prop's Set 247 // on each new received props. 248 expandedNodes: this.expandedNodes, 249 renderLabelCell: this.renderLabel, 250 onContextMenuRow: this.onContextMenuTreeRow, 251 columns: [], 252 onClickRow: this.onClickTreeRow, 253 }); 254 } 255 256 /** 257 * Custom tree value rendering. This method is responsible for 258 * rendering highlighted query string within the search result 259 * result tree. 260 */ 261 renderValue(props) { 262 const { member } = props; 263 const { query, caseSensitive } = this.props; 264 265 // Handle only second level (zero based) that displays 266 // the search result. Find the query string inside the 267 // search result value (`props.object`) and render it 268 // within a span element with proper class name. 269 // level 0 = resource name 270 if (member.level === SEARCH_RESULT_LEVEL) { 271 const { object } = member; 272 273 // Handles multiple matches in a string 274 if (object.startIndex && object.startIndex.length > 1) { 275 let indexStart = 0; 276 const allMatches = object.startIndex.map((match, index) => { 277 if (index === 0) { 278 indexStart = match - 50; 279 } 280 281 const highlightedMatch = [ 282 span( 283 { key: "match-" + match }, 284 object.value.substring(indexStart, match) 285 ), 286 span( 287 { 288 className: "query-match", 289 key: "match-" + match + "-highlight", 290 }, 291 object.value.substring(match, match + query.length) 292 ), 293 ]; 294 295 indexStart = match + query.length; 296 297 return highlightedMatch; 298 }); 299 300 return span( 301 { 302 title: limitTooltipLength(object.value), 303 }, 304 allMatches 305 ); 306 } 307 308 const indexStart = caseSensitive 309 ? object.value.indexOf(query) 310 : object.value.toLowerCase().indexOf(query.toLowerCase()); 311 const indexEnd = indexStart + query.length; 312 313 // Handles a match in a string 314 if (indexStart >= 0) { 315 return span( 316 { title: limitTooltipLength(object.value) }, 317 span({}, object.value.substring(0, indexStart)), 318 span( 319 { className: "query-match" }, 320 object.value.substring(indexStart, indexStart + query.length) 321 ), 322 span({}, object.value.substring(indexEnd, object.value.length)) 323 ); 324 } 325 326 // Default for key:value matches where query might not 327 // be present in the value, but found in the key. 328 return span( 329 { title: limitTooltipLength(object.value) }, 330 span({}, object.value) 331 ); 332 } 333 334 return this.provider.getValue(member.object); 335 } 336 337 render() { 338 const { 339 openSearch, 340 closeSearch, 341 clearSearchResults, 342 connector, 343 addSearchQuery, 344 search, 345 } = this.props; 346 return div( 347 { className: "search-panel", style: { width: "100%" } }, 348 Toolbar({ 349 searchboxRef: this.searchboxRef, 350 openSearch, 351 closeSearch, 352 clearSearchResults, 353 addSearchQuery, 354 search, 355 connector, 356 }), 357 div( 358 { className: "search-panel-content", style: { width: "100%" } }, 359 this.renderTree() 360 ), 361 StatusBar() 362 ); 363 } 364 } 365 366 module.exports = connect( 367 state => ({ 368 query: state.search.query, 369 caseSensitive: state.search.caseSensitive, 370 results: state.search.results, 371 ongoingSearch: state.search.ongoingSearch, 372 isDisplaying: state.ui.selectedActionBarTabId === PANELS.SEARCH, 373 status: state.search.status, 374 blockedUrls: state.requestBlocking.blockedUrls, 375 requests: state.requests.requests, 376 }), 377 (dispatch, props) => ({ 378 closeSearch: () => dispatch(Actions.closeSearch()), 379 openSearch: () => dispatch(Actions.openSearch()), 380 search: () => dispatch(Actions.search()), 381 clearSearchResults: () => dispatch(Actions.clearSearchResults()), 382 addSearchQuery: query => dispatch(Actions.addSearchQuery(query)), 383 navigate: searchResult => dispatch(Actions.navigate(searchResult)), 384 cloneRequest: id => dispatch(Actions.cloneRequest(id)), 385 openDetailsPanelTab: () => dispatch(Actions.openNetworkDetails(true)), 386 openHTTPCustomRequestTab: () => 387 dispatch(Actions.openHTTPCustomRequest(true)), 388 closeHTTPCustomRequestTab: () => 389 dispatch(Actions.openHTTPCustomRequest(false)), 390 sendCustomRequest: () => dispatch(Actions.sendCustomRequest()), 391 sendHTTPCustomRequest: request => 392 dispatch(Actions.sendHTTPCustomRequest(request)), 393 openStatistics: open => 394 dispatch(Actions.openStatistics(props.connector, open)), 395 openRequestBlockingAndAddUrl: url => 396 dispatch(Actions.openRequestBlockingAndAddUrl(url)), 397 openRequestBlockingAndDisableUrls: url => 398 dispatch(Actions.openRequestBlockingAndDisableUrls(url)), 399 removeBlockedUrl: url => dispatch(Actions.removeBlockedUrl(url)), 400 }) 401 )(SearchPanel);