/* 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 dom = require("resource://devtools/client/shared/vendor/react-dom-factories.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 Actions = require("resource://devtools/client/netmonitor/src/actions/index.js"); const { FILTER_SEARCH_DELAY, FILTER_TAGS, PANELS, } = require("resource://devtools/client/netmonitor/src/constants.js"); const { getDisplayedRequests, getRecordingState, getTypeFilteredRequests, getSelectedRequest, } = require("resource://devtools/client/netmonitor/src/selectors/index.js"); const { autocompleteProvider, } = require("resource://devtools/client/netmonitor/src/utils/filter-autocomplete-provider.js"); const { L10N, } = require("resource://devtools/client/netmonitor/src/utils/l10n.js"); const { fetchNetworkUpdatePacket, } = require("resource://devtools/client/netmonitor/src/utils/request-utils.js"); loader.lazyRequireGetter( this, "KeyShortcuts", "resource://devtools/client/shared/key-shortcuts.js" ); // MDN const { getFilterBoxURL, } = require("resource://devtools/client/netmonitor/src/utils/doc-utils.js"); const LEARN_MORE_URL = getFilterBoxURL(); // Components const NetworkThrottlingMenu = createFactory( require("resource://devtools/client/shared/components/throttling/NetworkThrottlingMenu.js") ); const SearchBox = createFactory( require("resource://devtools/client/shared/components/SearchBox.js") ); const { button, div, input, label, span, hr } = dom; // Localization const FILTER_KEY_SHORTCUT = L10N.getStr( "netmonitor.toolbar.filterFreetext.key" ); const SEARCH_KEY_SHORTCUT = L10N.getStr("netmonitor.toolbar.search.key"); const SEARCH_PLACE_HOLDER = L10N.getStr( "netmonitor.toolbar.filterFreetext.label" ); const COPY_KEY_SHORTCUT = L10N.getStr("netmonitor.toolbar.copy.key"); const TOOLBAR_CLEAR = L10N.getStr("netmonitor.toolbar.clear"); const TOOLBAR_TOGGLE_RECORDING = L10N.getStr( "netmonitor.toolbar.toggleRecording" ); const TOOLBAR_HTTP_CUSTOM_REQUEST = L10N.getStr( "netmonitor.toolbar.HTTPCustomRequest" ); const TOOLBAR_SEARCH = L10N.getStr("netmonitor.toolbar.search"); const TOOLBAR_BLOCKING = L10N.getStr("netmonitor.toolbar.requestBlocking"); const LEARN_MORE_TITLE = L10N.getStr( "netmonitor.toolbar.filterFreetext.learnMore" ); // Preferences const DEVTOOLS_DISABLE_CACHE_PREF = "devtools.cache.disabled"; const DEVTOOLS_ENABLE_PERSISTENT_LOG_PREF = "devtools.netmonitor.persistlog"; const TOOLBAR_FILTER_LABELS = FILTER_TAGS.concat("all").reduce( (o, tag) => Object.assign(o, { [tag]: L10N.getStr(`netmonitor.toolbar.filter.${tag}`), }), {} ); const DISABLE_CACHE_TOOLTIP = L10N.getStr( "netmonitor.toolbar.disableCache.tooltip" ); const DISABLE_CACHE_LABEL = L10N.getStr( "netmonitor.toolbar.disableCache.label" ); const MenuButton = createFactory( require("resource://devtools/client/shared/components/menu/MenuButton.js") ); loader.lazyGetter(this, "MenuItem", function () { return createFactory( require("resource://devtools/client/shared/components/menu/MenuItem.js") ); }); loader.lazyGetter(this, "MenuList", function () { return createFactory( require("resource://devtools/client/shared/components/menu/MenuList.js") ); }); // Menu loader.lazyRequireGetter( this, "HarMenuUtils", "resource://devtools/client/netmonitor/src/har/har-menu-utils.js", true ); loader.lazyRequireGetter( this, "copyString", "resource://devtools/shared/platform/clipboard.js", true ); // Throttling const Types = require("resource://devtools/client/shared/components/throttling/types.js"); const { changeNetworkThrottling, } = require("resource://devtools/client/shared/components/throttling/actions.js"); /** * Network monitor toolbar component. * * Toolbar contains a set of useful tools to control network requests * as well as set of filters for filtering the content. */ class Toolbar extends Component { static get propTypes() { return { actions: PropTypes.object.isRequired, connector: PropTypes.object.isRequired, toggleRecording: PropTypes.func.isRequired, recording: PropTypes.bool.isRequired, clearRequests: PropTypes.func.isRequired, // List of currently displayed requests (i.e. filtered & sorted). displayedRequests: PropTypes.array.isRequired, requestFilterTypes: PropTypes.object.isRequired, setRequestFilterText: PropTypes.func.isRequired, enablePersistentLogs: PropTypes.func.isRequired, togglePersistentLogs: PropTypes.func.isRequired, persistentLogsEnabled: PropTypes.bool.isRequired, disableBrowserCache: PropTypes.func.isRequired, toggleBrowserCache: PropTypes.func.isRequired, browserCacheDisabled: PropTypes.bool.isRequired, toggleRequestFilterType: PropTypes.func.isRequired, filteredRequests: PropTypes.array.isRequired, // Set to true if there is enough horizontal space // and the toolbar needs just one row. singleRow: PropTypes.bool.isRequired, // Callback for opening split console. openSplitConsole: PropTypes.func, networkThrottling: PropTypes.shape(Types.networkThrottling).isRequired, // Executed when throttling changes (through toolbar button). onChangeNetworkThrottling: PropTypes.func.isRequired, toggleSearchPanel: PropTypes.func.isRequired, toggleHTTPCustomRequestPanel: PropTypes.func.isRequired, networkActionBarOpen: PropTypes.bool, toggleRequestBlockingPanel: PropTypes.func.isRequired, networkActionBarSelectedPanel: PropTypes.string.isRequired, hasBlockedRequests: PropTypes.bool.isRequired, selectedRequest: PropTypes.object, toolboxDoc: PropTypes.object.isRequired, }; } constructor(props) { super(props); this.autocompleteProvider = this.autocompleteProvider.bind(this); this.onSearchBoxFocusKeyboardShortcut = this.onSearchBoxFocusKeyboardShortcut.bind(this); this.onSearchBoxFocus = this.onSearchBoxFocus.bind(this); this.toggleRequestFilterType = this.toggleRequestFilterType.bind(this); this.updatePersistentLogsEnabled = this.updatePersistentLogsEnabled.bind(this); this.updateBrowserCacheDisabled = this.updateBrowserCacheDisabled.bind(this); } componentDidMount() { Services.prefs.addObserver( DEVTOOLS_ENABLE_PERSISTENT_LOG_PREF, this.updatePersistentLogsEnabled ); Services.prefs.addObserver( DEVTOOLS_DISABLE_CACHE_PREF, this.updateBrowserCacheDisabled ); this.shortcuts = new KeyShortcuts({ window, }); this.shortcuts.on(SEARCH_KEY_SHORTCUT, event => { event.preventDefault(); this.props.toggleSearchPanel(); }); this.shortcuts.on(COPY_KEY_SHORTCUT, () => { if (this.props.selectedRequest && this.props.selectedRequest.url) { copyString(this.props.selectedRequest.url); } }); } shouldComponentUpdate(nextProps) { return ( this.props.persistentLogsEnabled !== nextProps.persistentLogsEnabled || this.props.browserCacheDisabled !== nextProps.browserCacheDisabled || this.props.recording !== nextProps.recording || this.props.networkActionBarOpen !== nextProps.networkActionBarOpen || this.props.singleRow !== nextProps.singleRow || !Object.is(this.props.requestFilterTypes, nextProps.requestFilterTypes) || this.props.networkThrottling !== nextProps.networkThrottling || // Filtered requests are useful only when searchbox is focused !!(this.refs.searchbox && this.refs.searchbox.focused) || this.props.networkActionBarSelectedPanel !== nextProps.networkActionBarSelectedPanel || this.props.hasBlockedRequests !== nextProps.hasBlockedRequests ); } componentWillUnmount() { Services.prefs.removeObserver( DEVTOOLS_ENABLE_PERSISTENT_LOG_PREF, this.updatePersistentLogsEnabled ); Services.prefs.removeObserver( DEVTOOLS_DISABLE_CACHE_PREF, this.updateBrowserCacheDisabled ); if (this.shortcuts) { this.shortcuts.destroy(); } } toggleRequestFilterType(evt) { if (evt.type === "keydown" && (evt.key !== "" || evt.key !== "Enter")) { return; } this.props.toggleRequestFilterType(evt.target.dataset.key); } updatePersistentLogsEnabled() { // Make sure the UI is updated when the pref changes. // It might happen when the user changed it through about:config or // through another Toolbox instance (opened in another browser tab). // In such case, skip telemetry recordings. this.props.enablePersistentLogs( Services.prefs.getBoolPref(DEVTOOLS_ENABLE_PERSISTENT_LOG_PREF), true ); } updateBrowserCacheDisabled() { this.props.disableBrowserCache( Services.prefs.getBoolPref(DEVTOOLS_DISABLE_CACHE_PREF) ); } autocompleteProvider(filter) { return autocompleteProvider(filter, this.props.filteredRequests); } onSearchBoxFocusKeyboardShortcut(event) { // Don't take focus when the keyboard shortcut is triggered in a CodeMirror instance, // so the CodeMirror search UI is displayed. return !!event.target.closest(".CodeMirror"); } onSearchBoxFocus() { const { connector, filteredRequests } = this.props; // Fetch responseCookies & responseHeaders for building autocomplete list filteredRequests.forEach(request => { fetchNetworkUpdatePacket(connector.requestData, request, [ "responseCookies", "responseHeaders", ]); }); } /** * Render a separator. */ renderSeparator() { return span({ className: "devtools-separator" }); } /** * Render a clear button. */ renderClearButton(clearRequests) { return button({ className: "devtools-button devtools-clear-icon requests-list-clear-button", title: TOOLBAR_CLEAR, onClick: clearRequests, }); } /** * Render a ToggleRecording button. */ renderToggleRecordingButton(recording, toggleRecording) { // Calculate class-list for toggle recording button. // The button has two states: pause/play. const toggleRecordingButtonClass = [ "devtools-button", "requests-list-pause-button", recording ? "devtools-pause-icon" : "devtools-play-icon", ].join(" "); return button({ className: toggleRecordingButtonClass, title: TOOLBAR_TOGGLE_RECORDING, onClick: toggleRecording, }); } /** * Render a blocking button. */ renderBlockingButton() { const { networkActionBarOpen, toggleRequestBlockingPanel, networkActionBarSelectedPanel, hasBlockedRequests, } = this.props; // The blocking feature is available behind a pref. if ( !Services.prefs.getBoolPref( "devtools.netmonitor.features.requestBlocking" ) ) { return null; } const className = ["devtools-button", "requests-list-blocking-button"]; if ( networkActionBarOpen && networkActionBarSelectedPanel === PANELS.BLOCKING ) { className.push("checked"); } if (hasBlockedRequests) { className.push("requests-list-blocking-button-enabled"); } return button({ className: className.join(" "), title: TOOLBAR_BLOCKING, "aria-pressed": networkActionBarOpen, onClick: toggleRequestBlockingPanel, }); } /** * Render a search button. */ renderSearchButton(toggleSearchPanel) { const { networkActionBarOpen, networkActionBarSelectedPanel } = this.props; // The search feature is available behind a pref. if (!Services.prefs.getBoolPref("devtools.netmonitor.features.search")) { return null; } const className = [ "devtools-button", "devtools-search-icon", "requests-list-search-button", ]; if ( networkActionBarOpen && networkActionBarSelectedPanel === PANELS.SEARCH ) { className.push("checked"); } return button({ className: className.join(" "), title: TOOLBAR_SEARCH, "aria-pressed": networkActionBarOpen, onClick: toggleSearchPanel, }); } /** * Render a new HTTP Custom Request button. */ renderHTTPCustomRequestButton() { const { networkActionBarOpen, networkActionBarSelectedPanel, toggleHTTPCustomRequestPanel, } = this.props; // The new HTTP Custom Request feature is available behind a pref. if ( !Services.prefs.getBoolPref( "devtools.netmonitor.features.newEditAndResend" ) ) { return null; } const className = [ "devtools-button", "devtools-http-custom-request-icon", "requests-list-http-custom-request-button", ]; if ( networkActionBarOpen && networkActionBarSelectedPanel === PANELS.HTTP_CUSTOM_REQUEST ) { className.push("checked"); } return button({ className: className.join(" "), title: TOOLBAR_HTTP_CUSTOM_REQUEST, "aria-pressed": networkActionBarOpen, onClick: toggleHTTPCustomRequestPanel, }); } /** * Render filter buttons. */ renderFilterButtons(requestFilterTypes) { // Render list of filter-buttons. const buttons = Object.entries(requestFilterTypes).map(([type, checked]) => button( { className: `devtools-togglebutton requests-list-filter-${type}-button`, key: type, onClick: this.toggleRequestFilterType, onKeyDown: this.toggleRequestFilterType, "aria-pressed": checked, "data-key": type, }, TOOLBAR_FILTER_LABELS[type] ) ); return div({ className: "requests-list-filter-buttons" }, buttons); } /** * Render a Cache checkbox. */ renderCacheCheckbox(browserCacheDisabled, toggleBrowserCache) { return label( { className: "devtools-checkbox-label devtools-cache-checkbox", title: DISABLE_CACHE_TOOLTIP, }, input({ id: "devtools-cache-checkbox", className: "devtools-checkbox", type: "checkbox", checked: browserCacheDisabled, onChange: toggleBrowserCache, }), DISABLE_CACHE_LABEL ); } /** * Render network throttling menu button. */ renderThrottlingMenu() { const { networkThrottling, onChangeNetworkThrottling } = this.props; return NetworkThrottlingMenu({ networkThrottling, onChangeNetworkThrottling, }); } /** * Render filter Searchbox. */ renderFilterBox(setRequestFilterText) { return SearchBox({ delay: FILTER_SEARCH_DELAY, keyShortcut: FILTER_KEY_SHORTCUT, placeholder: SEARCH_PLACE_HOLDER, type: "filter", ref: "searchbox", onChange: setRequestFilterText, onFocusKeyboardShortcut: this.onSearchBoxFocusKeyboardShortcut, onFocus: this.onSearchBoxFocus, autocompleteProvider: this.autocompleteProvider, learnMoreUrl: LEARN_MORE_URL, learnMoreTitle: LEARN_MORE_TITLE, }); } renderSettingsMenuButton() { const { toolboxDoc } = this.props; return MenuButton( { menuId: "netmonitor-settings-menu-button", toolboxDoc, className: "devtools-button netmonitor-settings-menu-button", title: L10N.getStr("netmonitor.settings.menuTooltip"), }, // We pass the children in a function so we don't require the MenuItem and MenuList // components until we need to display them (i.e. when the button is clicked). () => this.renderSettingsMenuItems() ); } renderSettingsMenuItems() { const { actions, connector, displayedRequests, openSplitConsole, persistentLogsEnabled, togglePersistentLogs, } = this.props; const menuItems = [ MenuItem({ key: "netmonitor-settings-persist-item", className: "menu-item netmonitor-settings-persist-item", type: "checkbox", checked: persistentLogsEnabled, label: L10N.getStr("netmonitor.toolbar.enablePersistentLogs.label"), tooltip: L10N.getStr("netmonitor.toolbar.enablePersistentLogs.tooltip"), onClick: () => togglePersistentLogs(), }), hr({ key: "netmonitor-settings-har-divider" }), MenuItem({ key: "request-list-context-import-har", className: "menu-item netmonitor-settings-import-har-item", label: L10N.getStr("netmonitor.har.importHarDialogTitle"), tooltip: L10N.getStr("netmonitor.settings.importHarTooltip"), accesskey: L10N.getStr("netmonitor.context.importHar.accesskey"), onClick: () => HarMenuUtils.openHarFile(actions, openSplitConsole), }), MenuItem({ key: "request-list-context-save-all-as-har", className: "menu-item netmonitor-settings-save-har-item", label: L10N.getStr("netmonitor.context.saveAllAsHar"), accesskey: L10N.getStr("netmonitor.context.saveAllAsHar.accesskey"), tooltip: L10N.getStr("netmonitor.settings.saveHarTooltip"), disabled: !displayedRequests.length, onClick: () => HarMenuUtils.saveAllAsHar(displayedRequests, connector), }), MenuItem({ key: "request-list-context-copy-all-as-har", className: "menu-item netmonitor-settings-copy-har-item", label: L10N.getStr("netmonitor.context.copyAllAsHar"), accesskey: L10N.getStr("netmonitor.context.copyAllAsHar.accesskey"), tooltip: L10N.getStr("netmonitor.settings.copyHarTooltip"), disabled: !displayedRequests.length, onClick: () => HarMenuUtils.copyAllAsHar(displayedRequests, connector), }), ]; return MenuList({ id: "netmonitor-settings-menu-list" }, menuItems); } render() { const { toggleRecording, clearRequests, requestFilterTypes, setRequestFilterText, toggleBrowserCache, browserCacheDisabled, recording, singleRow, toggleSearchPanel, } = this.props; // Render the entire toolbar. // dock at bottom or dock at side has different layout return singleRow ? span( { id: "netmonitor-toolbar-container" }, span( { className: "devtools-toolbar devtools-input-toolbar" }, this.renderClearButton(clearRequests), this.renderSeparator(), this.renderFilterBox(setRequestFilterText), this.renderSeparator(), this.renderToggleRecordingButton(recording, toggleRecording), this.renderHTTPCustomRequestButton(), this.renderSearchButton(toggleSearchPanel), this.renderBlockingButton(toggleSearchPanel), this.renderSeparator(), this.renderFilterButtons(requestFilterTypes), this.renderSeparator(), this.renderCacheCheckbox(browserCacheDisabled, toggleBrowserCache), this.renderSeparator(), this.renderThrottlingMenu(), this.renderSeparator(), this.renderSettingsMenuButton() ) ) : span( { id: "netmonitor-toolbar-container" }, span( { className: "devtools-toolbar devtools-input-toolbar" }, this.renderClearButton(clearRequests), this.renderSeparator(), this.renderFilterBox(setRequestFilterText), this.renderSeparator(), this.renderToggleRecordingButton(recording, toggleRecording), this.renderHTTPCustomRequestButton(), this.renderSearchButton(toggleSearchPanel), this.renderBlockingButton(toggleSearchPanel), this.renderSeparator(), this.renderCacheCheckbox(browserCacheDisabled, toggleBrowserCache), this.renderSeparator(), this.renderThrottlingMenu(), this.renderSeparator(), this.renderSettingsMenuButton() ), span( { className: "devtools-toolbar devtools-input-toolbar" }, this.renderFilterButtons(requestFilterTypes) ) ); } } module.exports = connect( state => ({ browserCacheDisabled: state.ui.browserCacheDisabled, displayedRequests: getDisplayedRequests(state), hasBlockedRequests: state.requestBlocking.blockingEnabled && state.requestBlocking.blockedUrls.some(({ enabled }) => enabled), filteredRequests: getTypeFilteredRequests(state), persistentLogsEnabled: state.ui.persistentLogsEnabled, recording: getRecordingState(state), requestFilterTypes: state.filters.requestFilterTypes, networkThrottling: state.networkThrottling, networkActionBarOpen: state.ui.networkActionOpen, networkActionBarSelectedPanel: state.ui.selectedActionBarTabId || "", selectedRequest: getSelectedRequest(state), }), dispatch => ({ clearRequests: () => dispatch(Actions.clearRequests()), disableBrowserCache: disabled => dispatch(Actions.disableBrowserCache(disabled)), enablePersistentLogs: (enabled, skipTelemetry) => dispatch(Actions.enablePersistentLogs(enabled, skipTelemetry)), setRequestFilterText: text => dispatch(Actions.setRequestFilterText(text)), toggleBrowserCache: () => dispatch(Actions.toggleBrowserCache()), toggleRecording: () => dispatch(Actions.toggleRecording()), togglePersistentLogs: () => dispatch(Actions.togglePersistentLogs()), toggleRequestFilterType: type => dispatch(Actions.toggleRequestFilterType(type)), onChangeNetworkThrottling: (enabled, profile) => dispatch(changeNetworkThrottling(enabled, profile)), toggleHTTPCustomRequestPanel: () => dispatch(Actions.toggleHTTPCustomRequestPanel()), toggleSearchPanel: () => dispatch(Actions.toggleSearchPanel()), toggleRequestBlockingPanel: () => dispatch(Actions.toggleRequestBlockingPanel()), }) )(Toolbar);