blob: 010964b0a1dbe7fb973a22922672b9c86804b557 [file] [log] [blame]
// Copyright 2016 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.
import * as Common from '../../core/common/common.js';
import * as i18n from '../../core/i18n/i18n.js';
import type * as SDK from '../../core/sdk/sdk.js';
import * as DataGrid from '../../ui/legacy/components/data_grid/data_grid.js';
import * as Components from '../../ui/legacy/components/utils/utils.js';
import * as UI from '../../ui/legacy/legacy.js';
import * as ThemeSupport from '../../ui/legacy/theme_support/theme_support.js';
import {NetworkRequestNode, type NetworkNode} from './NetworkDataGridNode.js';
import {type NetworkLogView} from './NetworkLogView.js';
import {NetworkManageCustomHeadersView} from './NetworkManageCustomHeadersView.js';
import {
type NetworkTimeCalculator,
type NetworkTransferDurationCalculator,
type NetworkTransferTimeCalculator,
} from './NetworkTimeCalculator.js';
import {NetworkWaterfallColumn} from './NetworkWaterfallColumn.js';
import {RequestInitiatorView} from './RequestInitiatorView.js';
const UIStrings = {
/**
*@description Data grid name for Network Log data grids
*/
networkLog: 'Network Log',
/**
*@description Inner element text content in Network Log View Columns of the Network panel
*/
waterfall: 'Waterfall',
/**
*@description A context menu item in the Network Log View Columns of the Network panel
*/
responseHeaders: 'Response Headers',
/**
*@description Text in Network Log View Columns of the Network panel
*/
manageHeaderColumns: 'Manage Header Columns…',
/**
*@description Text for the start time of an activity
*/
startTime: 'Start Time',
/**
*@description Text in Network Log View Columns of the Network panel
*/
responseTime: 'Response Time',
/**
*@description Text in Network Log View Columns of the Network panel
*/
endTime: 'End Time',
/**
*@description Text in Network Log View Columns of the Network panel
*/
totalDuration: 'Total Duration',
/**
*@description Text for the latency of a task
*/
latency: 'Latency',
/**
*@description Text for the name of something
*/
name: 'Name',
/**
*@description Text that refers to a file path
*/
path: 'Path',
/**
*@description Text in Timeline UIUtils of the Performance panel
*/
url: 'Url',
/**
*@description Text for one or a group of functions
*/
method: 'Method',
/**
*@description Text for the status of something
*/
status: 'Status',
/**
*@description Generic label for any text
*/
text: 'Text',
/**
*@description Text for security or network protocol
*/
protocol: 'Protocol',
/**
*@description Text in Network Log View Columns of the Network panel
*/
scheme: 'Scheme',
/**
*@description Text for the domain of a website
*/
domain: 'Domain',
/**
*@description Text in Network Log View Columns of the Network panel
*/
remoteAddress: 'Remote Address',
/**
*@description Text that refers to some types
*/
type: 'Type',
/**
*@description Text for the initiator of something
*/
initiator: 'Initiator',
/**
*@description Column header in the Network log view of the Network panel
*/
initiatorAddressSpace: 'Initiator Address Space',
/**
*@description Text for web cookies
*/
cookies: 'Cookies',
/**
*@description Text in Network Log View Columns of the Network panel
*/
setCookies: 'Set Cookies',
/**
*@description Text for the size of something
*/
size: 'Size',
/**
*@description Text in Network Log View Columns of the Network panel
*/
content: 'Content',
/**
*@description Text that refers to the time
*/
time: 'Time',
/**
*@description Text to show the priority of an item
*/
priority: 'Priority',
/**
*@description Text in Network Log View Columns of the Network panel
*/
connectionId: 'Connection ID',
/**
*@description Text in Network Log View Columns of the Network panel
*/
remoteAddressSpace: 'Remote Address Space',
};
const str_ = i18n.i18n.registerUIStrings('panels/network/NetworkLogViewColumns.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
const i18nLazyString = i18n.i18n.getLazilyComputedLocalizedString.bind(undefined, str_);
export class NetworkLogViewColumns {
private networkLogView: NetworkLogView;
private readonly persistantSettings: Common.Settings.Setting<{
[x: string]: {
visible: boolean,
title: string,
},
}>;
private readonly networkLogLargeRowsSetting: Common.Settings.Setting<boolean>;
private readonly eventDividers: Map<string, number[]>;
private eventDividersShown: boolean;
private gridMode: boolean;
private columns: Descriptor[];
private waterfallRequestsAreStale: boolean;
private waterfallScrollerWidthIsStale: boolean;
private readonly popupLinkifier: Components.Linkifier.Linkifier;
private calculatorsMap: Map<string, NetworkTimeCalculator>;
private lastWheelTime: number;
private dataGridInternal!: DataGrid.SortableDataGrid.SortableDataGrid<NetworkNode>;
private splitWidget!: UI.SplitWidget.SplitWidget;
private waterfallColumn!: NetworkWaterfallColumn;
private activeScroller!: Element;
private dataGridScroller!: HTMLElement;
private waterfallScroller!: HTMLElement;
private waterfallScrollerContent!: HTMLDivElement;
private waterfallHeaderElement!: HTMLElement;
private waterfallColumnSortIcon!: UI.Icon.Icon;
private activeWaterfallSortId!: string;
private popoverHelper?: UI.PopoverHelper.PopoverHelper;
private hasScrollerTouchStarted?: boolean;
private scrollerTouchStartPos?: number;
constructor(
networkLogView: NetworkLogView, timeCalculator: NetworkTransferTimeCalculator,
durationCalculator: NetworkTransferDurationCalculator,
networkLogLargeRowsSetting: Common.Settings.Setting<boolean>) {
this.networkLogView = networkLogView;
this.persistantSettings = Common.Settings.Settings.instance().createSetting('networkLogColumns', {});
this.networkLogLargeRowsSetting = networkLogLargeRowsSetting;
this.networkLogLargeRowsSetting.addChangeListener(this.updateRowsSize, this);
this.eventDividers = new Map();
this.eventDividersShown = false;
this.gridMode = true;
this.columns = [];
this.waterfallRequestsAreStale = false;
this.waterfallScrollerWidthIsStale = true;
this.popupLinkifier = new Components.Linkifier.Linkifier();
this.calculatorsMap = new Map();
this.calculatorsMap.set(_calculatorTypes.Time, timeCalculator);
this.calculatorsMap.set(_calculatorTypes.Duration, durationCalculator);
this.lastWheelTime = 0;
this.setupDataGrid();
this.setupWaterfall();
ThemeSupport.ThemeSupport.instance().addEventListener(ThemeSupport.ThemeChangeEvent.eventName, () => {
this.scheduleRefresh();
});
}
private static convertToDataGridDescriptor(columnConfig: Descriptor): DataGrid.DataGrid.ColumnDescriptor {
const title = columnConfig.title instanceof Function ? columnConfig.title() : columnConfig.title;
return {
id: columnConfig.id,
title,
sortable: columnConfig.sortable,
align: columnConfig.align,
nonSelectable: columnConfig.nonSelectable,
weight: columnConfig.weight,
allowInSortByEvenWhenHidden: columnConfig.allowInSortByEvenWhenHidden,
} as DataGrid.DataGrid.ColumnDescriptor;
}
wasShown(): void {
this.updateRowsSize();
}
willHide(): void {
if (this.popoverHelper) {
this.popoverHelper.hidePopover();
}
}
reset(): void {
if (this.popoverHelper) {
this.popoverHelper.hidePopover();
}
this.eventDividers.clear();
}
private setupDataGrid(): void {
const defaultColumns = _defaultColumns;
const defaultColumnConfig = _defaultColumnConfig;
this.columns = ([] as Descriptor[]);
for (const currentConfigColumn of defaultColumns) {
const descriptor = Object.assign({}, defaultColumnConfig, currentConfigColumn);
const columnConfig = (descriptor as Descriptor);
columnConfig.id = columnConfig.id;
if (columnConfig.subtitle) {
const title = columnConfig.title instanceof Function ? columnConfig.title() : columnConfig.title;
const subtitle = columnConfig.subtitle instanceof Function ? columnConfig.subtitle() : columnConfig.subtitle;
columnConfig.titleDOMFragment = this.makeHeaderFragment(title, subtitle);
}
this.columns.push(columnConfig);
}
this.loadCustomColumnsAndSettings();
this.popoverHelper =
new UI.PopoverHelper.PopoverHelper(this.networkLogView.element, this.getPopoverRequest.bind(this));
this.popoverHelper.setHasPadding(true);
this.popoverHelper.setTimeout(300, 300);
this.dataGridInternal = new DataGrid.SortableDataGrid.SortableDataGrid<NetworkNode>(({
displayName: (i18nString(UIStrings.networkLog) as string),
columns: this.columns.map(NetworkLogViewColumns.convertToDataGridDescriptor),
editCallback: undefined,
deleteCallback: undefined,
refreshCallback: undefined,
}));
this.dataGridInternal.element.addEventListener('mousedown', event => {
if (!this.dataGridInternal.selectedNode && event.button) {
event.consume();
}
}, true);
this.dataGridScroller = (this.dataGridInternal.scrollContainer as HTMLDivElement);
this.updateColumns();
this.dataGridInternal.addEventListener(DataGrid.DataGrid.Events.SortingChanged, this.sortHandler, this);
this.dataGridInternal.setHeaderContextMenuCallback(this.innerHeaderContextMenu.bind(this));
this.activeWaterfallSortId = WaterfallSortIds.StartTime;
this.dataGridInternal.markColumnAsSortedBy(_initialSortColumn, DataGrid.DataGrid.Order.Ascending);
this.splitWidget = new UI.SplitWidget.SplitWidget(true, true, 'networkPanelSplitViewWaterfall', 200);
const widget = this.dataGridInternal.asWidget();
widget.setMinimumSize(150, 0);
this.splitWidget.setMainWidget(widget);
}
private setupWaterfall(): void {
this.waterfallColumn = new NetworkWaterfallColumn(this.networkLogView.calculator());
this.waterfallColumn.element.addEventListener('contextmenu', handleContextMenu.bind(this));
this.waterfallColumn.element.addEventListener('wheel', this.onMouseWheel.bind(this, false), {passive: true});
this.waterfallColumn.element.addEventListener('touchstart', this.onTouchStart.bind(this));
this.waterfallColumn.element.addEventListener('touchmove', this.onTouchMove.bind(this));
this.waterfallColumn.element.addEventListener('touchend', this.onTouchEnd.bind(this));
this.dataGridScroller.addEventListener('wheel', this.onMouseWheel.bind(this, true), true);
this.dataGridScroller.addEventListener('touchstart', this.onTouchStart.bind(this));
this.dataGridScroller.addEventListener('touchmove', this.onTouchMove.bind(this));
this.dataGridScroller.addEventListener('touchend', this.onTouchEnd.bind(this));
this.waterfallScroller =
(this.waterfallColumn.contentElement.createChild('div', 'network-waterfall-v-scroll') as HTMLDivElement);
this.waterfallScrollerContent =
(this.waterfallScroller.createChild('div', 'network-waterfall-v-scroll-content') as HTMLDivElement);
this.dataGridInternal.addEventListener(DataGrid.DataGrid.Events.PaddingChanged, () => {
this.waterfallScrollerWidthIsStale = true;
this.syncScrollers();
});
this.dataGridInternal.addEventListener(
DataGrid.ViewportDataGrid.Events.ViewportCalculated, this.redrawWaterfallColumn.bind(this));
this.createWaterfallHeader();
this.waterfallColumn.contentElement.classList.add('network-waterfall-view');
this.waterfallColumn.setMinimumSize(100, 0);
this.splitWidget.setSidebarWidget(this.waterfallColumn);
this.switchViewMode(false);
function handleContextMenu(this: NetworkLogViewColumns, ev: Event): void {
const event = (ev as MouseEvent);
const node = this.waterfallColumn.getNodeFromPoint(event.offsetX, event.offsetY);
if (!node) {
return;
}
const request = node.request();
if (!request) {
return;
}
const contextMenu = new UI.ContextMenu.ContextMenu(event);
this.networkLogView.handleContextMenuForRequest(contextMenu, request);
void contextMenu.show();
}
}
private onMouseWheel(shouldConsume: boolean, ev: Event): void {
if (shouldConsume) {
ev.consume(true);
}
const event = (ev as WheelEvent);
const hasRecentWheel = Date.now() - this.lastWheelTime < 80;
this.activeScroller.scrollBy({top: event.deltaY, behavior: hasRecentWheel ? 'auto' : 'smooth'});
this.syncScrollers();
this.lastWheelTime = Date.now();
}
private onTouchStart(ev: Event): void {
const event = (ev as TouchEvent);
this.hasScrollerTouchStarted = true;
this.scrollerTouchStartPos = event.changedTouches[0].pageY;
}
private onTouchMove(ev: Event): void {
if (!this.hasScrollerTouchStarted) {
return;
}
const event = (ev as TouchEvent);
const currentPos = event.changedTouches[0].pageY;
const delta = (this.scrollerTouchStartPos as number) - currentPos;
this.activeScroller.scrollBy({top: delta, behavior: 'auto'});
this.syncScrollers();
this.scrollerTouchStartPos = currentPos;
}
private onTouchEnd(): void {
this.hasScrollerTouchStarted = false;
}
private syncScrollers(): void {
if (!this.waterfallColumn.isShowing()) {
return;
}
this.waterfallScrollerContent.style.height =
this.dataGridScroller.scrollHeight - this.dataGridInternal.headerHeight() + 'px';
this.updateScrollerWidthIfNeeded();
this.dataGridScroller.scrollTop = this.waterfallScroller.scrollTop;
}
private updateScrollerWidthIfNeeded(): void {
if (this.waterfallScrollerWidthIsStale) {
this.waterfallScrollerWidthIsStale = false;
this.waterfallColumn.setRightPadding(
this.waterfallScroller.offsetWidth - this.waterfallScrollerContent.offsetWidth);
}
}
private redrawWaterfallColumn(): void {
if (!this.waterfallRequestsAreStale) {
this.updateScrollerWidthIfNeeded();
this.waterfallColumn.update(
this.activeScroller.scrollTop, this.eventDividersShown ? this.eventDividers : undefined);
return;
}
this.syncScrollers();
const nodes = this.networkLogView.flatNodesList();
this.waterfallColumn.update(this.activeScroller.scrollTop, this.eventDividers, nodes);
}
private createWaterfallHeader(): void {
this.waterfallHeaderElement =
(this.waterfallColumn.contentElement.createChild('div', 'network-waterfall-header') as HTMLElement);
this.waterfallHeaderElement.addEventListener('click', waterfallHeaderClicked.bind(this));
this.waterfallHeaderElement.addEventListener(
'contextmenu', event => this.innerHeaderContextMenu(new UI.ContextMenu.ContextMenu(event)));
const innerElement = this.waterfallHeaderElement.createChild('div');
innerElement.textContent = i18nString(UIStrings.waterfall);
this.waterfallColumnSortIcon = UI.Icon.Icon.create('', 'sort-order-icon');
this.waterfallHeaderElement.createChild('div', 'sort-order-icon-container')
.appendChild(this.waterfallColumnSortIcon);
function waterfallHeaderClicked(this: NetworkLogViewColumns): void {
const sortOrders = DataGrid.DataGrid.Order;
const wasSortedByWaterfall = this.dataGridInternal.sortColumnId() === 'waterfall';
const wasSortedAscending = this.dataGridInternal.isSortOrderAscending();
const sortOrder = wasSortedByWaterfall && wasSortedAscending ? sortOrders.Descending : sortOrders.Ascending;
this.dataGridInternal.markColumnAsSortedBy('waterfall', sortOrder);
this.sortHandler();
}
}
setCalculator(x: NetworkTimeCalculator): void {
this.waterfallColumn.setCalculator(x);
}
scheduleRefresh(): void {
this.waterfallColumn.scheduleDraw();
}
private updateRowsSize(): void {
const largeRows = Boolean(this.networkLogLargeRowsSetting.get());
this.dataGridInternal.element.classList.toggle('small', !largeRows);
this.dataGridInternal.scheduleUpdate();
this.waterfallScrollerWidthIsStale = true;
this.waterfallColumn.setRowHeight(largeRows ? 41 : 21);
this.waterfallScroller.classList.toggle('small', !largeRows);
this.waterfallHeaderElement.classList.toggle('small', !largeRows);
// Request an animation frame because under certain conditions
// (see crbug.com/1019723) this.waterfallScroller.offsetTop does
// not return the value it's supposed to return as of the applied
// css classes.
window.requestAnimationFrame(() => {
this.waterfallColumn.setHeaderHeight(this.waterfallScroller.offsetTop);
this.waterfallColumn.scheduleDraw();
});
}
show(element: Element): void {
this.splitWidget.show(element);
}
setHidden(value: boolean): void {
UI.ARIAUtils.setHidden(this.splitWidget.element, value);
}
dataGrid(): DataGrid.SortableDataGrid.SortableDataGrid<NetworkNode> {
return this.dataGridInternal;
}
sortByCurrentColumn(): void {
this.sortHandler();
}
private sortHandler(): void {
const columnId = this.dataGridInternal.sortColumnId();
this.networkLogView.removeAllNodeHighlights();
this.waterfallRequestsAreStale = true;
if (columnId === 'waterfall') {
if (this.dataGridInternal.sortOrder() === DataGrid.DataGrid.Order.Ascending) {
this.waterfallColumnSortIcon.setIconType('smallicon-triangle-up');
} else {
this.waterfallColumnSortIcon.setIconType('smallicon-triangle-down');
}
const sortFunction =
(NetworkRequestNode.RequestPropertyComparator.bind(null, this.activeWaterfallSortId) as
(arg0: DataGrid.SortableDataGrid.SortableDataGridNode<NetworkNode>,
arg1: DataGrid.SortableDataGrid.SortableDataGridNode<NetworkNode>) => number);
this.dataGridInternal.sortNodes(sortFunction, !this.dataGridInternal.isSortOrderAscending());
this.dataGridSortedForTest();
return;
}
this.waterfallColumnSortIcon.setIconType('');
const columnConfig = this.columns.find(columnConfig => columnConfig.id === columnId);
if (!columnConfig || !columnConfig.sortingFunction) {
return;
}
const sortingFunction =
(columnConfig.sortingFunction as
((arg0: DataGrid.SortableDataGrid.SortableDataGridNode<NetworkNode>,
arg1: DataGrid.SortableDataGrid.SortableDataGridNode<NetworkNode>) => number) |
undefined);
if (!sortingFunction) {
return;
}
this.dataGridInternal.sortNodes(sortingFunction, !this.dataGridInternal.isSortOrderAscending());
this.dataGridSortedForTest();
}
private dataGridSortedForTest(): void {
}
private updateColumns(): void {
if (!this.dataGridInternal) {
return;
}
const visibleColumns = new Set<string>();
if (this.gridMode) {
for (const columnConfig of this.columns) {
if (columnConfig.visible) {
visibleColumns.add(columnConfig.id);
}
}
} else {
// Find the first visible column from the path group
const visibleColumn = this.columns.find(c => c.hideableGroup === 'path' && c.visible);
if (visibleColumn) {
visibleColumns.add(visibleColumn.id);
} else {
// This should not happen because inside a hideableGroup
// there should always be at least one column visible
// This is just in case.
visibleColumns.add('name');
}
}
this.dataGridInternal.setColumnsVisiblity(visibleColumns);
}
switchViewMode(gridMode: boolean): void {
if (this.gridMode === gridMode) {
return;
}
this.gridMode = gridMode;
if (gridMode) {
this.splitWidget.showBoth();
this.activeScroller = this.waterfallScroller;
this.waterfallScroller.scrollTop = this.dataGridScroller.scrollTop;
this.dataGridInternal.setScrollContainer(this.waterfallScroller);
} else {
this.networkLogView.removeAllNodeHighlights();
this.splitWidget.hideSidebar();
this.activeScroller = this.dataGridScroller;
this.dataGridInternal.setScrollContainer(this.dataGridScroller);
}
this.networkLogView.element.classList.toggle('brief-mode', !gridMode);
this.updateColumns();
this.updateRowsSize();
}
private toggleColumnVisibility(columnConfig: Descriptor): void {
this.loadCustomColumnsAndSettings();
columnConfig.visible = !columnConfig.visible;
this.saveColumnsSettings();
this.updateColumns();
}
private saveColumnsSettings(): void {
const saveableSettings: {
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[x: string]: any,
} = {};
for (const columnConfig of this.columns) {
saveableSettings[columnConfig.id] = {visible: columnConfig.visible, title: columnConfig.title};
}
this.persistantSettings.set(saveableSettings);
}
private loadCustomColumnsAndSettings(): void {
const savedSettings = this.persistantSettings.get();
const columnIds = Object.keys(savedSettings);
for (const columnId of columnIds) {
const setting = savedSettings[columnId];
let columnConfig = this.columns.find(columnConfig => columnConfig.id === columnId);
if (!columnConfig) {
columnConfig = this.addCustomHeader(setting.title, columnId) || undefined;
}
if (columnConfig && columnConfig.hideable && typeof setting.visible === 'boolean') {
columnConfig.visible = Boolean(setting.visible);
}
if (columnConfig && typeof setting.title === 'string') {
columnConfig.title = setting.title;
}
}
}
private makeHeaderFragment(title: string, subtitle: string): DocumentFragment {
const fragment = document.createDocumentFragment();
UI.UIUtils.createTextChild(fragment, title);
const subtitleDiv = fragment.createChild('div', 'network-header-subtitle');
UI.UIUtils.createTextChild(subtitleDiv, subtitle);
return fragment;
}
private innerHeaderContextMenu(contextMenu: UI.ContextMenu.SubMenu): void {
const columnConfigs = this.columns.filter(columnConfig => columnConfig.hideable);
const nonResponseHeaders = columnConfigs.filter(columnConfig => !columnConfig.isResponseHeader);
const hideableGroups = new Map<string, Descriptor[]>();
const nonResponseHeadersWithoutGroup: Descriptor[] = [];
// Sort columns into their groups
for (const columnConfig of nonResponseHeaders) {
if (!columnConfig.hideableGroup) {
nonResponseHeadersWithoutGroup.push(columnConfig);
} else {
const name = columnConfig.hideableGroup;
let hideableGroup = hideableGroups.get(name);
if (!hideableGroup) {
hideableGroup = [];
hideableGroups.set(name, hideableGroup);
}
hideableGroup.push(columnConfig);
}
}
// Add all the groups first
for (const group of hideableGroups.values()) {
const visibleColumns = group.filter(columnConfig => columnConfig.visible);
for (const columnConfig of group) {
// Make sure that at least one item in every group is enabled
const isDisabled = visibleColumns.length === 1 && visibleColumns[0] === columnConfig;
const title = columnConfig.title instanceof Function ? columnConfig.title() : columnConfig.title;
contextMenu.headerSection().appendCheckboxItem(
title, this.toggleColumnVisibility.bind(this, columnConfig), columnConfig.visible, isDisabled);
}
contextMenu.headerSection().appendSeparator();
}
// Add normal columns not belonging to any group
for (const columnConfig of nonResponseHeadersWithoutGroup) {
const title = columnConfig.title instanceof Function ? columnConfig.title() : columnConfig.title;
contextMenu.headerSection().appendCheckboxItem(
title, this.toggleColumnVisibility.bind(this, columnConfig), columnConfig.visible);
}
const responseSubMenu = contextMenu.footerSection().appendSubMenuItem(i18nString(UIStrings.responseHeaders));
const responseHeaders = columnConfigs.filter(columnConfig => columnConfig.isResponseHeader);
for (const columnConfig of responseHeaders) {
const title = columnConfig.title instanceof Function ? columnConfig.title() : columnConfig.title;
responseSubMenu.defaultSection().appendCheckboxItem(
title, this.toggleColumnVisibility.bind(this, columnConfig), columnConfig.visible);
}
responseSubMenu.footerSection().appendItem(
i18nString(UIStrings.manageHeaderColumns), this.manageCustomHeaderDialog.bind(this));
const waterfallSortIds = WaterfallSortIds;
const waterfallSubMenu = contextMenu.footerSection().appendSubMenuItem(i18nString(UIStrings.waterfall));
waterfallSubMenu.defaultSection().appendCheckboxItem(
i18nString(UIStrings.startTime), setWaterfallMode.bind(this, waterfallSortIds.StartTime),
this.activeWaterfallSortId === waterfallSortIds.StartTime);
waterfallSubMenu.defaultSection().appendCheckboxItem(
i18nString(UIStrings.responseTime), setWaterfallMode.bind(this, waterfallSortIds.ResponseTime),
this.activeWaterfallSortId === waterfallSortIds.ResponseTime);
waterfallSubMenu.defaultSection().appendCheckboxItem(
i18nString(UIStrings.endTime), setWaterfallMode.bind(this, waterfallSortIds.EndTime),
this.activeWaterfallSortId === waterfallSortIds.EndTime);
waterfallSubMenu.defaultSection().appendCheckboxItem(
i18nString(UIStrings.totalDuration), setWaterfallMode.bind(this, waterfallSortIds.Duration),
this.activeWaterfallSortId === waterfallSortIds.Duration);
waterfallSubMenu.defaultSection().appendCheckboxItem(
i18nString(UIStrings.latency), setWaterfallMode.bind(this, waterfallSortIds.Latency),
this.activeWaterfallSortId === waterfallSortIds.Latency);
function setWaterfallMode(this: NetworkLogViewColumns, sortId: WaterfallSortIds): void {
let calculator = this.calculatorsMap.get(_calculatorTypes.Time);
const waterfallSortIds = WaterfallSortIds;
if (sortId === waterfallSortIds.Duration || sortId === waterfallSortIds.Latency) {
calculator = this.calculatorsMap.get(_calculatorTypes.Duration);
}
this.networkLogView.setCalculator((calculator as NetworkTimeCalculator));
this.activeWaterfallSortId = sortId;
this.dataGridInternal.markColumnAsSortedBy('waterfall', DataGrid.DataGrid.Order.Ascending);
this.sortHandler();
}
}
private manageCustomHeaderDialog(): void {
const customHeaders = [];
for (const columnConfig of this.columns) {
const title = columnConfig.title instanceof Function ? columnConfig.title() : columnConfig.title;
if (columnConfig.isResponseHeader) {
customHeaders.push({title, editable: columnConfig.isCustomHeader});
}
}
const manageCustomHeaders = new NetworkManageCustomHeadersView(
customHeaders, headerTitle => Boolean(this.addCustomHeader(headerTitle)), this.changeCustomHeader.bind(this),
this.removeCustomHeader.bind(this));
const dialog = new UI.Dialog.Dialog();
manageCustomHeaders.show(dialog.contentElement);
dialog.setSizeBehavior(UI.GlassPane.SizeBehavior.MeasureContent);
// @ts-ignore
// TypeScript somehow tries to appy the `WidgetElement` class to the
// `Document` type of the (Document|Element) union. WidgetElement inherits
// from HTMLElement so its valid to be passed here.
dialog.show(this.networkLogView.element);
}
private removeCustomHeader(headerId: string): boolean {
headerId = headerId.toLowerCase();
const index = this.columns.findIndex(columnConfig => columnConfig.id === headerId);
if (index === -1) {
return false;
}
this.columns.splice(index, 1);
this.dataGridInternal.removeColumn(headerId);
this.saveColumnsSettings();
this.updateColumns();
return true;
}
private addCustomHeader(headerTitle: string, headerId?: string, index?: number): Descriptor|null {
if (!headerId) {
headerId = headerTitle.toLowerCase();
}
if (index === undefined) {
index = this.columns.length - 1;
}
const currentColumnConfig = this.columns.find(columnConfig => columnConfig.id === headerId);
if (currentColumnConfig) {
return null;
}
const columnConfigBase = Object.assign({}, _defaultColumnConfig, {
id: headerId,
title: headerTitle,
isResponseHeader: true,
isCustomHeader: true,
visible: true,
sortingFunction: NetworkRequestNode.ResponseHeaderStringComparator.bind(null, headerId),
});
// Split out the column config from the typed version, as doing it in a single assignment causes
// issues with Closure compiler.
const columnConfig = (columnConfigBase as Descriptor);
this.columns.splice(index, 0, columnConfig);
if (this.dataGridInternal) {
this.dataGridInternal.addColumn(NetworkLogViewColumns.convertToDataGridDescriptor(columnConfig), index);
}
this.saveColumnsSettings();
this.updateColumns();
return columnConfig;
}
private changeCustomHeader(oldHeaderId: string, newHeaderTitle: string, newHeaderId?: string): boolean {
if (!newHeaderId) {
newHeaderId = newHeaderTitle.toLowerCase();
}
oldHeaderId = oldHeaderId.toLowerCase();
const oldIndex = this.columns.findIndex(columnConfig => columnConfig.id === oldHeaderId);
const oldColumnConfig = this.columns[oldIndex];
const currentColumnConfig = this.columns.find(columnConfig => columnConfig.id === newHeaderId);
if (!oldColumnConfig || (currentColumnConfig && oldHeaderId !== newHeaderId)) {
return false;
}
this.removeCustomHeader(oldHeaderId);
this.addCustomHeader(newHeaderTitle, newHeaderId, oldIndex);
return true;
}
private getPopoverRequest(event: Event): UI.PopoverHelper.PopoverRequest|null {
if (!this.gridMode) {
return null;
}
const hoveredNode = this.networkLogView.hoveredNode();
if (!hoveredNode || !event.target) {
return null;
}
const anchor = (event.target as HTMLElement).enclosingNodeOrSelfWithClass('network-script-initiated');
if (!anchor) {
return null;
}
const request = hoveredNode.request();
if (!request) {
return null;
}
return {
box: anchor.boxInWindow(),
show: async(popover: UI.GlassPane.GlassPane): Promise<boolean> => {
this.popupLinkifier.setLiveLocationUpdateCallback(() => {
popover.setSizeBehavior(UI.GlassPane.SizeBehavior.MeasureContent);
});
const content = RequestInitiatorView.createStackTracePreview(
(request as SDK.NetworkRequest.NetworkRequest), this.popupLinkifier, false);
if (!content) {
return false;
}
popover.contentElement.appendChild(content.element);
return true;
},
hide: this.popupLinkifier.reset.bind(this.popupLinkifier),
};
}
addEventDividers(times: number[], className: string): void {
// TODO(allada) Remove this and pass in the color.
let color = 'transparent';
switch (className) {
case 'network-dcl-divider':
color = '#0867CB';
break;
case 'network-load-divider':
color = '#B31412';
break;
default:
return;
}
const currentTimes = this.eventDividers.get(color) || [];
this.eventDividers.set(color, currentTimes.concat(times));
this.networkLogView.scheduleRefresh();
}
hideEventDividers(): void {
this.eventDividersShown = true;
this.redrawWaterfallColumn();
}
showEventDividers(): void {
this.eventDividersShown = false;
this.redrawWaterfallColumn();
}
selectFilmStripFrame(time: number): void {
this.eventDividers.set(_filmStripDividerColor, [time]);
this.redrawWaterfallColumn();
}
clearFilmStripFrame(): void {
this.eventDividers.delete(_filmStripDividerColor);
this.redrawWaterfallColumn();
}
}
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
// eslint-disable-next-line @typescript-eslint/naming-convention
export const _initialSortColumn = 'waterfall';
// TODO(crbug.com/1167717): Make this a const enum again
// eslint-disable-next-line rulesdir/const_enum, @typescript-eslint/naming-convention
export enum _calculatorTypes {
Duration = 'Duration',
Time = 'Time',
}
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
// eslint-disable-next-line @typescript-eslint/naming-convention
export const _defaultColumnConfig: Object = {
subtitle: null,
visible: false,
weight: 6,
sortable: true,
hideable: true,
hideableGroup: null,
nonSelectable: false,
isResponseHeader: false,
isCustomHeader: false,
allowInSortByEvenWhenHidden: false,
};
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
// eslint-disable-next-line @typescript-eslint/naming-convention
const _temporaryDefaultColumns = [
{
id: 'name',
title: i18nLazyString(UIStrings.name),
subtitle: i18nLazyString(UIStrings.path),
visible: true,
weight: 20,
hideable: true,
hideableGroup: 'path',
sortingFunction: NetworkRequestNode.NameComparator,
},
{
id: 'path',
title: i18nLazyString(UIStrings.path),
hideable: true,
hideableGroup: 'path',
sortingFunction: NetworkRequestNode.RequestPropertyComparator.bind(null, 'pathname'),
},
{
id: 'url',
title: i18nLazyString(UIStrings.url),
hideable: true,
hideableGroup: 'path',
sortingFunction: NetworkRequestNode.RequestURLComparator,
},
{
id: 'method',
title: i18nLazyString(UIStrings.method),
sortingFunction: NetworkRequestNode.RequestPropertyComparator.bind(null, 'requestMethod'),
},
{
id: 'status',
title: i18nLazyString(UIStrings.status),
visible: true,
subtitle: i18nLazyString(UIStrings.text),
sortingFunction: NetworkRequestNode.RequestPropertyComparator.bind(null, 'statusCode'),
},
{
id: 'protocol',
title: i18nLazyString(UIStrings.protocol),
sortingFunction: NetworkRequestNode.RequestPropertyComparator.bind(null, 'protocol'),
},
{
id: 'scheme',
title: i18nLazyString(UIStrings.scheme),
sortingFunction: NetworkRequestNode.RequestPropertyComparator.bind(null, 'scheme'),
},
{
id: 'domain',
title: i18nLazyString(UIStrings.domain),
sortingFunction: NetworkRequestNode.RequestPropertyComparator.bind(null, 'domain'),
},
{
id: 'remoteaddress',
title: i18nLazyString(UIStrings.remoteAddress),
weight: 10,
align: DataGrid.DataGrid.Align.Right,
sortingFunction: NetworkRequestNode.RemoteAddressComparator,
},
{
id: 'remoteaddress-space',
title: i18nLazyString(UIStrings.remoteAddressSpace),
visible: false,
weight: 10,
sortingFunction: NetworkRequestNode.RemoteAddressSpaceComparator,
},
{
id: 'type',
title: i18nLazyString(UIStrings.type),
visible: true,
sortingFunction: NetworkRequestNode.TypeComparator,
},
{
id: 'initiator',
title: i18nLazyString(UIStrings.initiator),
visible: true,
weight: 10,
sortingFunction: NetworkRequestNode.InitiatorComparator,
},
{
id: 'initiator-address-space',
title: i18nLazyString(UIStrings.initiatorAddressSpace),
visible: false,
weight: 10,
sortingFunction: NetworkRequestNode.InitiatorAddressSpaceComparator,
},
{
id: 'cookies',
title: i18nLazyString(UIStrings.cookies),
align: DataGrid.DataGrid.Align.Right,
sortingFunction: NetworkRequestNode.RequestCookiesCountComparator,
},
{
id: 'setcookies',
title: i18nLazyString(UIStrings.setCookies),
align: DataGrid.DataGrid.Align.Right,
sortingFunction: NetworkRequestNode.ResponseCookiesCountComparator,
},
{
id: 'size',
title: i18nLazyString(UIStrings.size),
visible: true,
subtitle: i18nLazyString(UIStrings.content),
align: DataGrid.DataGrid.Align.Right,
sortingFunction: NetworkRequestNode.SizeComparator,
},
{
id: 'time',
title: i18nLazyString(UIStrings.time),
visible: true,
subtitle: i18nLazyString(UIStrings.latency),
align: DataGrid.DataGrid.Align.Right,
sortingFunction: NetworkRequestNode.RequestPropertyComparator.bind(null, 'duration'),
},
{id: 'priority', title: i18nLazyString(UIStrings.priority), sortingFunction: NetworkRequestNode.PriorityComparator},
{
id: 'connectionid',
title: i18nLazyString(UIStrings.connectionId),
sortingFunction: NetworkRequestNode.RequestPropertyComparator.bind(null, 'connectionId'),
},
{
id: 'cache-control',
isResponseHeader: true,
title: i18n.i18n.lockedLazyString('Cache-Control'),
sortingFunction: NetworkRequestNode.ResponseHeaderStringComparator.bind(null, 'cache-control'),
},
{
id: 'connection',
isResponseHeader: true,
title: i18n.i18n.lockedLazyString('Connection'),
sortingFunction: NetworkRequestNode.ResponseHeaderStringComparator.bind(null, 'connection'),
},
{
id: 'content-encoding',
isResponseHeader: true,
title: i18n.i18n.lockedLazyString('Content-Encoding'),
sortingFunction: NetworkRequestNode.ResponseHeaderStringComparator.bind(null, 'content-encoding'),
},
{
id: 'content-length',
isResponseHeader: true,
title: i18n.i18n.lockedLazyString('Content-Length'),
align: DataGrid.DataGrid.Align.Right,
sortingFunction: NetworkRequestNode.ResponseHeaderNumberComparator.bind(null, 'content-length'),
},
{
id: 'etag',
isResponseHeader: true,
title: i18n.i18n.lockedLazyString('ETag'),
sortingFunction: NetworkRequestNode.ResponseHeaderStringComparator.bind(null, 'etag'),
},
{
id: 'keep-alive',
isResponseHeader: true,
title: i18n.i18n.lockedLazyString('Keep-Alive'),
sortingFunction: NetworkRequestNode.ResponseHeaderStringComparator.bind(null, 'keep-alive'),
},
{
id: 'last-modified',
isResponseHeader: true,
title: i18n.i18n.lockedLazyString('Last-Modified'),
sortingFunction: NetworkRequestNode.ResponseHeaderDateComparator.bind(null, 'last-modified'),
},
{
id: 'server',
isResponseHeader: true,
title: i18n.i18n.lockedLazyString('Server'),
sortingFunction: NetworkRequestNode.ResponseHeaderStringComparator.bind(null, 'server'),
},
{
id: 'vary',
isResponseHeader: true,
title: i18n.i18n.lockedLazyString('Vary'),
sortingFunction: NetworkRequestNode.ResponseHeaderStringComparator.bind(null, 'vary'),
},
// This header is a placeholder to let datagrid know that it can be sorted by this column, but never shown.
{
id: 'waterfall',
title: i18nLazyString(UIStrings.waterfall),
visible: false,
hideable: false,
allowInSortByEvenWhenHidden: true,
},
];
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-explicit-any
const _defaultColumns = (_temporaryDefaultColumns as any);
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
// eslint-disable-next-line @typescript-eslint/naming-convention
export const _filmStripDividerColor = '#fccc49';
// TODO(crbug.com/1167717): Make this a const enum again
// eslint-disable-next-line rulesdir/const_enum
export enum WaterfallSortIds {
StartTime = 'startTime',
ResponseTime = 'responseReceivedTime',
EndTime = 'endTime',
Duration = 'duration',
Latency = 'latency',
}
export interface Descriptor {
id: string;
title: string|(() => string);
titleDOMFragment?: DocumentFragment;
subtitle: string|(() => string)|null;
visible: boolean;
weight: number;
hideable: boolean;
hideableGroup: string|null;
nonSelectable: boolean;
sortable: boolean;
align?: string|null;
isResponseHeader: boolean;
sortingFunction: (arg0: NetworkNode, arg1: NetworkNode) => number | undefined;
isCustomHeader: boolean;
allowInSortByEvenWhenHidden: boolean;
}