Files
react-native-devtools-frontend/front_end/resources/ServiceWorkerCacheViews.js
T
Tim van der Lippe 70842f313f Replace title property setter with Tooltip install invocation
ui/Tooltip.js overrides the HTMLElement prototype to override the
title property. Rather than using this property, we shoul be
calling Tooltip.install directly. This makes sure that new
components are not relying on the behavior of the legacy
prototype patching.

These usages have been manually audited using the following regexes:

Search: ([\S]+)\.title = ([^;]+);
Replace: UI.Tooltip.Tooltip.install($1, $2);

Note that there are classes in DevTools that also have a title
property. Most notably `TreeElement`. We should not be replacing
these, as they do not inherit from HTMLElement. Luckily, we are
running TypeScript to make sure we don't call `Tooltip.install`
with a non-HTMLElement.

A follow-up CL will clean up the getters.

R=jacktfranklin@chromium.org

Bug: 1150762
No-presubmit: True
Change-Id: I5928e75c70293531849e0576f4fb2a2a8b3e02d2
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2555060
Commit-Queue: Tim van der Lippe <tvanderlippe@chromium.org>
Auto-Submit: Tim van der Lippe <tvanderlippe@chromium.org>
Reviewed-by: Jack Franklin <jacktfranklin@chromium.org>
2020-11-23 17:16:46 +00:00

492 lines
16 KiB
JavaScript

// 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.
import * as Common from '../common/common.js';
import * as DataGrid from '../data_grid/data_grid.js';
import * as Network from '../network/network.js';
import * as Platform from '../platform/platform.js';
import * as SDK from '../sdk/sdk.js';
import * as UI from '../ui/ui.js';
export class ServiceWorkerCacheView extends UI.View.SimpleView {
/**
* @param {!SDK.ServiceWorkerCacheModel.ServiceWorkerCacheModel} model
* @param {!SDK.ServiceWorkerCacheModel.Cache} cache
*/
constructor(model, cache) {
super(Common.UIString.UIString('Cache'));
this.registerRequiredCSS('resources/serviceWorkerCacheViews.css', {enableLegacyPatching: true});
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.Toolbar('data-view-toolbar', this.element);
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);
/** @type {?UI.Widget.Widget} */
this._preview = null;
this._cache = cache;
/** @type {?DataGrid.DataGrid.DataGridImpl<!DataGridNode>} */
this._dataGrid = null;
this._refreshThrottler = new Common.Throttler.Throttler(300);
this._refreshButton = new UI.Toolbar.ToolbarButton(Common.UIString.UIString('Refresh'), 'largeicon-refresh');
this._refreshButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, this._refreshButtonClicked, this);
editorToolbar.appendToolbarItem(this._refreshButton);
this._deleteSelectedButton =
new UI.Toolbar.ToolbarButton(Common.UIString.UIString('Delete Selected'), 'largeicon-delete');
this._deleteSelectedButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, event => {
this._deleteButtonClicked(null);
});
editorToolbar.appendToolbarItem(this._deleteSelectedButton);
const entryPathFilterBox = new UI.Toolbar.ToolbarInput(ls`Filter by Path`, '', 1);
editorToolbar.appendToolbarItem(entryPathFilterBox);
const entryPathFilterThrottler = new Common.Throttler.Throttler(300);
this._entryPathFilter = '';
entryPathFilterBox.addEventListener(UI.Toolbar.ToolbarInput.Event.TextChanged, () => {
entryPathFilterThrottler.schedule(() => {
this._entryPathFilter = entryPathFilterBox.value();
return this._updateData(true);
});
});
this._returnCount = /** @type {?number} */ (null);
this._summaryBarElement = /** @type {?Element} */ (null);
/** @type {?Promise<!{entries: !Array<!Protocol.CacheStorage.DataEntry>, returnCount: number}>} */
this._loadingPromise = 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.Widget} preview
*/
_showPreview(preview) {
if (preview && this._preview === preview) {
return;
}
if (this._preview) {
this._preview.detach();
}
if (!preview) {
preview = new UI.EmptyWidget.EmptyWidget(Common.UIString.UIString('Select a cache entry above to preview'));
}
this._preview = preview;
this._preview.show(this._previewPanel.element);
}
/**
* @return {!DataGrid.DataGrid.DataGridImpl<!DataGridNode>}
*/
_createDataGrid() {
const columns = /** @type {!Array<!DataGrid.DataGrid.ColumnDescriptor>} */ ([
{id: 'number', title: '#', sortable: false, width: '3px'},
{id: 'name', title: Common.UIString.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.UIString('Content-Type'), weight: 1, sortable: true}, {
id: 'contentLength',
title: Common.UIString.UIString('Content-Length'),
weight: 1,
align: DataGrid.DataGrid.Align.Right,
sortable: true
},
{
id: 'responseTime',
title: Common.UIString.UIString('Time Cached'),
width: '12em',
weight: 1,
align: DataGrid.DataGrid.Align.Right,
sortable: true
}
]);
const dataGrid = new DataGrid.DataGrid.DataGridImpl({
displayName: ls`Service Worker Cache`,
columns,
deleteCallback: this._deleteButtonClicked.bind(this),
refreshCallback: this._updateData.bind(this, true),
editCallback: undefined,
});
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 dataGrid = this._dataGrid;
const accending = dataGrid.isSortOrderAscending();
const columnId = dataGrid.sortColumnId();
/** @type {function(!DataGridNode, !DataGridNode): number} */
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 = dataGrid.rootNode().children.slice();
dataGrid.rootNode().removeChildren();
children.sort((a, b) => {
const result = comparator(/** @type {!DataGridNode} */ (a), /** @type {!DataGridNode} */ (b));
return accending ? result : -result;
});
children.forEach(child => dataGrid.rootNode().appendChild(child));
}
/**
* @param {?DataGrid.DataGrid.DataGridNode<!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 {ServiceWorkerCacheView}
*/
_updateDataCallback(skipCount, entries, returnCount) {
if (!this._dataGrid) {
return;
}
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, !DataGridNode>} */
const oldEntries = new Map();
const rootNode = this._dataGrid.rootNode();
for (const node of rootNode.children) {
oldEntries.set(node.data.url, /** @type {!DataGridNode} */ (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 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;
return;
}
/**
* @param {!Common.EventTarget.EventTargetEvent} event
*/
_refreshButtonClicked(event) {
this._updateData(true);
}
/**
* @param {!Common.EventTarget.EventTargetEvent} 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.NetworkRequest} request
*/
async _previewCachedResponse(request) {
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 && this._dataGrid.selectedNode && request === this._dataGrid.selectedNode.data) {
this._showPreview(preview);
}
}
/**
* @param {!Protocol.CacheStorage.DataEntry} entry
* @return {!SDK.NetworkRequest.NetworkRequest}
*/
_createRequest(entry) {
const request =
new SDK.NetworkRequest.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.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 : SDK.NetworkRequest.MIME_TYPE.PLAIN;
request.mimeType = /** @type {!SDK.NetworkRequest.MIME_TYPE} */ (contentType);
header = entry.responseHeaders.find(header => header.name.toLowerCase() === 'content-length');
request.resourceSize = (header && Number(header.value)) || 0;
let resourceType = Common.ResourceType.ResourceType.fromMimeType(contentType);
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;
}
/**
* @param {!SDK.NetworkRequest.NetworkRequest} request
* @return {!Promise<!SDK.NetworkRequest.ContentData>}
*/
async _requestContent(request) {
const isText = request.resourceType().isTextType();
/** @type {!SDK.NetworkRequest.ContentData} */
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() {
}
}
/** @type {!WeakMap<!SDK.NetworkRequest.NetworkRequest, !RequestView>} */
const networkRequestToPreview = new WeakMap();
ServiceWorkerCacheView._previewSymbol = Symbol('preview');
/**
* @extends {DataGrid.DataGrid.DataGridNode<!DataGridNode>}
*/
export class DataGridNode extends DataGrid.DataGrid.DataGridNode {
/**
* @param {number} number
* @param {!SDK.NetworkRequest.NetworkRequest} request
* @param {!Protocol.CacheStorage.CachedResponseType} responseType
*/
constructor(number, request, responseType) {
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;
}
/**
* @override
* @param {string} columnId
* @return {!HTMLElement}
*/
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.DataGridImpl.setElementText(cell, value || '', true);
UI.Tooltip.Tooltip.install(cell, this._request.url());
return cell;
}
}
export class RequestView extends UI.Widget.VBox {
/**
* @param {!SDK.NetworkRequest.NetworkRequest} request
*/
constructor(request) {
super();
this._tabbedPane = new UI.TabbedPane.TabbedPane();
this._tabbedPane.addEventListener(UI.TabbedPane.Events.TabSelected, this._tabSelected, this);
this._resourceViewTabSetting = Common.Settings.Settings.instance().createSetting('cacheStorageViewTab', 'preview');
this._tabbedPane.appendTab(
'headers', Common.UIString.UIString('Headers'), new Network.RequestHeadersView.RequestHeadersView(request));
this._tabbedPane.appendTab(
'preview', Common.UIString.UIString('Preview'), new Network.RequestPreviewView.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 (tabId && !this._tabbedPane.selectTab(tabId)) {
this._tabbedPane.selectTab('headers');
}
}
/**
* @param {!Common.EventTarget.EventTargetEvent} event
*/
_tabSelected(event) {
if (!event.data.isUserGesture) {
return;
}
this._resourceViewTabSetting.set(event.data.tabId);
}
}