| // 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. |
| |
| Resources.ServiceWorkerCacheView = class extends UI.SimpleView { |
| /** |
| * @param {!SDK.ServiceWorkerCacheModel} model |
| * @param {!SDK.ServiceWorkerCacheModel.Cache} cache |
| */ |
| constructor(model, cache) { |
| super(Common.UIString('Cache')); |
| this.registerRequiredCSS('resources/serviceWorkerCacheViews.css'); |
| |
| this._model = model; |
| this._entriesForTest = null; |
| |
| this.element.classList.add('service-worker-cache-data-view'); |
| this.element.classList.add('storage-view'); |
| |
| const editorToolbar = new UI.Toolbar('data-view-toolbar', this.element); |
| this._splitWidget = new UI.SplitWidget(false, false); |
| this._splitWidget.show(this.element); |
| |
| this._previewPanel = new UI.VBox(); |
| const resizer = this._previewPanel.element.createChild('div', 'cache-preview-panel-resizer'); |
| this._splitWidget.setMainWidget(this._previewPanel); |
| this._splitWidget.installResizer(resizer); |
| |
| /** @type {?UI.Widget} */ |
| this._preview = null; |
| |
| this._cache = cache; |
| /** @type {?DataGrid.DataGrid} */ |
| this._dataGrid = null; |
| this._refreshThrottler = new Common.Throttler(300); |
| this._refreshButton = new UI.ToolbarButton(Common.UIString('Refresh'), 'largeicon-refresh'); |
| this._refreshButton.addEventListener(UI.ToolbarButton.Events.Click, this._refreshButtonClicked, this); |
| editorToolbar.appendToolbarItem(this._refreshButton); |
| |
| this._deleteSelectedButton = new UI.ToolbarButton(Common.UIString('Delete Selected'), 'largeicon-delete'); |
| this._deleteSelectedButton.addEventListener(UI.ToolbarButton.Events.Click, () => this._deleteButtonClicked(null)); |
| editorToolbar.appendToolbarItem(this._deleteSelectedButton); |
| |
| const entryPathFilterBox = new UI.ToolbarInput(ls`Filter by Path`, '', 1); |
| editorToolbar.appendToolbarItem(entryPathFilterBox); |
| const entryPathFilterThrottler = new Common.Throttler(300); |
| this._entryPathFilter = ''; |
| entryPathFilterBox.addEventListener(UI.ToolbarInput.Event.TextChanged, () => { |
| entryPathFilterThrottler.schedule(() => { |
| this._entryPathFilter = entryPathFilterBox.value(); |
| return this._updateData(true); |
| }); |
| }); |
| |
| this._returnCount = /** @type {?number} */ (null); |
| this._summaryBarElement = /** @type {?Element} */ (null); |
| this._loadingPromise = /** @type {?Promise} */ (null); |
| |
| this.update(cache); |
| } |
| |
| _resetDataGrid() { |
| 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() { |
| this._model.addEventListener( |
| SDK.ServiceWorkerCacheModel.Events.CacheStorageContentUpdated, this._cacheContentUpdated, this); |
| this._updateData(true); |
| } |
| |
| /** |
| * @override |
| */ |
| willHide() { |
| this._model.removeEventListener( |
| SDK.ServiceWorkerCacheModel.Events.CacheStorageContentUpdated, this._cacheContentUpdated, this); |
| } |
| |
| /** |
| * @param {?UI.Widget} preview |
| */ |
| _showPreview(preview) { |
| if (preview && this._preview === preview) { |
| return; |
| } |
| if (this._preview) { |
| this._preview.detach(); |
| } |
| if (!preview) { |
| preview = new UI.EmptyWidget(Common.UIString('Select a cache entry above to preview')); |
| } |
| this._preview = preview; |
| this._preview.show(this._previewPanel.element); |
| } |
| |
| /** |
| * @return {!DataGrid.DataGrid} |
| */ |
| _createDataGrid() { |
| const columns = /** @type {!Array<!DataGrid.DataGrid.ColumnDescriptor>} */ ([ |
| {id: 'number', title: '#', sortable: false, width: '3px'}, |
| {id: 'name', title: Common.UIString('Name'), weight: 4, sortable: true}, |
| {id: 'responseType', title: ls`Response-Type`, weight: 1, align: DataGrid.DataGrid.Align.Right, sortable: true}, |
| {id: 'contentType', title: Common.UIString('Content-Type'), weight: 1, sortable: true}, { |
| id: 'contentLength', |
| title: Common.UIString('Content-Length'), |
| weight: 1, |
| align: DataGrid.DataGrid.Align.Right, |
| sortable: true |
| }, |
| { |
| id: 'responseTime', |
| title: Common.UIString('Time Cached'), |
| width: '12em', |
| weight: 1, |
| align: DataGrid.DataGrid.Align.Right, |
| sortable: true |
| } |
| ]); |
| const dataGrid = new DataGrid.DataGrid( |
| columns, undefined, this._deleteButtonClicked.bind(this), this._updateData.bind(this, true)); |
| |
| dataGrid.addEventListener(DataGrid.DataGrid.Events.SortingChanged, this._sortingChanged, this); |
| |
| dataGrid.addEventListener( |
| DataGrid.DataGrid.Events.SelectedNode, event => this._previewCachedResponse(event.data.data), this); |
| dataGrid.setStriped(true); |
| return dataGrid; |
| } |
| |
| _sortingChanged() { |
| if (!this._dataGrid) { |
| return; |
| } |
| |
| const accending = this._dataGrid.isSortOrderAscending(); |
| const columnId = this._dataGrid.sortColumnId(); |
| let comparator; |
| if (columnId === 'name') { |
| comparator = (a, b) => a._name.localeCompare(b._name); |
| } else if (columnId === 'contentType') { |
| comparator = (a, b) => a.data.mimeType.localeCompare(b.data.mimeType); |
| } else if (columnId === 'contentLength') { |
| comparator = (a, b) => a.data.resourceSize - b.data.resourceSize; |
| } else if (columnId === 'responseTime') { |
| comparator = (a, b) => a.data.endTime - b.data.endTime; |
| } else if (columnId === 'responseType') { |
| comparator = (a, b) => a._responseType.localeCompare(b._responseType); |
| } |
| |
| const children = this._dataGrid.rootNode().children.slice(); |
| this._dataGrid.rootNode().removeChildren(); |
| children.sort((a, b) => { |
| const result = comparator(a, b); |
| return accending ? result : -result; |
| }); |
| children.forEach(child => this._dataGrid.rootNode().appendChild(child)); |
| } |
| |
| /** |
| * @param {?DataGrid.DataGridNode} node |
| */ |
| async _deleteButtonClicked(node) { |
| if (!node) { |
| node = this._dataGrid && this._dataGrid.selectedNode; |
| if (!node) { |
| return; |
| } |
| } |
| await this._model.deleteCacheEntry(this._cache, /** @type {string} */ (node.data.url())); |
| node.remove(); |
| } |
| |
| /** |
| * @param {!SDK.ServiceWorkerCacheModel.Cache} cache |
| */ |
| update(cache) { |
| this._cache = cache; |
| this._resetDataGrid(); |
| this._updateData(true); |
| } |
| |
| _updateSummaryBar() { |
| 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 = ls`Matching entries: ${this._returnCount}`; |
| } else { |
| span.textContent = ls`Total entries: ${this._returnCount}`; |
| } |
| } |
| |
| /** |
| * @param {number} skipCount |
| * @param {!Array<!Protocol.CacheStorage.DataEntry>} entries |
| * @param {number} returnCount |
| * @this {Resources.ServiceWorkerCacheView} |
| */ |
| _updateDataCallback(skipCount, entries, returnCount) { |
| const selected = this._dataGrid.selectedNode && this._dataGrid.selectedNode.data.url(); |
| this._refreshButton.setEnabled(true); |
| this._entriesForTest = entries; |
| this._returnCount = returnCount; |
| this._updateSummaryBar(); |
| |
| /** @type {!Map<string, !DataGrid.DataGridNode>} */ |
| const oldEntries = new Map(); |
| const rootNode = this._dataGrid.rootNode(); |
| for (const node of rootNode.children) { |
| oldEntries.set(node.data.url, node); |
| } |
| rootNode.removeChildren(); |
| let selectedNode = 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 Resources.ServiceWorkerCacheView.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(); |
| } |
| |
| /** |
| * @param {boolean} force |
| */ |
| async _updateData(force) { |
| if (!force && this._loadingPromise) { |
| return this._loadingPromise; |
| } |
| this._refreshButton.setEnabled(false); |
| |
| if (this._loadingPromise) { |
| return this._loadingPromise; |
| } |
| |
| this._loadingPromise = new Promise(resolve => { |
| this._model.loadAllCacheData(this._cache, this._entryPathFilter, (entries, returnCount) => { |
| resolve([entries, returnCount]); |
| }); |
| }); |
| |
| const [entries, returnCount] = await this._loadingPromise; |
| this._updateDataCallback(0, entries, returnCount); |
| this._loadingPromise = null; |
| } |
| |
| /** |
| * @param {!Common.Event} event |
| */ |
| _refreshButtonClicked(event) { |
| this._updateData(true); |
| } |
| |
| /** |
| * @param {!Common.Event} event |
| */ |
| _cacheContentUpdated(event) { |
| const nameAndOrigin = event.data; |
| if (this._cache.securityOrigin !== nameAndOrigin.origin || this._cache.cacheName !== nameAndOrigin.cacheName) { |
| return; |
| } |
| this._refreshThrottler.schedule(() => Promise.resolve(this._updateData(true)), true); |
| } |
| |
| /** |
| * @param {!SDK.NetworkRequest} request |
| */ |
| async _previewCachedResponse(request) { |
| let preview = request[Resources.ServiceWorkerCacheView._previewSymbol]; |
| if (!preview) { |
| preview = new Resources.ServiceWorkerCacheView.RequestView(request); |
| request[Resources.ServiceWorkerCacheView._previewSymbol] = preview; |
| } |
| |
| // It is possible that table selection changes before the preview opens. |
| if (request === this._dataGrid.selectedNode.data) { |
| this._showPreview(preview); |
| } |
| } |
| |
| /** |
| * @param {!Protocol.CacheStorage.DataEntry} entry |
| * @return {!SDK.NetworkRequest} |
| */ |
| _createRequest(entry) { |
| const request = new SDK.NetworkRequest('cache-storage-' + entry.requestURL, entry.requestURL, '', '', '', null); |
| request.requestMethod = entry.requestMethod; |
| request.setRequestHeaders(entry.requestHeaders); |
| request.statusCode = entry.responseStatus; |
| request.statusText = entry.responseStatusText; |
| request.protocol = new Common.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'); |
| const contentType = header ? header.value : 'text/plain'; |
| request.mimeType = contentType; |
| |
| header = entry.responseHeaders.find(header => header.name.toLowerCase() === 'content-length'); |
| request.resourceSize = (header && header.value) | 0; |
| |
| let resourceType = Common.ResourceType.fromMimeType(contentType); |
| if (!resourceType) { |
| resourceType = Common.ResourceType.fromURL(entry.requestURL) || Common.resourceTypes.Other; |
| } |
| request.setResourceType(resourceType); |
| request.setContentDataProvider(this._requestContent.bind(this, request)); |
| return request; |
| } |
| |
| /** |
| * @param {!SDK.NetworkRequest} request |
| * @return {!Promise<!SDK.NetworkRequest.ContentData>} |
| */ |
| async _requestContent(request) { |
| const isText = request.resourceType().isTextType(); |
| const contentData = {error: null, content: null, encoded: !isText}; |
| const response = await this._cache.requestCachedResponse(request.url(), request.requestHeaders()); |
| if (response) { |
| contentData.content = isText ? window.atob(response.body) : response.body; |
| } |
| return contentData; |
| } |
| |
| _updatedForTest() { |
| } |
| }; |
| |
| Resources.ServiceWorkerCacheView._previewSymbol = Symbol('preview'); |
| |
| Resources.ServiceWorkerCacheView.DataGridNode = class extends DataGrid.DataGridNode { |
| /** |
| * @param {number} number |
| * @param {!SDK.NetworkRequest} request |
| * @param {!Protocol.CacheStorage.CachedResponseType} responseType |
| */ |
| constructor(number, request, responseType) { |
| super(request); |
| this._number = number; |
| const parsed = new Common.ParsedURL(request.url()); |
| if (parsed.isValid) { |
| this._name = request.url().trimURL(parsed.domain()); |
| } else { |
| this._name = request.url(); |
| } |
| this._request = request; |
| this._responseType = responseType; |
| } |
| |
| /** |
| * @override |
| * @param {string} columnId |
| * @return {!Element} |
| */ |
| createCell(columnId) { |
| const cell = this.createTD(columnId); |
| let value; |
| if (columnId === 'number') { |
| value = String(this._number); |
| } else if (columnId === 'name') { |
| value = this._name; |
| } else if (columnId === 'responseType') { |
| if (this._responseType === 'opaqueResponse') { |
| value = 'opaque'; |
| } else if (this._responseType === 'opaqueRedirect') { |
| value = 'opaqueredirect'; |
| } else { |
| value = this._responseType; |
| } |
| } else if (columnId === 'contentType') { |
| value = this._request.mimeType; |
| } else if (columnId === 'contentLength') { |
| value = (this._request.resourceSize | 0).toLocaleString('en-US'); |
| } else if (columnId === 'responseTime') { |
| value = new Date(this._request.endTime * 1000).toLocaleString(); |
| } |
| DataGrid.DataGrid.setElementText(cell, value || '', true); |
| cell.title = this._request.url(); |
| return cell; |
| } |
| }; |
| |
| Resources.ServiceWorkerCacheView.RequestView = class extends UI.VBox { |
| /** |
| * @param {!SDK.NetworkRequest} request |
| */ |
| constructor(request) { |
| super(); |
| |
| this._tabbedPane = new UI.TabbedPane(); |
| this._tabbedPane.addEventListener(UI.TabbedPane.Events.TabSelected, this._tabSelected, this); |
| this._resourceViewTabSetting = Common.settings.createSetting('cacheStorageViewTab', 'preview'); |
| |
| this._tabbedPane.appendTab('headers', Common.UIString('Headers'), new Network.RequestHeadersView(request)); |
| this._tabbedPane.appendTab('preview', Common.UIString('Preview'), new Network.RequestPreviewView(request)); |
| this._tabbedPane.show(this.element); |
| } |
| |
| /** |
| * @override |
| */ |
| wasShown() { |
| super.wasShown(); |
| this._selectTab(); |
| } |
| |
| /** |
| * @param {string=} tabId |
| */ |
| _selectTab(tabId) { |
| if (!tabId) { |
| tabId = this._resourceViewTabSetting.get(); |
| } |
| if (!this._tabbedPane.selectTab(tabId)) { |
| this._tabbedPane.selectTab('headers'); |
| } |
| } |
| |
| _tabSelected(event) { |
| if (!event.data.isUserGesture) { |
| return; |
| } |
| this._resourceViewTabSetting.set(event.data.tabId); |
| } |
| }; |