chrome-devtools-frontend
Version:
Chrome DevTools UI
1,074 lines (962 loc) • 45.7 kB
text/typescript
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/* eslint-disable rulesdir/no-imperative-dom-api */
/*
* Copyright (C) 2007, 2008 Apple Inc. All rights reserved.
* Copyright (C) 2008, 2009 Anthony Ricaud <rik@webkit.org>
* Copyright (C) 2011 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import '../../ui/legacy/legacy.js';
import * as Common from '../../core/common/common.js';
import * as Host from '../../core/host/host.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as Platform from '../../core/platform/platform.js';
import * as SDK from '../../core/sdk/sdk.js';
import type * as Extensions from '../../models/extensions/extensions.js';
import * as Logs from '../../models/logs/logs.js';
import * as Trace from '../../models/trace/trace.js';
import * as Workspace from '../../models/workspace/workspace.js';
import * as NetworkForward from '../../panels/network/forward/forward.js';
import * as PerfUI from '../../ui/legacy/components/perf_ui/perf_ui.js';
import * as UI from '../../ui/legacy/legacy.js';
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
import * as MobileThrottling from '../mobile_throttling/mobile_throttling.js';
import * as Search from '../search/search.js';
import {Events, type RequestActivatedEvent} from './NetworkDataGridNode.js';
import {NetworkItemView} from './NetworkItemView.js';
import {NetworkLogView} from './NetworkLogView.js';
import {NetworkOverview} from './NetworkOverview.js';
import networkPanelStyles from './networkPanel.css.js';
import {NetworkSearchScope} from './NetworkSearchScope.js';
import {type NetworkTimeCalculator, NetworkTransferTimeCalculator} from './NetworkTimeCalculator.js';
const UIStrings = {
/**
*@description Text to close something
*/
close: 'Close',
/**
*@description Title of a search bar or tool
*/
search: 'Search',
/**
*@description Tooltip text that appears on the setting to preserve log when hovering over the item
*/
doNotClearLogOnPageReload: 'Do not clear log on page reload / navigation',
/**
*@description Text to preserve the log after refreshing
*/
preserveLog: 'Preserve log',
/**
*@description Text to disable cache while DevTools is open
*/
disableCacheWhileDevtoolsIsOpen: 'Disable cache while DevTools is open',
/**
*@description Text in Network Config View of the Network panel
*/
disableCache: 'Disable cache',
/**
*@description Tooltip text that appears when hovering over the largeicon settings gear in show settings pane setting in network panel of the network panel
*/
networkSettings: 'Network settings',
/**
*@description Tooltip for expanding network request row setting
*/
showMoreInformationInRequestRows: 'Show more information in request rows',
/**
*@description Text in Network Panel used to toggle the "big request rows" setting.
*/
useLargeRequestRows: 'Big request rows',
/**
*@description Tooltip text for network request overview setting
*/
showOverviewOfNetworkRequests: 'Show overview of network requests',
/**
*@description Text in Network Panel used to show the overview for a given network request.
*/
showOverview: 'Overview',
/**
*@description Tooltip for group by frame network setting
*/
groupRequestsByTopLevelRequest: 'Group requests by top level request frame',
/**
*@description Text for group by frame network setting
*/
groupByFrame: 'Group by frame',
/**
*@description Tooltip for capture screenshot network setting
*/
captureScreenshotsWhenLoadingA: 'Capture screenshots when loading a page',
/**
*@description Text to take screenshots
*/
captureScreenshots: 'Screenshots',
/**
* @description Tooltip text that appears when hovering over the largeicon load button in the
* Network Panel. This action prompts the user to select a HAR file to upload to DevTools.
*/
importHarFile: 'Import `HAR` file…',
/**
* @description Tooltip text that appears when hovering over the download button in the Network
* panel, when the setting to allow generating HAR files with sensitive data is enabled. HAR is
* a file format (HTTP Archive) and should not be translated. This action triggers a context
* menu with two options, one to download HAR sanitized and one to download HAR with sensitive
* data.
*/
exportHar: 'Export `HAR` (either sanitized or with sensitive data)',
/**
* @description Tooltip text that appears when hovering over the download button in the Network
* panel, when the setting to allow generating HAR files with sensitive data is disabled. HAR is
* a file format (HTTP Archive) and should not be translated. This action triggers the download
* of a HAR file.
*
* This string is also used as the first item in the context menu for the download button in
* the Network panel, when the setting to allow generating HAR files with sensitive data is
* enabled.
*/
exportHarSanitized: 'Export `HAR` (sanitized)…',
/**
* @description Context menu item in the context menu for the download button of the Network panel,
* which is only available when the Network setting to allow generating HAR with sensitive data
* is active. HAR is a file format (HTTP Archive) and should not be translated. This action
* triggers the download of a HAR file with sensitive data included.
*/
exportHarWithSensitiveData: 'Export `HAR` (with sensitive data)…',
/**
*@description Text for throttling the network
*/
throttling: 'Throttling',
/**
*@description Text in Network Panel to tell the user to reload the page to capture screenshots.
*@example {Ctrl + R} PH1
*/
hitSToReloadAndCaptureFilmstrip: 'Press {PH1} to reload and capture filmstrip.',
/**
* @description A context menu item that is shown for resources in other panels
* to open them in the Network panel.
*/
openInNetworkPanel: 'Open in Network panel',
/**
* @description A context menu item that is shown for resources in other panels
* to open them in the Network panel, but when there's no associated network
* request. This context menu item is always disabled and only provided to give
* the developer an idea of why they cannot open the resource in the Network
* panel.
*/
openInNetworkPanelMissingRequest: 'Open in Network panel (missing request)',
/**
*@description Text in Network Panel that is displayed whilst the recording is in progress.
*/
recordingFrames: 'Recording frames…',
/**
*@description Text in Network Panel that is displayed when frames are being fetched.
*/
fetchingFrames: 'Fetching frames…',
/**
* @description Text of a button in the Network panel's toolbar that open Network Conditions panel in the drawer.
*/
moreNetworkConditions: 'More network conditions…',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/network/NetworkPanel.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
let networkPanelInstance: NetworkPanel;
export class NetworkPanel extends UI.Panel.Panel implements
UI.ContextMenu
.Provider<SDK.NetworkRequest.NetworkRequest|SDK.Resource.Resource|Workspace.UISourceCode.UISourceCode>,
UI.View.ViewLocationResolver {
private readonly networkLogShowOverviewSetting: Common.Settings.Setting<boolean>;
private readonly networkLogLargeRowsSetting: Common.Settings.Setting<boolean>;
private readonly networkRecordFilmStripSetting: Common.Settings.Setting<boolean>;
private readonly toggleRecordAction: UI.ActionRegistration.Action;
private pendingStopTimer!: number|undefined;
networkItemView: NetworkItemView|null;
private filmStripView: PerfUI.FilmStripView.FilmStripView|null;
private filmStripRecorder: FilmStripRecorder|null;
private currentRequest: SDK.NetworkRequest.NetworkRequest|null;
private readonly panelToolbar: UI.Toolbar.Toolbar;
private readonly rightToolbar: UI.Toolbar.Toolbar;
private readonly filterBar: UI.FilterBar.FilterBar;
private showSettingsPaneSetting: Common.Settings.Setting<boolean>;
private readonly filmStripPlaceholderElement: HTMLElement;
private readonly overviewPane: PerfUI.TimelineOverviewPane.TimelineOverviewPane;
private readonly networkOverview: NetworkOverview;
private readonly overviewPlaceholderElement: HTMLElement;
private readonly calculator: NetworkTransferTimeCalculator;
private splitWidget: UI.SplitWidget.SplitWidget;
private readonly sidebarLocation: UI.View.TabbedViewLocation;
private readonly progressBarContainer: HTMLDivElement;
networkLogView: NetworkLogView;
private readonly fileSelectorElement: HTMLElement;
private readonly detailsWidget: UI.Widget.VBox;
private readonly closeButtonElement: UI.UIUtils.DevToolsCloseButton;
private preserveLogSetting: Common.Settings.Setting<boolean>;
recordLogSetting: Common.Settings.Setting<boolean>;
private readonly throttlingSelect: UI.Toolbar.ToolbarComboBox;
private readonly displayScreenshotDelay: number;
constructor(displayScreenshotDelay: number) {
super('network');
this.registerRequiredCSS(networkPanelStyles);
this.displayScreenshotDelay = displayScreenshotDelay;
this.networkLogShowOverviewSetting =
Common.Settings.Settings.instance().createSetting('network-log-show-overview', true);
this.networkLogLargeRowsSetting =
Common.Settings.Settings.instance().createSetting('network-log-large-rows', false);
this.networkRecordFilmStripSetting =
Common.Settings.Settings.instance().createSetting('network-record-film-strip-setting', false);
this.toggleRecordAction = UI.ActionRegistry.ActionRegistry.instance().getAction('network.toggle-recording');
this.networkItemView = null;
this.filmStripView = null;
this.filmStripRecorder = null;
this.currentRequest = null;
const panel = new UI.Widget.VBox();
const networkToolbarContainer = panel.contentElement.createChild('div', 'network-toolbar-container');
networkToolbarContainer.role = 'toolbar';
this.panelToolbar = networkToolbarContainer.createChild('devtools-toolbar');
this.panelToolbar.role = 'presentation';
this.panelToolbar.wrappable = true;
this.panelToolbar.setAttribute('jslog', `${VisualLogging.toolbar('network-main')}`);
this.rightToolbar = networkToolbarContainer.createChild('devtools-toolbar');
this.rightToolbar.role = 'presentation';
this.filterBar = new UI.FilterBar.FilterBar('network-panel', true);
this.filterBar.show(panel.contentElement);
this.filterBar.addEventListener(UI.FilterBar.FilterBarEvents.CHANGED, this.handleFilterChanged.bind(this));
const settingsPane = panel.contentElement.createChild('div', 'network-settings-pane');
settingsPane.append(
UI.SettingsUI.createSettingCheckbox(
i18nString(UIStrings.useLargeRequestRows), this.networkLogLargeRowsSetting,
i18nString(UIStrings.showMoreInformationInRequestRows)),
UI.SettingsUI.createSettingCheckbox(
i18nString(UIStrings.groupByFrame),
Common.Settings.Settings.instance().moduleSetting('network.group-by-frame'),
i18nString(UIStrings.groupRequestsByTopLevelRequest)),
UI.SettingsUI.createSettingCheckbox(
i18nString(UIStrings.showOverview), this.networkLogShowOverviewSetting,
i18nString(UIStrings.showOverviewOfNetworkRequests)),
UI.SettingsUI.createSettingCheckbox(
i18nString(UIStrings.captureScreenshots), this.networkRecordFilmStripSetting,
i18nString(UIStrings.captureScreenshotsWhenLoadingA)),
);
this.showSettingsPaneSetting =
Common.Settings.Settings.instance().createSetting('network-show-settings-toolbar', false);
settingsPane.classList.toggle('hidden', !this.showSettingsPaneSetting.get());
this.showSettingsPaneSetting.addChangeListener(
() => settingsPane.classList.toggle('hidden', !this.showSettingsPaneSetting.get()));
this.filmStripPlaceholderElement = panel.contentElement.createChild('div', 'network-film-strip-placeholder');
// Create top overview component.
this.overviewPane = new PerfUI.TimelineOverviewPane.TimelineOverviewPane('network');
this.overviewPane.addEventListener(
PerfUI.TimelineOverviewPane.Events.OVERVIEW_PANE_WINDOW_CHANGED, this.onWindowChanged.bind(this));
this.overviewPane.element.id = 'network-overview-panel';
this.networkOverview = new NetworkOverview();
this.overviewPane.setOverviewControls([this.networkOverview]);
this.overviewPlaceholderElement = panel.contentElement.createChild('div');
this.calculator = new NetworkTransferTimeCalculator();
this.splitWidget = new UI.SplitWidget.SplitWidget(true, false, 'network-panel-split-view-state');
this.splitWidget.hideMain();
this.splitWidget.show(panel.contentElement);
panel.setDefaultFocusedChild(this.filterBar);
const initialSidebarWidth = 225;
const splitWidget = new UI.SplitWidget.SplitWidget(true, false, 'network-panel-sidebar-state', initialSidebarWidth);
splitWidget.hideSidebar();
splitWidget.enableShowModeSaving();
splitWidget.show(this.element);
this.sidebarLocation = UI.ViewManager.ViewManager.instance().createTabbedLocation(async () => {
void UI.ViewManager.ViewManager.instance().showView('network');
splitWidget.showBoth();
}, 'network-sidebar', true);
const tabbedPane = this.sidebarLocation.tabbedPane();
tabbedPane.setMinimumSize(100, 25);
tabbedPane.element.classList.add('network-tabbed-pane');
tabbedPane.element.addEventListener('keydown', event => {
if (event.key !== Platform.KeyboardUtilities.ESCAPE_KEY) {
return;
}
splitWidget.hideSidebar();
event.consume();
void VisualLogging.logKeyDown(event.currentTarget, event, 'hide-sidebar');
});
const closeSidebar = new UI.Toolbar.ToolbarButton(i18nString(UIStrings.close), 'cross');
closeSidebar.addEventListener(UI.Toolbar.ToolbarButton.Events.CLICK, () => splitWidget.hideSidebar());
closeSidebar.element.setAttribute('jslog', `${VisualLogging.close().track({click: true})}`);
tabbedPane.rightToolbar().appendToolbarItem(closeSidebar);
splitWidget.setSidebarWidget(tabbedPane);
splitWidget.setMainWidget(panel);
splitWidget.setDefaultFocusedChild(panel);
this.setDefaultFocusedChild(splitWidget);
this.progressBarContainer = document.createElement('div');
this.networkLogView =
new NetworkLogView(this.filterBar, this.progressBarContainer, this.networkLogLargeRowsSetting);
this.splitWidget.setSidebarWidget(this.networkLogView);
this.fileSelectorElement =
(UI.UIUtils.createFileSelectorElement(this.networkLogView.onLoadFromFile.bind(this.networkLogView)) as
HTMLElement);
panel.element.appendChild(this.fileSelectorElement);
this.detailsWidget = new UI.Widget.VBox();
this.detailsWidget.element.classList.add('network-details-view');
this.splitWidget.setMainWidget(this.detailsWidget);
this.closeButtonElement = document.createElement('dt-close-button');
this.closeButtonElement.addEventListener('click', async () => {
const action = UI.ActionRegistry.ActionRegistry.instance().getAction('network.hide-request-details');
await action.execute();
}, false);
this.closeButtonElement.style.margin = '0 5px';
this.networkLogShowOverviewSetting.addChangeListener(this.toggleShowOverview, this);
this.networkLogLargeRowsSetting.addChangeListener(this.toggleLargerRequests, this);
this.networkRecordFilmStripSetting.addChangeListener(this.toggleRecordFilmStrip, this);
this.preserveLogSetting = Common.Settings.Settings.instance().moduleSetting('network-log.preserve-log');
this.recordLogSetting = Common.Settings.Settings.instance().moduleSetting('network-log.record-log');
this.recordLogSetting.addChangeListener(({data}) => this.toggleRecord(data));
this.throttlingSelect = this.createThrottlingConditionsSelect();
this.setupToolbarButtons(splitWidget);
this.toggleRecord(this.recordLogSetting.get());
this.toggleShowOverview();
this.toggleLargerRequests();
this.toggleRecordFilmStrip();
this.updateUI();
SDK.TargetManager.TargetManager.instance().addModelListener(
SDK.ResourceTreeModel.ResourceTreeModel, SDK.ResourceTreeModel.Events.WillReloadPage, this.willReloadPage, this,
{scoped: true});
SDK.TargetManager.TargetManager.instance().addModelListener(
SDK.ResourceTreeModel.ResourceTreeModel, SDK.ResourceTreeModel.Events.Load, this.load, this, {scoped: true});
this.networkLogView.addEventListener(Events.RequestSelected, this.onRequestSelected, this);
this.networkLogView.addEventListener(Events.RequestActivated, this.onRequestActivated, this);
Logs.NetworkLog.NetworkLog.instance().addEventListener(
Logs.NetworkLog.Events.RequestAdded, this.onUpdateRequest, this);
Logs.NetworkLog.NetworkLog.instance().addEventListener(
Logs.NetworkLog.Events.RequestUpdated, this.onUpdateRequest, this);
Logs.NetworkLog.NetworkLog.instance().addEventListener(Logs.NetworkLog.Events.Reset, this.onNetworkLogReset, this);
}
static instance(opts?: {
forceNew: boolean,
displayScreenshotDelay?: number,
}): NetworkPanel {
if (!networkPanelInstance || opts?.forceNew) {
networkPanelInstance = new NetworkPanel(opts?.displayScreenshotDelay ?? 1000);
}
return networkPanelInstance;
}
static revealAndFilter(filters: Array<{
filterType: NetworkForward.UIFilter.FilterType | null,
filterValue: string,
}>): Promise<void> {
const panel = NetworkPanel.instance();
let filterString = '';
for (const filter of filters) {
if (filter.filterType) {
filterString += `${filter.filterType}:${filter.filterValue} `;
} else {
filterString += `${filter.filterValue} `;
}
}
panel.networkLogView.setTextFilterValue(filterString);
return UI.ViewManager.ViewManager.instance().showView('network');
}
throttlingSelectForTest(): UI.Toolbar.ToolbarComboBox {
return this.throttlingSelect;
}
private onWindowChanged(
event: Common.EventTarget.EventTargetEvent<PerfUI.TimelineOverviewPane.OverviewPaneWindowChangedEvent>): void {
const startTime = Math.max(this.calculator.minimumBoundary(), event.data.startTime / 1000);
const endTime = Math.min(this.calculator.maximumBoundary(), event.data.endTime / 1000);
if (startTime === this.calculator.minimumBoundary() && endTime === this.calculator.maximumBoundary()) {
// Reset the filters for NetworkLogView when the window is reset
// to its boundaries. This clears the filters and allows the users
// to see the incoming requests after they have updated the curtains
// to be in the edges. (ex: by double clicking on the overview grid)
this.networkLogView.setWindow(0, 0);
} else {
this.networkLogView.setWindow(startTime, endTime);
}
}
private async searchToggleClick(): Promise<void> {
const action = UI.ActionRegistry.ActionRegistry.instance().getAction('network.search');
await action.execute();
}
private setupToolbarButtons(splitWidget: UI.SplitWidget.SplitWidget): void {
const searchToggle = new UI.Toolbar.ToolbarToggle(i18nString(UIStrings.search), 'search', undefined, 'search');
function updateSidebarToggle(): void {
const isSidebarShowing = splitWidget.showMode() !== UI.SplitWidget.ShowMode.ONLY_MAIN;
searchToggle.setToggled(isSidebarShowing);
if (!isSidebarShowing) {
(searchToggle.element as HTMLElement).focus();
}
}
this.panelToolbar.appendToolbarItem(UI.Toolbar.Toolbar.createActionButton(this.toggleRecordAction));
this.panelToolbar.appendToolbarItem(UI.Toolbar.Toolbar.createActionButton('network.clear'));
this.panelToolbar.appendSeparator();
this.panelToolbar.appendToolbarItem(this.filterBar.filterButton());
updateSidebarToggle();
splitWidget.addEventListener(UI.SplitWidget.Events.SHOW_MODE_CHANGED, updateSidebarToggle);
searchToggle.addEventListener(UI.Toolbar.ToolbarButton.Events.CLICK, () => {
void this.searchToggleClick();
});
this.panelToolbar.appendToolbarItem(searchToggle);
this.panelToolbar.appendSeparator();
this.panelToolbar.appendToolbarItem(new UI.Toolbar.ToolbarSettingCheckbox(
this.preserveLogSetting, i18nString(UIStrings.doNotClearLogOnPageReload), i18nString(UIStrings.preserveLog)));
this.panelToolbar.appendSeparator();
const disableCacheCheckbox = new UI.Toolbar.ToolbarSettingCheckbox(
Common.Settings.Settings.instance().moduleSetting('cache-disabled'),
i18nString(UIStrings.disableCacheWhileDevtoolsIsOpen), i18nString(UIStrings.disableCache));
this.panelToolbar.appendToolbarItem(disableCacheCheckbox);
this.panelToolbar.appendToolbarItem(this.throttlingSelect);
const networkConditionsButton = new UI.Toolbar.ToolbarButton(
i18nString(UIStrings.moreNetworkConditions), 'network-settings', undefined, 'network-conditions');
networkConditionsButton.addEventListener(UI.Toolbar.ToolbarButton.Events.CLICK, () => {
void UI.ViewManager.ViewManager.instance().showView('network.config');
}, this);
this.panelToolbar.appendToolbarItem(networkConditionsButton);
this.rightToolbar.appendToolbarItem(new UI.Toolbar.ToolbarItem(this.progressBarContainer));
this.rightToolbar.appendSeparator();
this.rightToolbar.appendToolbarItem(new UI.Toolbar.ToolbarSettingToggle(
this.showSettingsPaneSetting, 'gear', i18nString(UIStrings.networkSettings), 'gear-filled',
'network-settings'));
const exportHarContextMenu = (contextMenu: UI.ContextMenu.ContextMenu): void => {
contextMenu.defaultSection().appendItem(
i18nString(UIStrings.exportHarSanitized),
this.networkLogView.exportAll.bind(this.networkLogView, {sanitize: true}),
{jslogContext: 'export-har'},
);
contextMenu.defaultSection().appendItem(
i18nString(UIStrings.exportHarWithSensitiveData),
this.networkLogView.exportAll.bind(this.networkLogView, {sanitize: false}),
{jslogContext: 'export-har-with-sensitive-data'},
);
};
this.panelToolbar.appendSeparator();
const importHarButton =
new UI.Toolbar.ToolbarButton(i18nString(UIStrings.importHarFile), 'import', undefined, 'import-har');
importHarButton.addEventListener(
UI.Toolbar.ToolbarButton.Events.CLICK, () => this.fileSelectorElement.click(), this);
this.panelToolbar.appendToolbarItem(importHarButton);
const exportHarButton =
new UI.Toolbar.ToolbarButton(i18nString(UIStrings.exportHarSanitized), 'download', undefined, 'export-har');
exportHarButton.addEventListener(
UI.Toolbar.ToolbarButton.Events.CLICK,
this.networkLogView.exportAll.bind(this.networkLogView, {sanitize: true}), this);
this.panelToolbar.appendToolbarItem(exportHarButton);
const exportHarMenuButton = new UI.Toolbar.ToolbarMenuButton(
exportHarContextMenu, /* isIconDropdown */ true, /* useSoftMenu */ false, 'export-har-menu', 'download');
exportHarMenuButton.setTitle(i18nString(UIStrings.exportHar));
this.panelToolbar.appendToolbarItem(exportHarMenuButton);
const networkShowOptionsToGenerateHarWithSensitiveData = Common.Settings.Settings.instance().createSetting(
'network.show-options-to-generate-har-with-sensitive-data', false);
const updateShowOptionsToGenerateHarWithSensitiveData = (): void => {
const showOptionsToGenerateHarWithSensitiveData = networkShowOptionsToGenerateHarWithSensitiveData.get();
exportHarButton.setVisible(!showOptionsToGenerateHarWithSensitiveData);
exportHarMenuButton.setVisible(showOptionsToGenerateHarWithSensitiveData);
};
networkShowOptionsToGenerateHarWithSensitiveData.addChangeListener(updateShowOptionsToGenerateHarWithSensitiveData);
updateShowOptionsToGenerateHarWithSensitiveData();
}
private createThrottlingConditionsSelect(): UI.Toolbar.ToolbarComboBox {
const toolbarItem = new UI.Toolbar.ToolbarComboBox(null, i18nString(UIStrings.throttling));
toolbarItem.setMaxWidth(160);
MobileThrottling.ThrottlingManager.throttlingManager().createNetworkThrottlingSelector(toolbarItem.element);
return toolbarItem;
}
toggleRecord(toggled: boolean): void {
this.toggleRecordAction.setToggled(toggled);
if (this.recordLogSetting.get() !== toggled) {
this.recordLogSetting.set(toggled);
}
this.networkLogView.setRecording(toggled);
if (!toggled && this.filmStripRecorder) {
this.filmStripRecorder.stopRecording(this.filmStripAvailable.bind(this));
}
}
private filmStripAvailable(filmStrip: Trace.Extras.FilmStrip.Data): void {
if (this.filmStripView) {
this.filmStripView.setModel(filmStrip);
}
const timestamps = filmStrip.frames.map(frame => {
// The network view works in seconds.
return Trace.Helpers.Timing.microToSeconds(frame.screenshotEvent.ts);
});
this.networkLogView.addFilmStripFrames(timestamps);
}
private onNetworkLogReset(event: Common.EventTarget.EventTargetEvent<Logs.NetworkLog.ResetEvent>): void {
const {clearIfPreserved} = event.data;
if (!this.preserveLogSetting.get() || clearIfPreserved) {
this.calculator.reset();
this.overviewPane.reset();
}
if (this.filmStripView) {
this.resetFilmStripView();
}
}
private willReloadPage(): void {
if (this.pendingStopTimer) {
clearTimeout(this.pendingStopTimer);
delete this.pendingStopTimer;
}
if (this.isShowing() && this.filmStripRecorder) {
this.filmStripRecorder.startRecording();
}
}
private load(): void {
if (this.filmStripRecorder?.isRecording()) {
if (this.pendingStopTimer) {
window.clearTimeout(this.pendingStopTimer);
}
this.pendingStopTimer = window.setTimeout(this.stopFilmStripRecording.bind(this), this.displayScreenshotDelay);
}
}
private stopFilmStripRecording(): void {
if (this.filmStripRecorder) {
this.filmStripRecorder.stopRecording(this.filmStripAvailable.bind(this));
}
delete this.pendingStopTimer;
}
private toggleLargerRequests(): void {
this.updateUI();
}
private toggleShowOverview(): void {
const toggled = this.networkLogShowOverviewSetting.get();
if (toggled) {
this.overviewPane.show(this.overviewPlaceholderElement);
} else {
this.overviewPane.detach();
}
this.doResize();
}
private toggleRecordFilmStrip(): void {
const toggled = this.networkRecordFilmStripSetting.get();
if (toggled && !this.filmStripRecorder) {
this.filmStripView = new PerfUI.FilmStripView.FilmStripView();
this.filmStripView.element.classList.add('network-film-strip');
this.filmStripView.element.setAttribute('jslog', `${VisualLogging.section('film-strip')}`);
this.filmStripRecorder = new FilmStripRecorder(this.networkLogView.timeCalculator(), this.filmStripView);
this.filmStripView.show(this.filmStripPlaceholderElement);
this.filmStripView.addEventListener(PerfUI.FilmStripView.Events.FRAME_SELECTED, this.onFilmFrameSelected, this);
this.filmStripView.addEventListener(PerfUI.FilmStripView.Events.FRAME_ENTER, this.onFilmFrameEnter, this);
this.filmStripView.addEventListener(PerfUI.FilmStripView.Events.FRAME_EXIT, this.onFilmFrameExit, this);
this.resetFilmStripView();
}
if (!toggled && this.filmStripRecorder) {
if (this.filmStripView) {
this.filmStripView.detach();
}
this.filmStripView = null;
this.filmStripRecorder = null;
}
}
private resetFilmStripView(): void {
const reloadShortcut =
UI.ShortcutRegistry.ShortcutRegistry.instance().shortcutsForAction('inspector-main.reload')[0];
if (this.filmStripView) {
this.filmStripView.reset();
if (reloadShortcut) {
this.filmStripView.setStatusText(
i18nString(UIStrings.hitSToReloadAndCaptureFilmstrip, {PH1: reloadShortcut.title()}));
}
}
}
override elementsToRestoreScrollPositionsFor(): Element[] {
return this.networkLogView.elementsToRestoreScrollPositionsFor();
}
override wasShown(): void {
super.wasShown();
UI.Context.Context.instance().setFlavor(NetworkPanel, this);
// Record the network tool load time after the panel has loaded.
Host.userMetrics.panelLoaded('network', 'DevTools.Launch.Network');
}
override willHide(): void {
UI.Context.Context.instance().setFlavor(NetworkPanel, null);
super.willHide();
}
revealAndHighlightRequest(request: SDK.NetworkRequest.NetworkRequest): void {
this.hideRequestPanel();
if (request) {
this.networkLogView.revealAndHighlightRequest(request);
}
}
revealAndHighlightRequestWithId(request: NetworkForward.NetworkRequestId.NetworkRequestId): void {
this.hideRequestPanel();
if (request) {
this.networkLogView.revealAndHighlightRequestWithId(request);
}
}
async selectAndActivateRequest(
request: SDK.NetworkRequest.NetworkRequest, shownTab?: NetworkForward.UIRequestLocation.UIRequestTabs,
options?: NetworkForward.UIRequestLocation.FilterOptions): Promise<NetworkItemView|null> {
await UI.ViewManager.ViewManager.instance().showView('network');
this.networkLogView.selectRequest(request, options);
this.showRequestPanel(shownTab);
this.networkLogView.revealAndHighlightRequest(request);
return this.networkItemView;
}
private handleFilterChanged(): void {
this.hideRequestPanel();
}
private onRequestSelected(event: Common.EventTarget.EventTargetEvent<SDK.NetworkRequest.NetworkRequest|null>): void {
const request = event.data;
this.currentRequest = request;
this.networkOverview.setHighlightedRequest(request);
this.updateNetworkItemView();
UI.Context.Context.instance().setFlavor(SDK.NetworkRequest.NetworkRequest, request);
}
private onRequestActivated(event: Common.EventTarget.EventTargetEvent<RequestActivatedEvent>): void {
const {showPanel, tab, takeFocus} = event.data;
if (showPanel) {
this.showRequestPanel(tab, takeFocus);
} else {
this.hideRequestPanel();
}
}
private showRequestPanel(shownTab?: NetworkForward.UIRequestLocation.UIRequestTabs, takeFocus?: boolean): void {
if (this.splitWidget.showMode() === UI.SplitWidget.ShowMode.BOTH && !shownTab && !takeFocus) {
// If panel is already shown, and we are not forcing a specific tab, return.
return;
}
this.clearNetworkItemView();
if (this.currentRequest) {
const networkItemView = this.createNetworkItemView(shownTab);
if (networkItemView && takeFocus) {
networkItemView.focus();
}
}
this.updateUI();
}
hideRequestPanel(): void {
this.clearNetworkItemView();
this.splitWidget.hideMain();
this.updateUI();
}
private updateNetworkItemView(): void {
if (this.splitWidget.showMode() === UI.SplitWidget.ShowMode.BOTH) {
this.clearNetworkItemView();
this.createNetworkItemView();
this.updateUI();
}
}
private clearNetworkItemView(): void {
if (this.networkItemView) {
this.networkItemView.detach();
this.networkItemView = null;
}
}
private createNetworkItemView(initialTab?: NetworkForward.UIRequestLocation.UIRequestTabs): NetworkItemView
|undefined {
if (!this.currentRequest) {
return;
}
this.networkItemView = new NetworkItemView(this.currentRequest, this.networkLogView.timeCalculator(), initialTab);
this.networkItemView.leftToolbar().appendToolbarItem(new UI.Toolbar.ToolbarItem(this.closeButtonElement));
this.networkItemView.show(this.detailsWidget.element);
this.splitWidget.showBoth();
return this.networkItemView;
}
private updateUI(): void {
if (this.detailsWidget) {
this.detailsWidget.element.classList.toggle(
'network-details-view-tall-header', this.networkLogLargeRowsSetting.get());
}
if (this.networkLogView) {
this.networkLogView.switchViewMode(!this.splitWidget.isResizable());
}
}
appendApplicableItems(
this: NetworkPanel, event: Event, contextMenu: UI.ContextMenu.ContextMenu,
target: SDK.NetworkRequest.NetworkRequest|SDK.Resource.Resource|Workspace.UISourceCode.UISourceCode|
SDK.TraceObject.RevealableNetworkRequest): void {
const appendRevealItem = (request: SDK.NetworkRequest.NetworkRequest): void => {
contextMenu.revealSection().appendItem(
i18nString(UIStrings.openInNetworkPanel),
() => UI.ViewManager.ViewManager.instance()
.showView('network')
.then(this.networkLogView.resetFilter.bind(this.networkLogView))
.then(this.revealAndHighlightRequest.bind(this, request)),
{jslogContext: 'reveal-in-network'});
};
const appendRevealItemMissingData = (): void => {
contextMenu.revealSection().appendItem(i18nString(UIStrings.openInNetworkPanelMissingRequest), () => {}, {
disabled: true,
jslogContext: 'reveal-in-network',
});
};
const appendRevealItemAndSelect = (request: SDK.TraceObject.RevealableNetworkRequest): void => {
contextMenu.revealSection().appendItem(
i18nString(UIStrings.openInNetworkPanel),
() => UI.ViewManager.ViewManager.instance()
.showView('network')
.then(this.networkLogView.resetFilter.bind(this.networkLogView))
.then(this.selectAndActivateRequest.bind(
this, request.networkRequest, NetworkForward.UIRequestLocation.UIRequestTabs.HEADERS_COMPONENT,
/* FilterOptions= */ undefined)),
{jslogContext: 'timeline.reveal-in-network'});
};
if ((event.target as Node).isSelfOrDescendant(this.element)) {
return;
}
if (target instanceof SDK.Resource.Resource) {
if (target.request) {
appendRevealItem(target.request);
} else {
appendRevealItemMissingData();
}
return;
}
if (target instanceof Workspace.UISourceCode.UISourceCode) {
const resource = SDK.ResourceTreeModel.ResourceTreeModel.resourceForURL(target.url());
if (resource?.request) {
appendRevealItem(resource.request);
} else {
appendRevealItemMissingData();
}
return;
}
if (target instanceof SDK.TraceObject.RevealableNetworkRequest) {
appendRevealItemAndSelect(target);
return;
}
if (this.networkItemView && this.networkItemView.isShowing() && this.networkItemView.request() === target) {
return;
}
appendRevealItem(target);
}
private onFilmFrameSelected(event: Common.EventTarget.EventTargetEvent<number>): void {
const timestamp = event.data;
this.overviewPane.setWindowTimes(Trace.Types.Timing.Milli(0), Trace.Types.Timing.Milli(timestamp));
}
private onFilmFrameEnter(event: Common.EventTarget.EventTargetEvent<number>): void {
const timestamp = event.data;
this.networkOverview.selectFilmStripFrame(timestamp);
this.networkLogView.selectFilmStripFrame(timestamp / 1000);
}
private onFilmFrameExit(): void {
this.networkOverview.clearFilmStripFrame();
this.networkLogView.clearFilmStripFrame();
}
private onUpdateRequest(event: Common.EventTarget.EventTargetEvent<{request: SDK.NetworkRequest.NetworkRequest}>):
void {
const {request} = event.data;
this.calculator.updateBoundaries(request);
// FIXME: Unify all time units across the frontend!
this.overviewPane.setBounds(
Trace.Types.Timing.Milli(this.calculator.minimumBoundary() * 1000),
Trace.Types.Timing.Milli(this.calculator.maximumBoundary() * 1000));
this.networkOverview.updateRequest(request);
}
resolveLocation(locationName: string): UI.View.ViewLocation|null {
if (locationName === 'network-sidebar') {
return this.sidebarLocation;
}
return null;
}
}
export class RequestRevealer implements Common.Revealer.Revealer<SDK.NetworkRequest.NetworkRequest> {
reveal(request: SDK.NetworkRequest.NetworkRequest): Promise<void> {
const panel = NetworkPanel.instance();
return UI.ViewManager.ViewManager.instance().showView('network').then(
panel.revealAndHighlightRequest.bind(panel, request));
}
}
export class RequestIdRevealer implements Common.Revealer.Revealer<NetworkForward.NetworkRequestId.NetworkRequestId> {
reveal(requestId: NetworkForward.NetworkRequestId.NetworkRequestId): Promise<void> {
const panel = NetworkPanel.instance();
return UI.ViewManager.ViewManager.instance().showView('network').then(
panel.revealAndHighlightRequestWithId.bind(panel, requestId));
}
}
export class NetworkLogWithFilterRevealer implements
Common.Revealer
.Revealer<Extensions.ExtensionServer.RevealableNetworkRequestFilter|NetworkForward.UIFilter.UIRequestFilter> {
reveal(request: Extensions.ExtensionServer.RevealableNetworkRequestFilter|NetworkForward.UIFilter.UIRequestFilter):
Promise<void> {
if ('filters' in request) {
return NetworkPanel.revealAndFilter(request.filters);
}
return NetworkPanel.revealAndFilter(request.filter ? [{filterType: null, filterValue: request.filter}] : []);
}
}
export class FilmStripRecorder implements Trace.TracingManager.TracingManagerClient {
private tracingManager: Trace.TracingManager.TracingManager|null;
private resourceTreeModel: SDK.ResourceTreeModel.ResourceTreeModel|null;
private readonly timeCalculator: NetworkTimeCalculator;
private readonly filmStripView: PerfUI.FilmStripView.FilmStripView;
private callback: ((filmStrip: Trace.Extras.FilmStrip.Data) => void)|null;
// Used to fetch screenshots of the page load and show them in the panel.
#traceEngine: Trace.TraceModel.Model;
#collectedTraceEvents: Trace.Types.Events.Event[] = [];
constructor(timeCalculator: NetworkTimeCalculator, filmStripView: PerfUI.FilmStripView.FilmStripView) {
this.#traceEngine = Trace.TraceModel.Model.createWithSubsetOfHandlers({
Screenshots: Trace.Handlers.ModelHandlers.Screenshots,
});
this.tracingManager = null;
this.resourceTreeModel = null;
this.timeCalculator = timeCalculator;
this.filmStripView = filmStripView;
this.callback = null;
}
traceEventsCollected(events: Trace.Types.Events.Event[]): void {
this.#collectedTraceEvents.push(...events);
}
async tracingComplete(): Promise<void> {
if (!this.tracingManager) {
return;
}
this.tracingManager = null;
await this.#traceEngine.parse(this.#collectedTraceEvents);
const data = this.#traceEngine.parsedTrace(this.#traceEngine.size() - 1) as
Trace.Extras.FilmStrip.HandlerDataWithScreenshots;
if (!data) {
return;
}
const zeroTimeInSeconds = Trace.Types.Timing.Seconds(this.timeCalculator.minimumBoundary());
const filmStrip =
Trace.Extras.FilmStrip.fromParsedTrace(data, Trace.Helpers.Timing.secondsToMicro(zeroTimeInSeconds));
if (this.callback) {
this.callback(filmStrip);
}
this.callback = null;
// Now we have created the film strip and stored the data, we need to reset
// the trace processor so that it is ready to record again if the user
// refreshes the page.
this.#traceEngine.resetProcessor();
if (this.resourceTreeModel) {
this.resourceTreeModel.resumeReload();
}
this.resourceTreeModel = null;
}
tracingBufferUsage(): void {
}
eventsRetrievalProgress(_progress: number): void {
}
startRecording(): void {
this.#collectedTraceEvents = [];
this.filmStripView.reset();
this.filmStripView.setStatusText(i18nString(UIStrings.recordingFrames));
const tracingManager =
SDK.TargetManager.TargetManager.instance().scopeTarget()?.model(Trace.TracingManager.TracingManager);
if (this.tracingManager || !tracingManager) {
return;
}
this.tracingManager = tracingManager;
this.resourceTreeModel = this.tracingManager.target().model(SDK.ResourceTreeModel.ResourceTreeModel);
void this.tracingManager.start(this, '-*,disabled-by-default-devtools.screenshot');
Host.userMetrics.actionTaken(Host.UserMetrics.Action.FilmStripStartedRecording);
}
isRecording(): boolean {
return Boolean(this.tracingManager);
}
stopRecording(callback: (filmStrip: Trace.Extras.FilmStrip.Data) => void): void {
if (!this.tracingManager) {
return;
}
this.tracingManager.stop();
if (this.resourceTreeModel) {
this.resourceTreeModel.suspendReload();
}
this.callback = callback;
this.filmStripView.setStatusText(i18nString(UIStrings.fetchingFrames));
}
}
export class ActionDelegate implements UI.ActionRegistration.ActionDelegate {
handleAction(context: UI.Context.Context, actionId: string): boolean {
const panel = context.flavor(NetworkPanel);
if (panel === null) {
return false;
}
switch (actionId) {
case 'network.toggle-recording': {
panel.toggleRecord(!panel.recordLogSetting.get());
return true;
}
case 'network.hide-request-details': {
if (!panel.networkItemView) {
return false;
}
panel.hideRequestPanel();
panel.networkLogView.resetFocus();
return true;
}
case 'network.search': {
const selection = UI.InspectorView.InspectorView.instance().element.window().getSelection();
if (!selection) {
return false;
}
let queryCandidate = '';
if (selection.rangeCount) {
queryCandidate = selection.toString().replace(/\r?\n.*/, '');
}
void SearchNetworkView.openSearch(queryCandidate);
return true;
}
case 'network.clear': {
Logs.NetworkLog.NetworkLog.instance().reset(true);
return true;
}
}
return false;
}
}
export class RequestLocationRevealer implements
Common.Revealer.Revealer<NetworkForward.UIRequestLocation.UIRequestLocation> {
async reveal(location: NetworkForward.UIRequestLocation.UIRequestLocation): Promise<void> {
const view =
await NetworkPanel.instance().selectAndActivateRequest(location.request, location.tab, location.filterOptions);
if (!view) {
return;
}
if (location.searchMatch) {
const {lineNumber, columnNumber, matchLength} = location.searchMatch;
const revealPosition = {
from: {lineNumber, columnNumber},
to: {lineNumber, columnNumber: columnNumber + matchLength},
};
await view.revealResponseBody(revealPosition);
}
if (location.header) {
view.revealHeader(location.header.section, location.header.header?.name);
}
}
}
let searchNetworkViewInstance: SearchNetworkView;
export class SearchNetworkView extends Search.SearchView.SearchView {
private constructor() {
super('network', new Common.Throttler.Throttler(/* timeoutMs */ 200));
}
static instance(opts: {
forceNew: boolean|null,
} = {forceNew: null}): SearchNetworkView {
const {forceNew} = opts;
if (!searchNetworkViewInstance || forceNew) {
searchNetworkViewInstance = new SearchNetworkView();
}
return searchNetworkViewInstance;
}
static async openSearch(query: string, searchImmediately?: boolean): Promise<Search.SearchView.SearchView> {
await UI.ViewManager.ViewManager.instance().showView('network.search-network-tab');
const searchView = SearchNetworkView.instance();
searchView.toggle(query, Boolean(searchImmediately));
return searchView;
}
override createScope(): Search.SearchScope.SearchScope {
return new NetworkSearchScope(Logs.NetworkLog.NetworkLog.instance());
}
}