// 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 DataGrid from '../data_grid/data_grid.js'; import * as Host from '../host/host.js'; import {ls} from '../platform/platform.js'; import * as SDK from '../sdk/sdk.js'; // eslint-disable-line no-unused-vars import * as TextUtils from '../text_utils/text_utils.js'; import * as UI from '../ui/ui.js'; export class DeveloperResourcesListView extends UI.Widget.VBox { /** * @param {function(!SDK.PageResourceLoader.PageResource):boolean} isVisibleFilter */ constructor(isVisibleFilter) { super(true); /** @type {!Map} */ this._nodeForItem = new Map(); this._isVisibleFilter = isVisibleFilter; /** @type {?RegExp} */ this._highlightRegExp = null; this.registerRequiredCSS('developer_resources/developerResourcesListView.css'); const columns = /** @type {!Array} */ ([ {id: 'status', title: ls`Status`, width: '60px', fixedWidth: true, sortable: true}, {id: 'url', title: ls`URL`, width: '250px', fixedWidth: false, sortable: true}, {id: 'initiator', title: ls`Initiator`, width: '80px', fixedWidth: false, sortable: true}, { id: 'size', title: ls`Total Bytes`, width: '80px', fixedWidth: true, sortable: true, align: DataGrid.DataGrid.Align.Right }, { id: 'errorMessage', title: ls`Error`, width: '200px', fixedWidth: false, sortable: true, } ]); /** @type {!DataGrid.SortableDataGrid.SortableDataGrid} */ this._dataGrid = new DataGrid.SortableDataGrid.SortableDataGrid({ displayName: ls`Developer Resources`, 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); } /** * @param {!UI.ContextMenu.ContextMenu} contextMenu * @param {!DataGrid.ViewportDataGrid.ViewportDataGridNode>} gridNode */ _populateContextMenu(contextMenu, gridNode) { const item = (/** @type {!GridNode} */ (gridNode)).item; contextMenu.clipboardSection().appendItem(ls`Copy URL`, () => { Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(item.url); }); if (item.initiator.initiatorUrl) { contextMenu.clipboardSection().appendItem(ls`Copy initiator URL`, () => { Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(item.initiator.initiatorUrl); }); } } /** * @param {!Iterable} items */ update(items) { 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() { this._nodeForItem.clear(); this._dataGrid.rootNode().removeChildren(); } /** * @param {?RegExp} highlightRegExp */ updateFilterAndHighlight(highlightRegExp) { this._highlightRegExp = highlightRegExp; let hadTreeUpdates = false; for (const node of this._nodeForItem.values()) { const shouldBeVisible = this._isVisibleFilter(node.item); const isVisible = !!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(); } } _sortingChanged() { const columnId = this._dataGrid.sortColumnId(); if (!columnId) { return; } const sortFunction = /** @type {null|function(!DataGrid.SortableDataGrid.SortableDataGridNode, !DataGrid.SortableDataGrid.SortableDataGridNode):number} */ (GridNode.sortFunctionForColumn(columnId)); if (sortFunction) { this._dataGrid.sortNodes(sortFunction, !this._dataGrid.isSortOrderAscending()); } } } /** * @extends {DataGrid.SortableDataGrid.SortableDataGridNode} */ class GridNode extends DataGrid.SortableDataGrid.SortableDataGridNode { /** * @param {!SDK.PageResourceLoader.PageResource} item */ constructor(item) { super(); this.item = item; /** @type {?RegExp} */ this._highlightRegExp = null; } /** * @param {?RegExp} highlightRegExp */ _setHighlight(highlightRegExp) { if (this._highlightRegExp === highlightRegExp) { return; } this._highlightRegExp = highlightRegExp; this.refresh(); } /** * @return {boolean} */ _refreshIfNeeded() { this.refresh(); return true; } /** * @override * @param {string} columnId * @return {!HTMLElement} */ createCell(columnId) { const cell = /** @type {!HTMLElement} */ (this.createTD(columnId)); switch (columnId) { case 'url': { cell.title = 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; cell.title = url; this.setCellAccessibleName(url, cell, columnId); cell.onmouseenter = () => { const frame = SDK.FrameManager.FrameManager.instance().getFrame(this.item.initiator.frameId || ''); if (frame) { frame.highlight(); } }; cell.onmouseleave = () => SDK.OverlayModel.OverlayModel.hideDOMNodeHighlight(); break; } case 'status': { if (this.item.success === null) { cell.textContent = ls`pending`; } else { cell.textContent = this.item.success ? ls`success` : ls`failure`; } break; } case 'size': { const size = this.item.size; if (size !== null) { const sizeSpan = cell.createChild('span'); sizeSpan.textContent = Number.withThousandsSeparator(size); const sizeAccessibleName = (size === 1) ? ls`1 byte` : ls`${size} bytes`; 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; } /** * @param {!Element} element * @param {string} textContent */ _highlight(element, textContent) { 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'); } /** * * @param {string} columnId * @returns {null|function(!GridNode, !GridNode):number} */ static sortFunctionForColumn(columnId) { /** * @param {*} x */ const nullToNegative = x => x === null ? -1 : Number(x); switch (columnId) { case 'url': return (a, b) => a.item.url.localeCompare(b.item.url); case 'status': return (a, b) => { return nullToNegative(a.item.success) - nullToNegative(b.item.success); }; case 'size': return (a, b) => nullToNegative(a.item.size) - nullToNegative(b.item.size); case 'initiator': return (a, b) => (a.item.initiator.initiatorUrl || '').localeCompare(b.item.initiator.initiatorUrl || ''); case 'errorMessage': return (a, b) => (a.item.errorMessage || '').localeCompare(b.item.errorMessage || ''); default: console.assert(false, 'Unknown sort field: ' + columnId); return null; } } }