UNPKG

chrome-devtools-frontend

Version:
567 lines (510 loc) 21.8 kB
// Copyright 2014 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. /* eslint-disable rulesdir/no-imperative-dom-api */ import '../../ui/legacy/legacy.js'; import * as Common from '../../core/common/common.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 type * as Protocol from '../../generated/protocol.js'; import * as TextUtils from '../../models/text_utils/text_utils.js'; import * as LegacyWrapper from '../../ui/components/legacy_wrapper/legacy_wrapper.js'; import * as DataGrid from '../../ui/legacy/components/data_grid/data_grid.js'; import * as UI from '../../ui/legacy/legacy.js'; import * as VisualLogging from '../../ui/visual_logging/visual_logging.js'; import * as NetworkComponents from '../network/components/components.js'; import * as Network from '../network/network.js'; import * as ApplicationComponents from './components/components.js'; import serviceWorkerCacheViewsStyles from './serviceWorkerCacheViews.css.js'; const UIStrings = { /** *@description Text in Application Panel Sidebar of the Application panel */ cache: 'Cache', /** *@description Text to refresh the page */ refresh: 'Refresh', /** *@description Tooltip text that appears when hovering over the largeicon delete button in the Service Worker Cache Views of the Application panel */ deleteSelected: 'Delete Selected', /** *@description Text in Service Worker Cache Views of the Application panel */ filterByPath: 'Filter by path', /** *@description Text in Service Worker Cache Views of the Application panel that shows if no cache entry is selected for preview */ noCacheEntrySelected: 'No cache entry selected', /** *@description Text in Service Worker Cache Views of the Application panel */ selectACacheEntryAboveToPreview: 'Select a cache entry above to preview', /** *@description Text for the name of something */ name: 'Name', /** *@description Text in Service Worker Cache Views of the Application panel */ timeCached: 'Time Cached', /** * @description Tooltip text that appears when hovering over the vary header column in the Service Worker Cache Views of the Application panel */ varyHeaderWarning: '⚠️ Set ignoreVary to true when matching this entry', /** *@description Text used to show that data was retrieved from ServiceWorker Cache */ serviceWorkerCache: '`Service Worker` Cache', /** *@description Span text content in Service Worker Cache Views of the Application panel *@example {2} PH1 */ matchingEntriesS: 'Matching entries: {PH1}', /** *@description Span text content in Indexed DBViews of the Application panel *@example {2} PH1 */ totalEntriesS: 'Total entries: {PH1}', /** *@description Text for network request headers */ headers: 'Headers', /** *@description Text for previewing items */ preview: 'Preview', } as const; const str_ = i18n.i18n.registerUIStrings('panels/application/ServiceWorkerCacheViews.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); export class ServiceWorkerCacheView extends UI.View.SimpleView { private model: SDK.ServiceWorkerCacheModel.ServiceWorkerCacheModel; protected entriesForTest: Protocol.CacheStorage.DataEntry[]|null; private readonly splitWidget: UI.SplitWidget.SplitWidget; private readonly previewPanel: UI.Widget.VBox; private preview: UI.Widget.Widget|null; private cache: SDK.ServiceWorkerCacheModel.Cache; private dataGrid: DataGrid.DataGrid.DataGridImpl<DataGridNode>|null; private readonly refreshThrottler: Common.Throttler.Throttler; private readonly refreshButton: UI.Toolbar.ToolbarButton; private readonly deleteSelectedButton: UI.Toolbar.ToolbarButton; private entryPathFilter: string; private returnCount: number|null; private summaryBarElement: Element|null; private loadingPromise: Promise<{ entries: Protocol.CacheStorage.DataEntry[], returnCount: number, }>|null; private readonly metadataView = new ApplicationComponents.StorageMetadataView.StorageMetadataView(); constructor(model: SDK.ServiceWorkerCacheModel.ServiceWorkerCacheModel, cache: SDK.ServiceWorkerCacheModel.Cache) { super(i18nString(UIStrings.cache)); this.registerRequiredCSS(serviceWorkerCacheViewsStyles); this.model = model; this.entriesForTest = null; this.element.classList.add('service-worker-cache-data-view'); this.element.classList.add('storage-view'); this.element.setAttribute('jslog', `${VisualLogging.pane('cache-storage-data')}`); const editorToolbar = this.element.createChild('devtools-toolbar', 'data-view-toolbar'); editorToolbar.setAttribute('jslog', `${VisualLogging.toolbar()}`); this.element.appendChild(this.metadataView); this.splitWidget = new UI.SplitWidget.SplitWidget(false, false); this.splitWidget.show(this.element); this.previewPanel = new UI.Widget.VBox(); const resizer = this.previewPanel.element.createChild('div', 'cache-preview-panel-resizer'); this.splitWidget.setMainWidget(this.previewPanel); this.splitWidget.installResizer(resizer); this.preview = null; this.cache = cache; const bucketInfo = this.model.target() .model(SDK.StorageBucketsModel.StorageBucketsModel) ?.getBucketByName(cache.storageBucket.storageKey, cache.storageBucket.name); if (bucketInfo) { this.metadataView.setStorageBucket(bucketInfo); } else if (cache.storageKey) { this.metadataView.setStorageKey(cache.storageKey); } this.dataGrid = null; this.refreshThrottler = new Common.Throttler.Throttler(300); this.refreshButton = new UI.Toolbar.ToolbarButton(i18nString(UIStrings.refresh), 'refresh', undefined, 'cache-storage.refresh'); this.refreshButton.addEventListener(UI.Toolbar.ToolbarButton.Events.CLICK, this.refreshButtonClicked, this); editorToolbar.appendToolbarItem(this.refreshButton); this.deleteSelectedButton = new UI.Toolbar.ToolbarButton( i18nString(UIStrings.deleteSelected), 'cross', undefined, 'cache-storage.delete-selected'); this.deleteSelectedButton.addEventListener(UI.Toolbar.ToolbarButton.Events.CLICK, _event => { void this.deleteButtonClicked(null); }); editorToolbar.appendToolbarItem(this.deleteSelectedButton); const entryPathFilterBox = new UI.Toolbar.ToolbarFilter(i18nString(UIStrings.filterByPath), 1); editorToolbar.appendToolbarItem(entryPathFilterBox); const entryPathFilterThrottler = new Common.Throttler.Throttler(300); this.entryPathFilter = ''; entryPathFilterBox.addEventListener(UI.Toolbar.ToolbarInput.Event.TEXT_CHANGED, () => { void entryPathFilterThrottler.schedule(() => { this.entryPathFilter = entryPathFilterBox.value(); return this.updateData(true); }); }); this.returnCount = (null as number | null); this.summaryBarElement = (null as Element | null); this.loadingPromise = null; this.update(cache); } private resetDataGrid(): void { if (this.dataGrid) { this.dataGrid.asWidget().detach(); } this.dataGrid = this.createDataGrid(); const dataGridWidget = this.dataGrid.asWidget(); this.splitWidget.setSidebarWidget(dataGridWidget); dataGridWidget.setMinimumSize(0, 250); } override wasShown(): void { this.model.addEventListener( SDK.ServiceWorkerCacheModel.Events.CACHE_STORAGE_CONTENT_UPDATED, this.cacheContentUpdated, this); void this.updateData(true); } override willHide(): void { this.model.removeEventListener( SDK.ServiceWorkerCacheModel.Events.CACHE_STORAGE_CONTENT_UPDATED, this.cacheContentUpdated, this); } private showPreview(preview: UI.Widget.Widget|null): void { if (preview && this.preview === preview) { return; } if (this.preview) { this.preview.detach(); } if (!preview) { preview = new UI.EmptyWidget.EmptyWidget( i18nString(UIStrings.noCacheEntrySelected), i18nString(UIStrings.selectACacheEntryAboveToPreview)); } this.preview = preview; this.preview.show(this.previewPanel.element); } private createDataGrid(): DataGrid.DataGrid.DataGridImpl<DataGridNode> { const columns = ([ {id: 'number', title: '#', sortable: false, width: '3px'}, {id: 'name', title: i18nString(UIStrings.name), weight: 4, sortable: true}, { id: 'response-type', title: i18n.i18n.lockedString('Response-Type'), weight: 1, align: DataGrid.DataGrid.Align.RIGHT, sortable: true, }, {id: 'content-type', title: i18n.i18n.lockedString('Content-Type'), weight: 1, sortable: true}, { id: 'content-length', title: i18n.i18n.lockedString('Content-Length'), weight: 1, align: DataGrid.DataGrid.Align.RIGHT, sortable: true, }, { id: 'response-time', title: i18nString(UIStrings.timeCached), width: '12em', weight: 1, align: DataGrid.DataGrid.Align.RIGHT, sortable: true, }, {id: 'vary-header', title: i18n.i18n.lockedString('Vary Header'), weight: 1, sortable: true}, ] as DataGrid.DataGrid.ColumnDescriptor[]); const dataGrid = new DataGrid.DataGrid.DataGridImpl({ displayName: i18nString(UIStrings.serviceWorkerCache), columns, deleteCallback: this.deleteButtonClicked.bind(this), refreshCallback: this.updateData.bind(this, true), }); dataGrid.addEventListener(DataGrid.DataGrid.Events.SORTING_CHANGED, this.sortingChanged, this); dataGrid.addEventListener(DataGrid.DataGrid.Events.SELECTED_NODE, event => { void this.previewCachedResponse(event.data.data as SDK.NetworkRequest.NetworkRequest); }, this); dataGrid.setStriped(true); return dataGrid; } private sortingChanged(): void { if (!this.dataGrid) { return; } const dataGrid = this.dataGrid; const accending = dataGrid.isSortOrderAscending(); const columnId = dataGrid.sortColumnId(); let comparator: (arg0: DataGridNode, arg1: DataGridNode) => number; if (columnId === 'name') { comparator = (a: DataGridNode, b: DataGridNode) => a.name.localeCompare(b.name); } else if (columnId === 'content-type') { comparator = (a: DataGridNode, b: DataGridNode) => a.data.mimeType.localeCompare(b.data.mimeType); } else if (columnId === 'content-length') { comparator = (a: DataGridNode, b: DataGridNode) => a.data.resourceSize - b.data.resourceSize; } else if (columnId === 'response-time') { comparator = (a: DataGridNode, b: DataGridNode) => a.data.endTime - b.data.endTime; } else if (columnId === 'response-type') { comparator = (a: DataGridNode, b: DataGridNode) => a.responseType.localeCompare(b.responseType); } else if (columnId === 'vary-header') { comparator = (a: DataGridNode, b: DataGridNode) => a.varyHeader.localeCompare(b.varyHeader); } const children = dataGrid.rootNode().children.slice(); dataGrid.rootNode().removeChildren(); children.sort((a, b) => { const result = comparator((a as DataGridNode), (b as DataGridNode)); return accending ? result : -result; }); children.forEach(child => dataGrid.rootNode().appendChild(child)); } private async deleteButtonClicked(node: DataGrid.DataGrid.DataGridNode<DataGridNode>|null): Promise<void> { if (!node) { node = this.dataGrid?.selectedNode ?? null; if (!node) { return; } } await this.model.deleteCacheEntry(this.cache, (node.data.url() as string)); node.remove(); } update(cache: SDK.ServiceWorkerCacheModel.Cache|null = null): void { if (!cache) { return; } this.cache = cache; this.resetDataGrid(); void this.updateData(true); } private updateSummaryBar(): void { if (!this.summaryBarElement) { this.summaryBarElement = this.element.createChild('div', 'cache-storage-summary-bar'); } this.summaryBarElement.removeChildren(); const span = this.summaryBarElement.createChild('span'); if (this.entryPathFilter) { span.textContent = i18nString(UIStrings.matchingEntriesS, {PH1: String(this.returnCount)}); } else { span.textContent = i18nString(UIStrings.totalEntriesS, {PH1: String(this.returnCount)}); } } private updateDataCallback(entries: Protocol.CacheStorage.DataEntry[], returnCount: number): void { if (!this.dataGrid) { return; } const selected = this.dataGrid.selectedNode?.data.url(); this.refreshButton.setEnabled(true); this.entriesForTest = entries; this.returnCount = returnCount; this.updateSummaryBar(); const oldEntries = new Map<string, DataGridNode>(); const rootNode = this.dataGrid.rootNode(); for (const node of rootNode.children) { oldEntries.set(node.data.url, (node as DataGridNode)); } rootNode.removeChildren(); let selectedNode: DataGridNode|null = null; for (let i = 0; i < entries.length; ++i) { const entry = entries[i]; let node = oldEntries.get(entry.requestURL); if (!node || node.data.responseTime !== entry.responseTime) { node = new DataGridNode(i, this.createRequest(entry), entry.responseType); node.selectable = true; } else { node.data.number = i; } rootNode.appendChild(node); if (entry.requestURL === selected) { selectedNode = node; } } if (!selectedNode) { this.showPreview(null); } else { selectedNode.revealAndSelect(); } this.updatedForTest(); } private async updateData(force: boolean): Promise<{ entries: Protocol.CacheStorage.DataEntry[], returnCount: number, }|undefined> { if (!force && this.loadingPromise) { return await this.loadingPromise; } this.refreshButton.setEnabled(false); if (this.loadingPromise) { return await this.loadingPromise; } this.loadingPromise = new Promise(resolve => { this.model.loadAllCacheData( this.cache, this.entryPathFilter, (entries: Protocol.CacheStorage.DataEntry[], returnCount: number) => { resolve({entries, returnCount}); }); }); const {entries, returnCount} = await this.loadingPromise; this.updateDataCallback(entries, returnCount); this.loadingPromise = null; return; } private refreshButtonClicked(): void { void this.updateData(true); } private cacheContentUpdated( event: Common.EventTarget.EventTargetEvent<SDK.ServiceWorkerCacheModel.CacheStorageContentUpdatedEvent>): void { const {cacheName, storageBucket} = event.data; if ((!this.cache.inBucket(storageBucket) || this.cache.cacheName !== cacheName)) { return; } void this.refreshThrottler.schedule( () => Promise.resolve(this.updateData(true)), Common.Throttler.Scheduling.AS_SOON_AS_POSSIBLE); } private async previewCachedResponse(request: SDK.NetworkRequest.NetworkRequest): Promise<void> { let preview = networkRequestToPreview.get(request); if (!preview) { preview = new RequestView(request); networkRequestToPreview.set(request, preview); } // It is possible that table selection changes before the preview opens. if (this.dataGrid?.selectedNode && request === this.dataGrid.selectedNode.data) { this.showPreview(preview); } } private createRequest(entry: Protocol.CacheStorage.DataEntry): SDK.NetworkRequest.NetworkRequest { const request = SDK.NetworkRequest.NetworkRequest.createWithoutBackendRequest( 'cache-storage-' + entry.requestURL, entry.requestURL as Platform.DevToolsPath.UrlString, Platform.DevToolsPath.EmptyUrlString, null); request.requestMethod = entry.requestMethod; request.setRequestHeaders(entry.requestHeaders); request.statusCode = entry.responseStatus; request.statusText = entry.responseStatusText; request.protocol = new Common.ParsedURL.ParsedURL(entry.requestURL).scheme; request.responseHeaders = entry.responseHeaders; request.setRequestHeadersText(''); request.endTime = entry.responseTime; let header = entry.responseHeaders.find(header => header.name.toLowerCase() === 'content-type'); let mimeType: string = Platform.MimeType.MimeType.PLAIN; if (header) { const result = Platform.MimeType.parseContentType(header.value); if (result.mimeType) { mimeType = result.mimeType; } } request.mimeType = mimeType; header = entry.responseHeaders.find(header => header.name.toLowerCase() === 'content-length'); request.resourceSize = (header && Number(header.value)) || 0; let resourceType = Common.ResourceType.ResourceType.fromMimeType(mimeType); if (!resourceType) { resourceType = Common.ResourceType.ResourceType.fromURL(entry.requestURL) || Common.ResourceType.resourceTypes.Other; } request.setResourceType(resourceType); request.setContentDataProvider(this.requestContent.bind(this, request)); return request; } private async requestContent(request: SDK.NetworkRequest.NetworkRequest): Promise<TextUtils.ContentData.ContentDataOrError> { const response = await this.cache.requestCachedResponse(request.url(), request.requestHeaders()); if (!response) { return {error: 'No cached response found'}; } return new TextUtils.ContentData.ContentData( response.body, /* isBase64=*/ true, request.mimeType, request.charset() ?? undefined); } private updatedForTest(): void { } } const networkRequestToPreview = new WeakMap<SDK.NetworkRequest.NetworkRequest, RequestView>(); export class DataGridNode extends DataGrid.DataGrid.DataGridNode<DataGridNode> { private number: number; name: string; private request: SDK.NetworkRequest.NetworkRequest; responseType: Protocol.CacheStorage.CachedResponseType; varyHeader: string; constructor( number: number, request: SDK.NetworkRequest.NetworkRequest, responseType: Protocol.CacheStorage.CachedResponseType) { super(request); this.number = number; const parsed = new Common.ParsedURL.ParsedURL(request.url()); if (parsed.isValid) { this.name = Platform.StringUtilities.trimURL(request.url(), parsed.domain()); } else { this.name = request.url(); } this.request = request; this.responseType = responseType; this.varyHeader = request.responseHeaders.find(header => header.name.toLowerCase() === 'vary')?.value || ''; } override createCell(columnId: string): HTMLElement { const cell = this.createTD(columnId); let value; let tooltip = this.request.url() as string; if (columnId === 'number') { value = String(this.number); } else if (columnId === 'name') { value = this.name; } else if (columnId === 'response-type') { if (this.responseType === 'opaqueResponse') { value = 'opaque'; } else if (this.responseType === 'opaqueRedirect') { value = 'opaqueredirect'; } else { value = this.responseType; } } else if (columnId === 'content-type') { value = this.request.mimeType; } else if (columnId === 'content-length') { value = (this.request.resourceSize | 0).toLocaleString('en-US'); } else if (columnId === 'response-time') { value = new Date(this.request.endTime * 1000).toLocaleString(); } else if (columnId === 'vary-header') { value = this.varyHeader; if (this.varyHeader) { tooltip = i18nString(UIStrings.varyHeaderWarning); } } const parentElement = cell.parentElement; let gridNode; if (parentElement && this.dataGrid) { gridNode = this.dataGrid.elementToDataGridNode.get(parentElement); } DataGrid.DataGrid.DataGridImpl.setElementText(cell, value || '', /* longText= */ true, gridNode); UI.Tooltip.Tooltip.install(cell, tooltip); return cell; } } export class RequestView extends UI.Widget.VBox { private tabbedPane: UI.TabbedPane.TabbedPane; private resourceViewTabSetting: Common.Settings.Setting<string>; constructor(request: SDK.NetworkRequest.NetworkRequest) { super(); this.tabbedPane = new UI.TabbedPane.TabbedPane(); this.tabbedPane.element.setAttribute('jslog', `${VisualLogging.section('network-item-preview')}`); this.tabbedPane.addEventListener(UI.TabbedPane.Events.TabSelected, this.tabSelected, this); this.resourceViewTabSetting = Common.Settings.Settings.instance().createSetting('cache-storage-view-tab', 'preview'); this.tabbedPane.appendTab( 'headers', i18nString(UIStrings.headers), LegacyWrapper.LegacyWrapper.legacyWrapper( UI.Widget.VBox, new NetworkComponents.RequestHeadersView.RequestHeadersView(request))); this.tabbedPane.appendTab( 'preview', i18nString(UIStrings.preview), new Network.RequestPreviewView.RequestPreviewView(request)); this.tabbedPane.show(this.element); } override wasShown(): void { super.wasShown(); this.selectTab(); } private selectTab(tabId?: string): void { if (!tabId) { tabId = this.resourceViewTabSetting.get(); } if (tabId && !this.tabbedPane.selectTab(tabId)) { this.tabbedPane.selectTab('headers'); } } private tabSelected(event: Common.EventTarget.EventTargetEvent<UI.TabbedPane.EventData>): void { if (!event.data.isUserGesture) { return; } this.resourceViewTabSetting.set(event.data.tabId); } }