Toolbar.js (22141B)
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 createFactory, 10 } = require("resource://devtools/client/shared/vendor/react.mjs"); 11 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); 12 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs"); 13 const { 14 connect, 15 } = require("resource://devtools/client/shared/vendor/react-redux.js"); 16 const Actions = require("resource://devtools/client/netmonitor/src/actions/index.js"); 17 const { 18 FILTER_SEARCH_DELAY, 19 FILTER_TAGS, 20 PANELS, 21 } = require("resource://devtools/client/netmonitor/src/constants.js"); 22 const { 23 getDisplayedRequests, 24 getRecordingState, 25 getTypeFilteredRequests, 26 getSelectedRequest, 27 } = require("resource://devtools/client/netmonitor/src/selectors/index.js"); 28 const { 29 autocompleteProvider, 30 } = require("resource://devtools/client/netmonitor/src/utils/filter-autocomplete-provider.js"); 31 const { 32 L10N, 33 } = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); 34 const { 35 fetchNetworkUpdatePacket, 36 } = require("resource://devtools/client/netmonitor/src/utils/request-utils.js"); 37 38 loader.lazyRequireGetter( 39 this, 40 "KeyShortcuts", 41 "resource://devtools/client/shared/key-shortcuts.js" 42 ); 43 44 // MDN 45 const { 46 getFilterBoxURL, 47 } = require("resource://devtools/client/netmonitor/src/utils/doc-utils.js"); 48 const LEARN_MORE_URL = getFilterBoxURL(); 49 50 // Components 51 const NetworkThrottlingMenu = createFactory( 52 require("resource://devtools/client/shared/components/throttling/NetworkThrottlingMenu.js") 53 ); 54 const SearchBox = createFactory( 55 require("resource://devtools/client/shared/components/SearchBox.js") 56 ); 57 58 const { button, div, input, label, span, hr } = dom; 59 60 // Localization 61 const FILTER_KEY_SHORTCUT = L10N.getStr( 62 "netmonitor.toolbar.filterFreetext.key" 63 ); 64 const SEARCH_KEY_SHORTCUT = L10N.getStr("netmonitor.toolbar.search.key"); 65 const SEARCH_PLACE_HOLDER = L10N.getStr( 66 "netmonitor.toolbar.filterFreetext.label" 67 ); 68 const COPY_KEY_SHORTCUT = L10N.getStr("netmonitor.toolbar.copy.key"); 69 const TOOLBAR_CLEAR = L10N.getStr("netmonitor.toolbar.clear"); 70 const TOOLBAR_TOGGLE_RECORDING = L10N.getStr( 71 "netmonitor.toolbar.toggleRecording" 72 ); 73 const TOOLBAR_HTTP_CUSTOM_REQUEST = L10N.getStr( 74 "netmonitor.toolbar.HTTPCustomRequest" 75 ); 76 const TOOLBAR_SEARCH = L10N.getStr("netmonitor.toolbar.search"); 77 const TOOLBAR_BLOCKING = L10N.getStr("netmonitor.toolbar.requestBlocking"); 78 const LEARN_MORE_TITLE = L10N.getStr( 79 "netmonitor.toolbar.filterFreetext.learnMore" 80 ); 81 82 // Preferences 83 const DEVTOOLS_DISABLE_CACHE_PREF = "devtools.cache.disabled"; 84 const DEVTOOLS_ENABLE_PERSISTENT_LOG_PREF = "devtools.netmonitor.persistlog"; 85 const TOOLBAR_FILTER_LABELS = FILTER_TAGS.concat("all").reduce( 86 (o, tag) => 87 Object.assign(o, { 88 [tag]: L10N.getStr(`netmonitor.toolbar.filter.${tag}`), 89 }), 90 {} 91 ); 92 const DISABLE_CACHE_TOOLTIP = L10N.getStr( 93 "netmonitor.toolbar.disableCache.tooltip" 94 ); 95 const DISABLE_CACHE_LABEL = L10N.getStr( 96 "netmonitor.toolbar.disableCache.label" 97 ); 98 99 const MenuButton = createFactory( 100 require("resource://devtools/client/shared/components/menu/MenuButton.js") 101 ); 102 103 loader.lazyGetter(this, "MenuItem", function () { 104 return createFactory( 105 require("resource://devtools/client/shared/components/menu/MenuItem.js") 106 ); 107 }); 108 109 loader.lazyGetter(this, "MenuList", function () { 110 return createFactory( 111 require("resource://devtools/client/shared/components/menu/MenuList.js") 112 ); 113 }); 114 115 // Menu 116 loader.lazyRequireGetter( 117 this, 118 "HarMenuUtils", 119 "resource://devtools/client/netmonitor/src/har/har-menu-utils.js", 120 true 121 ); 122 loader.lazyRequireGetter( 123 this, 124 "copyString", 125 "resource://devtools/shared/platform/clipboard.js", 126 true 127 ); 128 129 // Throttling 130 const Types = require("resource://devtools/client/shared/components/throttling/types.js"); 131 const { 132 changeNetworkThrottling, 133 } = require("resource://devtools/client/shared/components/throttling/actions.js"); 134 135 /** 136 * Network monitor toolbar component. 137 * 138 * Toolbar contains a set of useful tools to control network requests 139 * as well as set of filters for filtering the content. 140 */ 141 class Toolbar extends Component { 142 static get propTypes() { 143 return { 144 actions: PropTypes.object.isRequired, 145 connector: PropTypes.object.isRequired, 146 toggleRecording: PropTypes.func.isRequired, 147 recording: PropTypes.bool.isRequired, 148 clearRequests: PropTypes.func.isRequired, 149 // List of currently displayed requests (i.e. filtered & sorted). 150 displayedRequests: PropTypes.array.isRequired, 151 requestFilterTypes: PropTypes.object.isRequired, 152 setRequestFilterText: PropTypes.func.isRequired, 153 enablePersistentLogs: PropTypes.func.isRequired, 154 togglePersistentLogs: PropTypes.func.isRequired, 155 persistentLogsEnabled: PropTypes.bool.isRequired, 156 disableBrowserCache: PropTypes.func.isRequired, 157 toggleBrowserCache: PropTypes.func.isRequired, 158 browserCacheDisabled: PropTypes.bool.isRequired, 159 toggleRequestFilterType: PropTypes.func.isRequired, 160 filteredRequests: PropTypes.array.isRequired, 161 // Set to true if there is enough horizontal space 162 // and the toolbar needs just one row. 163 singleRow: PropTypes.bool.isRequired, 164 // Callback for opening split console. 165 openSplitConsole: PropTypes.func, 166 networkThrottling: PropTypes.shape(Types.networkThrottling).isRequired, 167 // Executed when throttling changes (through toolbar button). 168 onChangeNetworkThrottling: PropTypes.func.isRequired, 169 toggleSearchPanel: PropTypes.func.isRequired, 170 toggleHTTPCustomRequestPanel: PropTypes.func.isRequired, 171 networkActionBarOpen: PropTypes.bool, 172 toggleRequestBlockingPanel: PropTypes.func.isRequired, 173 networkActionBarSelectedPanel: PropTypes.string.isRequired, 174 hasBlockedRequests: PropTypes.bool.isRequired, 175 selectedRequest: PropTypes.object, 176 toolboxDoc: PropTypes.object.isRequired, 177 }; 178 } 179 180 constructor(props) { 181 super(props); 182 183 this.autocompleteProvider = this.autocompleteProvider.bind(this); 184 this.onSearchBoxFocusKeyboardShortcut = 185 this.onSearchBoxFocusKeyboardShortcut.bind(this); 186 this.onSearchBoxFocus = this.onSearchBoxFocus.bind(this); 187 this.toggleRequestFilterType = this.toggleRequestFilterType.bind(this); 188 this.updatePersistentLogsEnabled = 189 this.updatePersistentLogsEnabled.bind(this); 190 this.updateBrowserCacheDisabled = 191 this.updateBrowserCacheDisabled.bind(this); 192 } 193 194 componentDidMount() { 195 Services.prefs.addObserver( 196 DEVTOOLS_ENABLE_PERSISTENT_LOG_PREF, 197 this.updatePersistentLogsEnabled 198 ); 199 Services.prefs.addObserver( 200 DEVTOOLS_DISABLE_CACHE_PREF, 201 this.updateBrowserCacheDisabled 202 ); 203 204 this.shortcuts = new KeyShortcuts({ 205 window, 206 }); 207 208 this.shortcuts.on(SEARCH_KEY_SHORTCUT, event => { 209 event.preventDefault(); 210 this.props.toggleSearchPanel(); 211 }); 212 213 // Keyboard shortcut to copy the selected request URL 214 this.shortcuts.on(COPY_KEY_SHORTCUT, e => { 215 if (!this.props.selectedRequest?.url) { 216 return; 217 } 218 219 const selection = window.getSelection(); 220 if ( 221 // We don't want to copy selected URL in clipboard if the user selected some text… 222 (!selection.isCollapsed && selection.toString()) || 223 // …or if the keyboard shortcut happened in some inputs (which includes 224 // CodeMirror 5 underlying textarea) 225 e.target.matches("input, textarea") 226 ) { 227 return; 228 } 229 230 copyString(this.props.selectedRequest.url); 231 }); 232 } 233 234 shouldComponentUpdate(nextProps) { 235 return ( 236 this.props.persistentLogsEnabled !== nextProps.persistentLogsEnabled || 237 this.props.browserCacheDisabled !== nextProps.browserCacheDisabled || 238 this.props.recording !== nextProps.recording || 239 this.props.networkActionBarOpen !== nextProps.networkActionBarOpen || 240 this.props.singleRow !== nextProps.singleRow || 241 !Object.is(this.props.requestFilterTypes, nextProps.requestFilterTypes) || 242 this.props.networkThrottling !== nextProps.networkThrottling || 243 // Filtered requests are useful only when searchbox is focused 244 !!(this.refs.searchbox && this.refs.searchbox.focused) || 245 this.props.networkActionBarSelectedPanel !== 246 nextProps.networkActionBarSelectedPanel || 247 this.props.hasBlockedRequests !== nextProps.hasBlockedRequests 248 ); 249 } 250 251 componentWillUnmount() { 252 Services.prefs.removeObserver( 253 DEVTOOLS_ENABLE_PERSISTENT_LOG_PREF, 254 this.updatePersistentLogsEnabled 255 ); 256 Services.prefs.removeObserver( 257 DEVTOOLS_DISABLE_CACHE_PREF, 258 this.updateBrowserCacheDisabled 259 ); 260 261 if (this.shortcuts) { 262 this.shortcuts.destroy(); 263 } 264 } 265 266 toggleRequestFilterType(evt) { 267 if (evt.type === "keydown" && (evt.key !== "" || evt.key !== "Enter")) { 268 return; 269 } 270 this.props.toggleRequestFilterType(evt.target.dataset.key); 271 } 272 273 updatePersistentLogsEnabled() { 274 // Make sure the UI is updated when the pref changes. 275 // It might happen when the user changed it through about:config or 276 // through another Toolbox instance (opened in another browser tab). 277 // In such case, skip telemetry recordings. 278 this.props.enablePersistentLogs( 279 Services.prefs.getBoolPref(DEVTOOLS_ENABLE_PERSISTENT_LOG_PREF), 280 true 281 ); 282 } 283 284 updateBrowserCacheDisabled() { 285 this.props.disableBrowserCache( 286 Services.prefs.getBoolPref(DEVTOOLS_DISABLE_CACHE_PREF) 287 ); 288 } 289 290 autocompleteProvider(filter) { 291 return autocompleteProvider(filter, this.props.filteredRequests); 292 } 293 294 onSearchBoxFocusKeyboardShortcut(event) { 295 // Don't take focus when the keyboard shortcut is triggered in a CodeMirror instance, 296 // so the CodeMirror search UI is displayed. 297 return !!event.target.closest(".cm-editor"); 298 } 299 300 onSearchBoxFocus() { 301 const { connector, filteredRequests } = this.props; 302 303 // Fetch responseCookies & responseHeaders for building autocomplete list 304 filteredRequests.forEach(request => { 305 fetchNetworkUpdatePacket(connector.requestData, request, [ 306 "responseCookies", 307 "responseHeaders", 308 ]); 309 }); 310 } 311 312 /** 313 * Render a separator. 314 */ 315 renderSeparator() { 316 return span({ className: "devtools-separator" }); 317 } 318 319 /** 320 * Render a clear button. 321 */ 322 renderClearButton(clearRequests) { 323 return button({ 324 className: 325 "devtools-button devtools-clear-icon requests-list-clear-button", 326 title: TOOLBAR_CLEAR, 327 onClick: clearRequests, 328 }); 329 } 330 331 /** 332 * Render a ToggleRecording button. 333 */ 334 renderToggleRecordingButton(recording, toggleRecording) { 335 return button({ 336 className: "devtools-button requests-list-pause-button", 337 title: TOOLBAR_TOGGLE_RECORDING, 338 "aria-pressed": !recording, 339 onClick: toggleRecording, 340 }); 341 } 342 343 /** 344 * Render a blocking button. 345 */ 346 renderBlockingButton() { 347 const { 348 networkActionBarOpen, 349 toggleRequestBlockingPanel, 350 networkActionBarSelectedPanel, 351 hasBlockedRequests, 352 } = this.props; 353 354 // The blocking feature is available behind a pref. 355 if ( 356 !Services.prefs.getBoolPref( 357 "devtools.netmonitor.features.requestBlocking" 358 ) 359 ) { 360 return null; 361 } 362 363 const className = ["devtools-button", "requests-list-blocking-button"]; 364 if (hasBlockedRequests) { 365 className.push("requests-list-blocking-button-enabled"); 366 } 367 368 return button({ 369 className: className.join(" "), 370 title: TOOLBAR_BLOCKING, 371 "aria-pressed": 372 networkActionBarOpen && 373 networkActionBarSelectedPanel === PANELS.BLOCKING, 374 onClick: toggleRequestBlockingPanel, 375 }); 376 } 377 378 /** 379 * Render a search button. 380 */ 381 renderSearchButton(toggleSearchPanel) { 382 const { networkActionBarOpen, networkActionBarSelectedPanel } = this.props; 383 384 return button({ 385 className: "devtools-button devtools-search-icon", 386 title: TOOLBAR_SEARCH, 387 "aria-pressed": 388 networkActionBarOpen && networkActionBarSelectedPanel === PANELS.SEARCH, 389 onClick: toggleSearchPanel, 390 }); 391 } 392 393 /** 394 * Render a new HTTP Custom Request button. 395 */ 396 renderHTTPCustomRequestButton() { 397 const { 398 networkActionBarOpen, 399 networkActionBarSelectedPanel, 400 toggleHTTPCustomRequestPanel, 401 } = this.props; 402 403 // The new HTTP Custom Request feature is available behind a pref. 404 if ( 405 !Services.prefs.getBoolPref( 406 "devtools.netmonitor.features.newEditAndResend" 407 ) 408 ) { 409 return null; 410 } 411 412 return button({ 413 className: "devtools-button devtools-http-custom-request-icon", 414 title: TOOLBAR_HTTP_CUSTOM_REQUEST, 415 "aria-pressed": 416 networkActionBarOpen && 417 networkActionBarSelectedPanel === PANELS.HTTP_CUSTOM_REQUEST, 418 onClick: toggleHTTPCustomRequestPanel, 419 }); 420 } 421 422 /** 423 * Render filter buttons. 424 */ 425 renderFilterButtons(requestFilterTypes) { 426 // Render list of filter-buttons. 427 const buttons = Object.entries(requestFilterTypes).map(([type, checked]) => 428 button( 429 { 430 className: `devtools-togglebutton requests-list-filter-${type}-button`, 431 key: type, 432 onClick: this.toggleRequestFilterType, 433 onKeyDown: this.toggleRequestFilterType, 434 "aria-pressed": checked, 435 "data-key": type, 436 }, 437 TOOLBAR_FILTER_LABELS[type] 438 ) 439 ); 440 return div({ className: "requests-list-filter-buttons" }, buttons); 441 } 442 443 /** 444 * Render a Cache checkbox. 445 */ 446 renderCacheCheckbox(browserCacheDisabled, toggleBrowserCache) { 447 return label( 448 { 449 className: "devtools-checkbox-label devtools-cache-checkbox", 450 title: DISABLE_CACHE_TOOLTIP, 451 }, 452 input({ 453 id: "devtools-cache-checkbox", 454 className: "devtools-checkbox", 455 type: "checkbox", 456 checked: browserCacheDisabled, 457 onChange: toggleBrowserCache, 458 }), 459 DISABLE_CACHE_LABEL 460 ); 461 } 462 463 /** 464 * Render network throttling menu button. 465 */ 466 renderThrottlingMenu() { 467 const { networkThrottling, onChangeNetworkThrottling, toolboxDoc } = 468 this.props; 469 470 return NetworkThrottlingMenu({ 471 networkThrottling, 472 onChangeNetworkThrottling, 473 toolboxDoc, 474 }); 475 } 476 477 /** 478 * Render filter Searchbox. 479 */ 480 renderFilterBox(setRequestFilterText) { 481 return SearchBox({ 482 delay: FILTER_SEARCH_DELAY, 483 keyShortcut: FILTER_KEY_SHORTCUT, 484 placeholder: SEARCH_PLACE_HOLDER, 485 type: "filter", 486 ref: "searchbox", 487 initialValue: Services.prefs.getCharPref( 488 "devtools.netmonitor.requestfilter" 489 ), 490 onChange: setRequestFilterText, 491 onFocusKeyboardShortcut: this.onSearchBoxFocusKeyboardShortcut, 492 onFocus: this.onSearchBoxFocus, 493 autocompleteProvider: this.autocompleteProvider, 494 learnMoreUrl: LEARN_MORE_URL, 495 learnMoreTitle: LEARN_MORE_TITLE, 496 }); 497 } 498 499 renderSettingsMenuButton() { 500 const { toolboxDoc } = this.props; 501 return MenuButton( 502 { 503 menuId: "netmonitor-settings-menu-button", 504 toolboxDoc, 505 className: "devtools-button netmonitor-settings-menu-button", 506 title: L10N.getStr("netmonitor.settings.menuTooltip"), 507 }, 508 // We pass the children in a function so we don't require the MenuItem and MenuList 509 // components until we need to display them (i.e. when the button is clicked). 510 () => this.renderSettingsMenuItems() 511 ); 512 } 513 514 renderSettingsMenuItems() { 515 const { 516 actions, 517 connector, 518 displayedRequests, 519 openSplitConsole, 520 persistentLogsEnabled, 521 togglePersistentLogs, 522 } = this.props; 523 524 const menuItems = [ 525 MenuItem({ 526 key: "netmonitor-settings-persist-item", 527 className: "menu-item netmonitor-settings-persist-item", 528 type: "checkbox", 529 checked: persistentLogsEnabled, 530 label: L10N.getStr("netmonitor.toolbar.enablePersistentLogs.label"), 531 tooltip: L10N.getStr("netmonitor.toolbar.enablePersistentLogs.tooltip"), 532 onClick: () => togglePersistentLogs(), 533 }), 534 hr({ key: "netmonitor-settings-har-divider" }), 535 MenuItem({ 536 key: "request-list-context-import-har", 537 className: "menu-item netmonitor-settings-import-har-item", 538 label: L10N.getStr("netmonitor.har.importHarDialogTitle"), 539 tooltip: L10N.getStr("netmonitor.settings.importHarTooltip"), 540 accesskey: L10N.getStr("netmonitor.context.importHar.accesskey"), 541 onClick: () => HarMenuUtils.openHarFile(actions, openSplitConsole), 542 }), 543 MenuItem({ 544 key: "request-list-context-save-all-as-har", 545 className: "menu-item netmonitor-settings-save-har-item", 546 label: L10N.getStr("netmonitor.context.saveAllAsHar"), 547 accesskey: L10N.getStr("netmonitor.context.saveAllAsHar.accesskey"), 548 tooltip: L10N.getStr("netmonitor.settings.saveHarTooltip"), 549 disabled: !displayedRequests.length, 550 onClick: () => HarMenuUtils.saveAllAsHar(displayedRequests, connector), 551 }), 552 MenuItem({ 553 key: "request-list-context-copy-all-as-har", 554 className: "menu-item netmonitor-settings-copy-har-item", 555 label: L10N.getStr("netmonitor.context.copyAllAsHar"), 556 accesskey: L10N.getStr("netmonitor.context.copyAllAsHar.accesskey"), 557 tooltip: L10N.getStr("netmonitor.settings.copyHarTooltip"), 558 disabled: !displayedRequests.length, 559 onClick: () => HarMenuUtils.copyAllAsHar(displayedRequests, connector), 560 }), 561 ]; 562 563 return MenuList({ id: "netmonitor-settings-menu-list" }, menuItems); 564 } 565 566 render() { 567 const { 568 toggleRecording, 569 clearRequests, 570 requestFilterTypes, 571 setRequestFilterText, 572 toggleBrowserCache, 573 browserCacheDisabled, 574 recording, 575 singleRow, 576 toggleSearchPanel, 577 } = this.props; 578 579 // Render the entire toolbar. 580 // dock at bottom or dock at side has different layout 581 return singleRow 582 ? span( 583 { id: "netmonitor-toolbar-container" }, 584 span( 585 { className: "devtools-toolbar devtools-input-toolbar" }, 586 this.renderClearButton(clearRequests), 587 this.renderSeparator(), 588 this.renderFilterBox(setRequestFilterText), 589 this.renderSeparator(), 590 this.renderToggleRecordingButton(recording, toggleRecording), 591 this.renderHTTPCustomRequestButton(), 592 this.renderSearchButton(toggleSearchPanel), 593 this.renderBlockingButton(toggleSearchPanel), 594 this.renderSeparator(), 595 this.renderFilterButtons(requestFilterTypes), 596 this.renderSeparator(), 597 this.renderCacheCheckbox(browserCacheDisabled, toggleBrowserCache), 598 this.renderSeparator(), 599 this.renderThrottlingMenu(), 600 this.renderSeparator(), 601 this.renderSettingsMenuButton() 602 ) 603 ) 604 : span( 605 { id: "netmonitor-toolbar-container" }, 606 span( 607 { className: "devtools-toolbar devtools-input-toolbar" }, 608 this.renderClearButton(clearRequests), 609 this.renderSeparator(), 610 this.renderFilterBox(setRequestFilterText), 611 this.renderSeparator(), 612 this.renderToggleRecordingButton(recording, toggleRecording), 613 this.renderHTTPCustomRequestButton(), 614 this.renderSearchButton(toggleSearchPanel), 615 this.renderBlockingButton(toggleSearchPanel), 616 this.renderSeparator(), 617 this.renderCacheCheckbox(browserCacheDisabled, toggleBrowserCache), 618 this.renderSeparator(), 619 this.renderThrottlingMenu(), 620 this.renderSeparator(), 621 this.renderSettingsMenuButton() 622 ), 623 span( 624 { className: "devtools-toolbar devtools-input-toolbar" }, 625 this.renderFilterButtons(requestFilterTypes) 626 ) 627 ); 628 } 629 } 630 631 module.exports = connect( 632 state => ({ 633 browserCacheDisabled: state.ui.browserCacheDisabled, 634 displayedRequests: getDisplayedRequests(state), 635 hasBlockedRequests: 636 state.requestBlocking.blockingEnabled && 637 state.requestBlocking.blockedUrls.some(({ enabled }) => enabled), 638 filteredRequests: getTypeFilteredRequests(state), 639 persistentLogsEnabled: state.ui.persistentLogsEnabled, 640 recording: getRecordingState(state), 641 requestFilterTypes: state.filters.requestFilterTypes, 642 networkThrottling: state.networkThrottling, 643 networkActionBarOpen: state.ui.networkActionOpen, 644 networkActionBarSelectedPanel: state.ui.selectedActionBarTabId || "", 645 selectedRequest: getSelectedRequest(state), 646 }), 647 dispatch => ({ 648 clearRequests: () => 649 dispatch(Actions.clearRequests({ isExplicitClear: true })), 650 disableBrowserCache: disabled => 651 dispatch(Actions.disableBrowserCache(disabled)), 652 enablePersistentLogs: (enabled, skipTelemetry) => 653 dispatch(Actions.enablePersistentLogs(enabled, skipTelemetry)), 654 setRequestFilterText: text => dispatch(Actions.setRequestFilterText(text)), 655 toggleBrowserCache: () => dispatch(Actions.toggleBrowserCache()), 656 toggleRecording: () => dispatch(Actions.toggleRecording()), 657 togglePersistentLogs: () => dispatch(Actions.togglePersistentLogs()), 658 toggleRequestFilterType: type => 659 dispatch(Actions.toggleRequestFilterType(type)), 660 onChangeNetworkThrottling: (enabled, profile) => 661 dispatch(changeNetworkThrottling(enabled, profile)), 662 toggleHTTPCustomRequestPanel: () => 663 dispatch(Actions.toggleHTTPCustomRequestPanel()), 664 toggleSearchPanel: () => dispatch(Actions.toggleSearchPanel()), 665 toggleRequestBlockingPanel: () => 666 dispatch(Actions.toggleRequestBlockingPanel()), 667 }) 668 )(Toolbar);