UNPKG

chrome-devtools-frontend

Version:
341 lines (296 loc) • 13 kB
// Copyright 2025 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /* eslint-disable @devtools/no-imperative-dom-api */ 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 type * as SDK from '../../core/sdk/sdk.js'; import * as TextUtils from '../../core/text_utils/text_utils.js'; import * as DataGrid from '../../ui/legacy/components/data_grid/data_grid.js'; import * as SourceFrame from '../../ui/legacy/components/source_frame/source_frame.js'; import * as UI from '../../ui/legacy/legacy.js'; import type {BinaryResourceView} from './BinaryResourceView.js'; import viewStyles from './originalResourceChunkView.css.js'; const UIStrings = { /** * @description Text in Event Source Messages View of the Network panel */ data: 'Data', /** * @description Text in Messages View of the Network panel */ length: 'Length', /** * @description Text that refers to the time */ time: 'Time', /** * @description Text to clear everything */ clearAll: 'Clear All', /** * @description Text to filter result items */ filter: 'Filter', /** * @description Text in Messages View of the Network panel that shows if no message is selected for viewing its content */ noMessageSelected: 'No message selected', /** * @description Text in Messages View of the Network panel */ selectMessageToBrowseItsContent: 'Select message to browse its content.', /** * @description Text in Messages View of the Network panel */ copyMessageD: 'Copy message…', /** * @description A context menu item in the Messages View of the Network panel */ copyMessage: 'Copy message', /** * @description Text for everything */ all: 'All', /** * @description Text in Messages View of the Network panel */ send: 'Send', /** * @description Text in Messages View of the Network panel */ receive: 'Receive', } as const; const str_ = i18n.i18n.registerUIStrings('panels/network/OriginalResourceChunkView.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); const i18nLazyString = i18n.i18n.getLazilyComputedLocalizedString.bind(undefined, str_); export abstract class OriginalResourceChunkView<Chunk> extends UI.Widget.VBox { private readonly splitWidget: UI.SplitWidget.SplitWidget; private dataGrid: DataGrid.SortableDataGrid.SortableDataGrid<unknown>; private readonly timeComparator: (arg0: DataGrid.SortableDataGrid.SortableDataGridNode<DataGridItem>, arg1: DataGrid.SortableDataGrid.SortableDataGridNode<DataGridItem>) => number; private readonly mainToolbar: UI.Toolbar.Toolbar; private readonly clearAllButton: UI.Toolbar.ToolbarButton; private readonly filterTypeCombobox: UI.Toolbar.ToolbarComboBox; protected filterType: string|null = null; private readonly filterTextInput: UI.Toolbar.ToolbarInput; protected filterRegex: RegExp|null = null; private readonly frameEmptyWidget: UI.EmptyWidget.EmptyWidget; private currentSelectedNode?: DataGridItem|null; readonly request: SDK.NetworkRequest.NetworkRequest; private readonly messageFilterSetting: Common.Settings.Setting<string>; private sidebarWidget: UI.Widget.Widget|null = null; abstract getRequestChunks(): Chunk[]; abstract createGridItem(chunk: Chunk): DataGridItem; abstract chunkFilter(chunk: Chunk): boolean; constructor(request: SDK.NetworkRequest.NetworkRequest, messageFilterSettingKey: string, splitWidgetSettingKey: string, dataGridDisplayName: Common.UIString.LocalizedString, filterUsingRegexHint: Common.UIString.LocalizedString) { super(); this.messageFilterSetting = Common.Settings.Settings.instance().createSetting(messageFilterSettingKey, ''); this.registerRequiredCSS(viewStyles); this.request = request; this.element.classList.add('resource-chunk-view'); this.splitWidget = new UI.SplitWidget.SplitWidget(false, true, splitWidgetSettingKey); this.splitWidget.show(this.element); const columns: DataGrid.DataGrid.ColumnDescriptor[] = this.getColumns(); this.dataGrid = new DataGrid.SortableDataGrid.SortableDataGrid({ displayName: dataGridDisplayName, columns, }); this.dataGrid.setRowContextMenuCallback((menu, node) => this.onRowContextMenu(menu, node)); this.dataGrid.setEnableAutoScrollToBottom(true); this.dataGrid.setCellClass('resource-chunk-view-td'); this.timeComparator = (resourceChunkNodeTimeComparator as (arg0: DataGrid.SortableDataGrid.SortableDataGridNode<DataGridItem>, arg1: DataGrid.SortableDataGrid.SortableDataGridNode<DataGridItem>) => number); this.dataGrid.sortNodes(this.timeComparator, false); this.dataGrid.markColumnAsSortedBy('time', DataGrid.DataGrid.Order.Ascending); this.dataGrid.addEventListener(DataGrid.DataGrid.Events.SORTING_CHANGED, this.sortItems, this); this.dataGrid.setName(splitWidgetSettingKey + '_datagrid'); this.dataGrid.addEventListener(DataGrid.DataGrid.Events.SELECTED_NODE, event => { void this.onChunkSelected(event); }, this); this.dataGrid.addEventListener(DataGrid.DataGrid.Events.DESELECTED_NODE, this.onChunkDeselected, this); this.mainToolbar = document.createElement('devtools-toolbar'); this.clearAllButton = new UI.Toolbar.ToolbarButton(i18nString(UIStrings.clearAll), 'clear'); this.clearAllButton.addEventListener(UI.Toolbar.ToolbarButton.Events.CLICK, this.clearChunks, this); this.mainToolbar.appendToolbarItem(this.clearAllButton); this.filterTypeCombobox = new UI.Toolbar.ToolbarComboBox(this.onFilterTypeChanged.bind(this), i18nString(UIStrings.filter)); for (const filterItem of FILTER_TYPES) { const option = this.filterTypeCombobox.createOption(filterItem.label(), filterItem.name); this.filterTypeCombobox.addOption(option); } this.mainToolbar.appendToolbarItem(this.filterTypeCombobox); this.filterTextInput = new UI.Toolbar.ToolbarFilter(filterUsingRegexHint, 0.4); this.filterTextInput.addEventListener(UI.Toolbar.ToolbarInput.Event.TEXT_CHANGED, this.onFilterTextChanged, this); const filter = this.messageFilterSetting.get(); if (filter) { this.filterTextInput.setValue(filter); } this.mainToolbar.appendToolbarItem(this.filterTextInput); const mainContainer = new UI.Widget.VBox(); mainContainer.element.appendChild(this.mainToolbar); this.dataGrid.asWidget().show(mainContainer.element); mainContainer.setMinimumSize(0, 72); this.splitWidget.setMainWidget(mainContainer); this.frameEmptyWidget = new UI.EmptyWidget.EmptyWidget(i18nString(UIStrings.noMessageSelected), i18nString(UIStrings.selectMessageToBrowseItsContent)); this.sidebarWidget = this.frameEmptyWidget; this.splitWidget.setSidebarWidget(this.sidebarWidget); if (filter) { this.applyFilter(filter); } } private onRowContextMenu(contextMenu: UI.ContextMenu.ContextMenu, node: DataGrid.DataGrid.DataGridNode<unknown>): void { const item = node as DataGridItem; const binaryView = item.binaryView(); if (binaryView) { binaryView.addCopyToContextMenu(contextMenu, i18nString(UIStrings.copyMessageD)); } else { const dataVal = item.data.data; const textToCopy = typeof dataVal === 'string' ? dataVal : item.dataText(); contextMenu.clipboardSection().appendItem( i18nString(UIStrings.copyMessage), Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText.bind( Host.InspectorFrontendHost.InspectorFrontendHostInstance, textToCopy), {jslogContext: 'copy'}); } contextMenu.footerSection().appendItem(i18nString(UIStrings.clearAll), this.clearChunks.bind(this), {jslogContext: 'clear-all'}); } getColumns(): DataGrid.DataGrid.ColumnDescriptor[] { return [ {id: 'data', title: i18nString(UIStrings.data), sortable: false, weight: 88}, { id: 'length', title: i18nString(UIStrings.length), sortable: false, align: DataGrid.DataGrid.Align.RIGHT, weight: 5, }, {id: 'time', title: i18nString(UIStrings.time), sortable: true, weight: 7}, ]; } chunkAdded(chunk: Chunk): void { if (!this.chunkFilter(chunk)) { return; } this.dataGrid.insertChild(this.createGridItem(chunk)); } private clearChunks(): void { // TODO(allada): actually remove frames from request. clearChunkOffsets.set(this.request, this.getRequestChunks().length); this.performUpdate(); } private onFilterTypeChanged(): void { const val = (this.filterTypeCombobox.selectedOption() as HTMLOptionElement).value; this.filterType = val === 'all' ? null : val; this.performUpdate(); } private onFilterTextChanged(): void { const text = this.filterTextInput.value(); this.messageFilterSetting.set(text); this.applyFilter(text); } private applyFilter(text: string): void { if (text) { try { this.filterRegex = new RegExp(text, 'i'); } catch { this.filterRegex = new RegExp(Platform.StringUtilities.escapeForRegExp(text), 'i'); } } else { this.filterRegex = null; } this.performUpdate(); } private async onChunkSelected(event: Common.EventTarget.EventTargetEvent<DataGrid.DataGrid.DataGridNode<unknown>>): Promise<void> { this.currentSelectedNode = (event.data as DataGridItem); await this.updateSidebar(); } private onChunkDeselected(): void { this.currentSelectedNode = null; void this.updateSidebar(); } async updateSidebar(): Promise<void> { if (!this.currentSelectedNode) { this.sidebarWidget = null; this.updateSidebarWidget(); return; } const content = this.currentSelectedNode.dataText(); const binaryView = this.currentSelectedNode.binaryView(); if (binaryView) { this.sidebarWidget = binaryView; this.updateSidebarWidget(); return; } const jsonView = await SourceFrame.JSONView.JSONView.createView(content); if (jsonView) { this.sidebarWidget = jsonView; this.updateSidebarWidget(); return; } this.sidebarWidget = new SourceFrame.ResourceSourceFrame.ResourceSourceFrame( TextUtils.StaticContentProvider.StaticContentProvider.fromString(this.request.url(), this.request.resourceType(), content), ''); this.updateSidebarWidget(); } private updateSidebarWidget(): void { const activeSidebar = this.sidebarWidget || this.frameEmptyWidget; if (this.splitWidget.sidebarWidget() !== activeSidebar) { this.splitWidget.setSidebarWidget(activeSidebar); } } override performUpdate(): void { this.dataGrid.rootNode().removeChildren(); let chunks = this.getRequestChunks(); const offset = clearChunkOffsets.get(this.request) || 0; chunks = chunks.slice(offset).filter(this.chunkFilter.bind(this)); chunks.forEach(chunk => this.dataGrid.insertChild(this.createGridItem(chunk))); this.updateSidebarWidget(); } refresh(): void { this.performUpdate(); } private sortItems(): void { this.dataGrid.sortNodes(this.timeComparator, !this.dataGrid.isSortOrderAscending()); } getDataGridForTest(): DataGrid.SortableDataGrid.SortableDataGrid<unknown> { return this.dataGrid; } getSplitWidgetForTest(): UI.SplitWidget.SplitWidget { return this.splitWidget; } getFilterInputForTest(): UI.Toolbar.ToolbarInput { return this.filterTextInput; } getClearAllButtonForTest(): UI.Toolbar.ToolbarButton { return this.clearAllButton; } getFilterTypeComboboxForTest(): UI.Toolbar.ToolbarComboBox { return this.filterTypeCombobox; } } const FILTER_TYPES: UI.FilterBar.Item[] = [ {name: 'all', label: i18nLazyString(UIStrings.all), jslogContext: 'all'}, {name: 'send', label: i18nLazyString(UIStrings.send), jslogContext: 'send'}, {name: 'receive', label: i18nLazyString(UIStrings.receive), jslogContext: 'receive'}, ]; export abstract class DataGridItem extends DataGrid.SortableDataGrid.SortableDataGridNode<unknown> { abstract binaryView(): BinaryResourceView|null; abstract getTime(): number; abstract dataText(): string; abstract isTextFrame?: boolean; } function resourceChunkNodeTimeComparator(a: DataGridItem, b: DataGridItem): number { return a.getTime() - b.getTime(); } const clearChunkOffsets = new WeakMap<SDK.NetworkRequest.NetworkRequest, number>();