UNPKG

chrome-devtools-frontend

Version:
326 lines (307 loc) • 11.5 kB
// 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()); } } override 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; } override 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; } } }