| // Copyright (c) 2020 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 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 * as TextUtils from '../../models/text_utils/text_utils.js'; |
| import * as DataGrid from '../../ui/legacy/components/data_grid/data_grid.js'; |
| import * as UI from '../../ui/legacy/legacy.js'; |
| |
| import developerResourcesListViewStyles from './developerResourcesListView.css.js'; |
| |
| const UIStrings = { |
| /** |
| *@description Text for the status of something |
| */ |
| status: 'Status', |
| /** |
| *@description Text for web URLs |
| */ |
| url: 'URL', |
| /** |
| *@description Text for the initiator of something |
| */ |
| initiator: 'Initiator', |
| /** |
| *@description Text in Coverage List View of the Coverage tab |
| */ |
| totalBytes: 'Total Bytes', |
| /** |
| *@description Text for errors |
| */ |
| error: 'Error', |
| /** |
| *@description Title for the developer resources tab |
| */ |
| developerResources: 'Developer Resources', |
| /** |
| *@description Text for a context menu entry |
| */ |
| copyUrl: 'Copy URL', |
| /** |
| * @description Text for a context menu entry. Command to copy a URL to the clipboard. The initiator |
| * of a request is the entity that caused this request to be sent. |
| */ |
| copyInitiatorUrl: 'Copy initiator URL', |
| /** |
| *@description Text for the status column of a list view |
| */ |
| pending: 'pending', |
| /** |
| *@description Text for the status column of a list view |
| */ |
| success: 'success', |
| /** |
| *@description Text for the status column of a list view |
| */ |
| failure: 'failure', |
| /** |
| *@description Accessible text for the value in bytes in memory allocation. |
| */ |
| sBytes: '{n, plural, =1 {# byte} other {# bytes}}', |
| }; |
| const str_ = i18n.i18n.registerUIStrings('panels/developer_resources/DeveloperResourcesListView.ts', UIStrings); |
| const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); |
| |
| export class DeveloperResourcesListView extends UI.Widget.VBox { |
| private readonly nodeForItem: Map<SDK.PageResourceLoader.PageResource, GridNode>; |
| private readonly isVisibleFilter: (arg0: SDK.PageResourceLoader.PageResource) => boolean; |
| private highlightRegExp: RegExp|null; |
| private dataGrid: DataGrid.SortableDataGrid.SortableDataGrid<GridNode>; |
| constructor(isVisibleFilter: (arg0: SDK.PageResourceLoader.PageResource) => boolean) { |
| super(true); |
| this.nodeForItem = new Map(); |
| this.isVisibleFilter = isVisibleFilter; |
| this.highlightRegExp = null; |
| |
| const columns = [ |
| {id: 'status', title: i18nString(UIStrings.status), width: '60px', fixedWidth: true, sortable: true}, |
| {id: 'url', title: i18nString(UIStrings.url), width: '250px', fixedWidth: false, sortable: true}, |
| {id: 'initiator', title: i18nString(UIStrings.initiator), width: '80px', fixedWidth: false, sortable: true}, |
| { |
| id: 'size', |
| title: i18nString(UIStrings.totalBytes), |
| width: '80px', |
| fixedWidth: true, |
| sortable: true, |
| align: DataGrid.DataGrid.Align.Right, |
| }, |
| { |
| id: 'errorMessage', |
| title: i18nString(UIStrings.error), |
| width: '200px', |
| fixedWidth: false, |
| sortable: true, |
| }, |
| ] as DataGrid.DataGrid.ColumnDescriptor[]; |
| this.dataGrid = new DataGrid.SortableDataGrid.SortableDataGrid({ |
| displayName: i18nString(UIStrings.developerResources), |
| columns, |
| editCallback: undefined, |
| refreshCallback: undefined, |
| deleteCallback: undefined, |
| }); |
| this.dataGrid.setResizeMethod(DataGrid.DataGrid.ResizeMethod.Last); |
| this.dataGrid.element.classList.add('flex-auto'); |
| this.dataGrid.addEventListener(DataGrid.DataGrid.Events.SortingChanged, this.sortingChanged, this); |
| this.dataGrid.setRowContextMenuCallback(this.populateContextMenu.bind(this)); |
| |
| const dataGridWidget = this.dataGrid.asWidget(); |
| dataGridWidget.show(this.contentElement); |
| this.setDefaultFocusedChild(dataGridWidget); |
| } |
| |
| private populateContextMenu( |
| contextMenu: UI.ContextMenu.ContextMenu, |
| gridNode: DataGrid.DataGrid.DataGridNode< |
| DataGrid.ViewportDataGrid.ViewportDataGridNode<DataGrid.SortableDataGrid.SortableDataGridNode<GridNode>>>): |
| void { |
| const item = (gridNode as GridNode).item; |
| contextMenu.clipboardSection().appendItem(i18nString(UIStrings.copyUrl), () => { |
| Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(item.url); |
| }); |
| if (item.initiator.initiatorUrl) { |
| contextMenu.clipboardSection().appendItem(i18nString(UIStrings.copyInitiatorUrl), () => { |
| Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(item.initiator.initiatorUrl); |
| }); |
| } |
| } |
| |
| update(items: Iterable<SDK.PageResourceLoader.PageResource>): void { |
| let hadUpdates = false; |
| const rootNode = this.dataGrid.rootNode(); |
| for (const item of items) { |
| let node = this.nodeForItem.get(item); |
| if (node) { |
| if (this.isVisibleFilter(node.item)) { |
| hadUpdates = node.refreshIfNeeded() || hadUpdates; |
| } |
| continue; |
| } |
| node = new GridNode(item); |
| this.nodeForItem.set(item, node); |
| if (this.isVisibleFilter(node.item)) { |
| rootNode.appendChild(node); |
| hadUpdates = true; |
| } |
| } |
| if (hadUpdates) { |
| this.sortingChanged(); |
| } |
| } |
| |
| reset(): void { |
| this.nodeForItem.clear(); |
| this.dataGrid.rootNode().removeChildren(); |
| } |
| |
| updateFilterAndHighlight(highlightRegExp: RegExp|null): void { |
| this.highlightRegExp = highlightRegExp; |
| let hadTreeUpdates = false; |
| for (const node of this.nodeForItem.values()) { |
| const shouldBeVisible = this.isVisibleFilter(node.item); |
| const isVisible = Boolean(node.parent); |
| if (shouldBeVisible) { |
| node.setHighlight(this.highlightRegExp); |
| } |
| if (shouldBeVisible === isVisible) { |
| continue; |
| } |
| hadTreeUpdates = true; |
| if (!shouldBeVisible) { |
| node.remove(); |
| } else { |
| this.dataGrid.rootNode().appendChild(node); |
| } |
| } |
| if (hadTreeUpdates) { |
| this.sortingChanged(); |
| } |
| } |
| |
| private sortingChanged(): void { |
| const columnId = this.dataGrid.sortColumnId(); |
| if (!columnId) { |
| return; |
| } |
| |
| const sortFunction = GridNode.sortFunctionForColumn(columnId) as ( |
| (arg0: DataGrid.SortableDataGrid.SortableDataGridNode<GridNode>, |
| arg1: DataGrid.SortableDataGrid.SortableDataGridNode<GridNode>) => number) | |
| null; |
| if (sortFunction) { |
| this.dataGrid.sortNodes(sortFunction, !this.dataGrid.isSortOrderAscending()); |
| } |
| } |
| wasShown(): void { |
| super.wasShown(); |
| this.registerCSSFiles([developerResourcesListViewStyles]); |
| } |
| } |
| |
| class GridNode extends DataGrid.SortableDataGrid.SortableDataGridNode<GridNode> { |
| item: SDK.PageResourceLoader.PageResource; |
| private highlightRegExp: RegExp|null; |
| constructor(item: SDK.PageResourceLoader.PageResource) { |
| super(); |
| this.item = item; |
| this.highlightRegExp = null; |
| } |
| |
| setHighlight(highlightRegExp: RegExp|null): void { |
| if (this.highlightRegExp === highlightRegExp) { |
| return; |
| } |
| this.highlightRegExp = highlightRegExp; |
| this.refresh(); |
| } |
| |
| refreshIfNeeded(): boolean { |
| this.refresh(); |
| return true; |
| } |
| |
| createCell(columnId: string): HTMLElement { |
| const cell = this.createTD(columnId) as HTMLElement; |
| switch (columnId) { |
| case 'url': { |
| UI.Tooltip.Tooltip.install(cell, this.item.url); |
| const outer = cell.createChild('div', 'url-outer'); |
| const prefix = outer.createChild('div', 'url-prefix'); |
| const suffix = outer.createChild('div', 'url-suffix'); |
| const splitURL = /^(.*)(\/[^/]*)$/.exec(this.item.url); |
| prefix.textContent = splitURL ? splitURL[1] : this.item.url; |
| suffix.textContent = splitURL ? splitURL[2] : ''; |
| if (this.highlightRegExp) { |
| this.highlight(outer, this.item.url); |
| } |
| this.setCellAccessibleName(this.item.url, cell, columnId); |
| break; |
| } |
| case 'initiator': { |
| const url = this.item.initiator.initiatorUrl || ''; |
| cell.textContent = url; |
| UI.Tooltip.Tooltip.install(cell, url); |
| this.setCellAccessibleName(url, cell, columnId); |
| cell.onmouseenter = (): void => { |
| const frameId = this.item.initiator.frameId; |
| const frame = frameId ? SDK.FrameManager.FrameManager.instance().getFrame(frameId) : null; |
| if (frame) { |
| void frame.highlight(); |
| } |
| }; |
| cell.onmouseleave = (): void => SDK.OverlayModel.OverlayModel.hideDOMNodeHighlight(); |
| break; |
| } |
| case 'status': { |
| if (this.item.success === null) { |
| cell.textContent = i18nString(UIStrings.pending); |
| } else { |
| cell.textContent = this.item.success ? i18nString(UIStrings.success) : i18nString(UIStrings.failure); |
| } |
| break; |
| } |
| case 'size': { |
| const size = this.item.size; |
| if (size !== null) { |
| const sizeSpan = cell.createChild('span'); |
| sizeSpan.textContent = Platform.NumberUtilities.withThousandsSeparator(size); |
| const sizeAccessibleName = i18nString(UIStrings.sBytes, {n: size}); |
| this.setCellAccessibleName(sizeAccessibleName, cell, columnId); |
| } |
| break; |
| } |
| case 'errorMessage': { |
| cell.classList.add('error-message'); |
| if (this.item.errorMessage) { |
| cell.textContent = this.item.errorMessage; |
| if (this.highlightRegExp) { |
| this.highlight(cell, this.item.errorMessage); |
| } |
| } |
| break; |
| } |
| } |
| return cell; |
| } |
| |
| private highlight(element: Element, textContent: string): void { |
| if (!this.highlightRegExp) { |
| return; |
| } |
| const matches = this.highlightRegExp.exec(textContent); |
| if (!matches || !matches.length) { |
| return; |
| } |
| const range = new TextUtils.TextRange.SourceRange(matches.index, matches[0].length); |
| UI.UIUtils.highlightRangesWithStyleClass(element, [range], 'filter-highlight'); |
| } |
| |
| static sortFunctionForColumn(columnId: string): ((arg0: GridNode, arg1: GridNode) => number)|null { |
| const nullToNegative = (x: boolean|number|null): number => x === null ? -1 : Number(x); |
| switch (columnId) { |
| case 'url': |
| return (a: GridNode, b: GridNode): number => a.item.url.localeCompare(b.item.url); |
| case 'status': |
| return (a: GridNode, b: GridNode): number => { |
| return nullToNegative(a.item.success) - nullToNegative(b.item.success); |
| }; |
| case 'size': |
| return (a: GridNode, b: GridNode): number => nullToNegative(a.item.size) - nullToNegative(b.item.size); |
| case 'initiator': |
| return (a: GridNode, b: GridNode): number => |
| (a.item.initiator.initiatorUrl || '').localeCompare(b.item.initiator.initiatorUrl || ''); |
| case 'errorMessage': |
| return (a: GridNode, b: GridNode): number => |
| (a.item.errorMessage || '').localeCompare(b.item.errorMessage || ''); |
| default: |
| console.assert(false, 'Unknown sort field: ' + columnId); |
| return null; |
| } |
| } |
| } |