blob: 0fbc0571aa350c76572e25d97d0612a56a5cef97 [file] [log] [blame]
// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/* eslint-disable @devtools/no-imperative-dom-api */
/*
* Copyright (C) 2007, 2008, 2010 Apple Inc. All rights reserved.
* Copyright (C) 2009 Joseph Pecoraro
* Copyright (C) 2013 Samsung Electronics. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import * as Common from '../../core/common/common.js';
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 Protocol from '../../generated/protocol.js';
import * as IssuesManager from '../../models/issues_manager/issues_manager.js';
import * as LegacyWrapper from '../../ui/components/legacy_wrapper/legacy_wrapper.js';
import {createIcon} from '../../ui/kit/kit.js';
import * as SourceFrame from '../../ui/legacy/components/source_frame/source_frame.js';
import * as UI from '../../ui/legacy/legacy.js';
import {ApplicationPanelTreeElement, ExpandableApplicationPanelTreeElement} from './ApplicationPanelTreeElement.js';
import {AppManifestView, Events as AppManifestViewEvents} from './AppManifestView.js';
import {BackForwardCacheTreeElement} from './BackForwardCacheTreeElement.js';
import {BackgroundServiceModel} from './BackgroundServiceModel.js';
import {BackgroundServiceView} from './BackgroundServiceView.js';
import {BounceTrackingMitigationsTreeElement} from './BounceTrackingMitigationsTreeElement.js';
import {type DOMStorage, DOMStorageModel, Events as DOMStorageModelEvents} from './DOMStorageModel.js';
import {
Events as ExtensionStorageModelEvents,
type ExtensionStorage,
ExtensionStorageModel,
} from './ExtensionStorageModel.js';
import {FrameDetailsReportView} from './FrameDetailsView.js';
import {
type Database as IndexedDBModelDatabase,
type DatabaseId,
Events as IndexedDBModelEvents,
type Index,
IndexedDBModel,
type ObjectStore,
} from './IndexedDBModel.js';
import {IDBDatabaseView, IDBDataView} from './IndexedDBViews.js';
import {Events as InterestGroupModelEvents, InterestGroupStorageModel} from './InterestGroupStorageModel.js';
import {InterestGroupTreeElement} from './InterestGroupTreeElement.js';
import {OpenedWindowDetailsView, WorkerDetailsView} from './OpenedWindowDetailsView.js';
import type * as PreloadingHelper from './preloading/helper/helper.js';
import {
PreloadingSummaryTreeElement,
} from './PreloadingTreeElement.js';
import {ReportingApiTreeElement} from './ReportingApiTreeElement.js';
import type {ResourcesPanel} from './ResourcesPanel.js';
import resourcesSidebarStyles from './resourcesSidebar.css.js';
import {ServiceWorkerCacheTreeElement} from './ServiceWorkerCacheTreeElement.js';
import {ServiceWorkersView} from './ServiceWorkersView.js';
import {SharedStorageListTreeElement} from './SharedStorageListTreeElement.js';
import {
Events as SharedStorageModelEvents,
type SharedStorageForOrigin,
SharedStorageModel,
} from './SharedStorageModel.js';
import {SharedStorageTreeElement} from './SharedStorageTreeElement.js';
import {StorageBucketsTreeParentElement} from './StorageBucketsTreeElement.js';
import {StorageView} from './StorageView.js';
import {TrustTokensTreeElement} from './TrustTokensTreeElement.js';
const UIStrings = {
/**
* @description Text in Application Panel Sidebar of the Application panel
*/
application: 'Application',
/**
* @description Text in Application Panel Sidebar of the Application panel
*/
storage: 'Storage',
/**
* @description Text in Application Panelthat shows if no local storage
* can be shown.
*/
noLocalStorage: 'No local storage detected',
/**
* @description Text in Application Panel Sidebar of the Application panel
*/
localStorage: 'Local storage',
/**
* @description Text in the Application panel describing the local storage tab.
*/
localStorageDescription: 'On this page you can view, add, edit, and delete local storage key-value pairs.',
/**
* @description Text in Application Panel Sidebar of the Application panel
*/
sessionStorage: 'Session storage',
/**
* @description Text in Application Panel if no session storage can be shown.
*/
noSessionStorage: 'No session storage detected',
/**
* @description Text in the Application panel describing the session storage tab.
*/
sessionStorageDescription: 'On this page you can view, add, edit, and delete session storage key-value pairs.',
/**
* @description Text in Application Panel Sidebar of the Application panel
*/
extensionStorage: 'Extension storage',
/**
* @description Text in Application Panel if no extension storage can be shown
*/
noExtensionStorage: 'No extension storage detected',
/**
* @description Text in the Application panel describing the extension storage tab.
*/
extensionStorageDescription: 'On this page you can view, add, edit, and delete extension storage key-value pairs.',
/**
* @description Text for extension session storage in Application panel
*/
extensionSessionStorage: 'Session',
/**
* @description Text for extension local storage in Application panel
*/
extensionLocalStorage: 'Local',
/**
* @description Text for extension sync storage in Application panel
*/
extensionSyncStorage: 'Sync',
/**
* @description Text for extension managed storage in Application panel
*/
extensionManagedStorage: 'Managed',
/**
* @description Text for web cookies
*/
cookies: 'Cookies',
/**
* @description Text in the Application Panel if no cookies are set
*/
noCookies: 'No cookies set',
/**
* @description Text for web cookies
*/
cookiesDescription: 'On this page you can view, add, edit, and delete cookies.',
/**
* @description Text in Application Panel Sidebar of the Application panel
*/
backgroundServices: 'Background services',
/**
* @description Text for rendering frames
*/
frames: 'Frames',
/**
* @description Text that appears on a button for the manifest resource type filter.
*/
manifest: 'Manifest',
/**
* @description Text in Application Panel Sidebar of the Application panel
*/
indexeddb: 'IndexedDB',
/**
* @description Text in Application Panel if no indexedDB is detected
*/
noIndexeddb: 'No indexedDB detected',
/**
* @description Text in the Application panel describing the extension storage tab.
*/
indexeddbDescription: 'On this page you can view and delete indexedDB key-value pairs and databases.',
/**
* @description A context menu item in the Application Panel Sidebar of the Application panel
*/
refreshIndexeddb: 'Refresh IndexedDB',
/**
* @description Tooltip in Application Panel Sidebar of the Application panel
* @example {1.0} PH1
*/
versionSEmpty: 'Version: {PH1} (empty)',
/**
* @description Tooltip in Application Panel Sidebar of the Application panel
* @example {1.0} PH1
*/
versionS: 'Version: {PH1}',
/**
* @description Text to clear content
*/
clear: 'Clear',
/**
* @description Text in Application Panel Sidebar of the Application panel
* @example {"key path"} PH1
*/
keyPathS: 'Key path: {PH1}',
/**
* @description Text in Application Panel Sidebar of the Application panel
*/
localFiles: 'Local Files',
/**
* @description Tooltip in Application Panel Sidebar of the Application panel
* @example {https://example.com} PH1
*/
cookiesUsedByFramesFromS: 'Cookies used by frames from {PH1}',
/**
* @description Text in Frames View of the Application panel
*/
openedWindows: 'Opened Windows',
/**
* @description Text in Frames View of the Application panel
*/
openedWindowsDescription: 'On this page you can view windows opened via window\.open\(\).',
/**
* @description Label for plural of worker type: web workers
*/
webWorkers: 'Web Workers',
/**
* @description Label in frame tree for unavailable document
*/
documentNotAvailable: 'No document detected',
/**
* @description Description of content of unavailable document in Application panel
*/
theContentOfThisDocumentHasBeen:
'The content of this document has been generated dynamically via \'document.write()\'.',
/**
* @description Text in Frames View of the Application panel
*/
windowWithoutTitle: 'Window without title',
/**
* @description Default name for worker
*/
worker: 'worker',
/**
* @description Description text for describing the dedicated worker tab.
*/
workerDescription: 'On this page you can view dedicated workers that are created by the parent frame.',
/**
* @description Aria text for screen reader to announce they can scroll to top of manifest if invoked
*/
onInvokeManifestAlert: 'Manifest: Invoke to scroll to the top of manifest',
/**
* @description Aria text for screen reader to announce they can scroll to a section if invoked
* @example {"Identity"} PH1
*/
beforeInvokeAlert: '{PH1}: Invoke to scroll to this section in manifest',
/**
* @description Alert message for screen reader to announce which subsection is being scrolled to
* @example {"Identity"} PH1
*/
onInvokeAlert: 'Scrolled to {PH1}',
/**
* @description Application sidebar panel
*/
applicationSidebarPanel: 'Application panel sidebar',
/**
* @description Tooltip in Application Panel Sidebar of the Application panel
* @example {https://example.com} PH1
*/
thirdPartyPhaseout: 'Cookies from {PH1} may have been blocked due to third-party cookie phaseout.',
/**
* @description Description text in the Application Panel describing a frame's resources
*/
resourceDescription: 'On this page you can view the frame\'s resources.'
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/application/ApplicationPanelSidebar.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
function assertNotMainTarget(targetId: Protocol.Target.TargetID|'main'): asserts targetId is Protocol.Target.TargetID {
if (targetId === 'main') {
throw new Error('Unexpected main target id');
}
}
function nameForExtensionStorageArea(storageArea: Protocol.Extensions.StorageArea): string {
switch (storageArea) {
case Protocol.Extensions.StorageArea.Session:
return i18nString(UIStrings.extensionSessionStorage);
case Protocol.Extensions.StorageArea.Local:
return i18nString(UIStrings.extensionLocalStorage);
case Protocol.Extensions.StorageArea.Sync:
return i18nString(UIStrings.extensionSyncStorage);
case Protocol.Extensions.StorageArea.Managed:
return i18nString(UIStrings.extensionManagedStorage);
default:
throw new Error(`Unrecognized storage type: ${storageArea}`);
}
}
export namespace SharedStorageTreeElementDispatcher {
export const enum Events {
SHARED_STORAGE_TREE_ELEMENT_ADDED = 'SharedStorageTreeElementAdded',
}
export interface SharedStorageTreeElementAddedEvent {
origin: string;
}
export interface EventTypes {
[Events.SHARED_STORAGE_TREE_ELEMENT_ADDED]: SharedStorageTreeElementAddedEvent;
}
}
export class ApplicationPanelSidebar extends UI.Widget.VBox implements SDK.TargetManager.Observer {
panel: ResourcesPanel;
private readonly sidebarTree: UI.TreeOutline.TreeOutlineInShadow;
private readonly applicationTreeElement: UI.TreeOutline.TreeElement;
serviceWorkersTreeElement: ServiceWorkersTreeElement;
localStorageListTreeElement: ExpandableApplicationPanelTreeElement;
sessionStorageListTreeElement: ExpandableApplicationPanelTreeElement;
extensionStorageListTreeElement: ExpandableApplicationPanelTreeElement;
indexedDBListTreeElement: IndexedDBTreeElement;
interestGroupTreeElement: InterestGroupTreeElement;
cookieListTreeElement: ExpandableApplicationPanelTreeElement;
trustTokensTreeElement: TrustTokensTreeElement;
cacheStorageListTreeElement: ServiceWorkerCacheTreeElement;
sharedStorageListTreeElement: SharedStorageListTreeElement;
storageBucketsTreeElement: StorageBucketsTreeParentElement|undefined;
private backForwardCacheListTreeElement?: BackForwardCacheTreeElement;
backgroundFetchTreeElement: BackgroundServiceTreeElement;
backgroundSyncTreeElement: BackgroundServiceTreeElement;
bounceTrackingMitigationsTreeElement: BounceTrackingMitigationsTreeElement;
notificationsTreeElement: BackgroundServiceTreeElement;
paymentHandlerTreeElement: BackgroundServiceTreeElement;
periodicBackgroundSyncTreeElement: BackgroundServiceTreeElement;
pushMessagingTreeElement: BackgroundServiceTreeElement;
reportingApiTreeElement: ReportingApiTreeElement;
preloadingSummaryTreeElement: PreloadingSummaryTreeElement|undefined;
private readonly resourcesSection: ResourcesSection;
private domStorageTreeElements: Map<DOMStorage, DOMStorageTreeElement>;
private extensionIdToStorageTreeParentElement: Map<string, ExtensionStorageTreeParentElement>;
private extensionStorageModels: ExtensionStorageModel[];
private extensionStorageTreeElements: Map<string, ExtensionStorageTreeElement>;
private sharedStorageTreeElements: Map<string, SharedStorageTreeElement>;
private domains: Record<string, boolean>;
// Holds main frame target.
private target?: SDK.Target.Target;
private previousHoveredElement?: FrameTreeElement;
readonly sharedStorageTreeElementDispatcher:
Common.ObjectWrapper.ObjectWrapper<SharedStorageTreeElementDispatcher.EventTypes>;
constructor(panel: ResourcesPanel) {
super();
this.panel = panel;
this.sidebarTree = new UI.TreeOutline.TreeOutlineInShadow(UI.TreeOutline.TreeVariant.NAVIGATION_TREE);
this.sidebarTree.registerRequiredCSS(resourcesSidebarStyles);
this.sidebarTree.element.classList.add('resources-sidebar');
this.sidebarTree.setHideOverflow(true);
this.sidebarTree.element.classList.add('filter-all');
// Listener needs to have been set up before the elements are added
this.sidebarTree.addEventListener(UI.TreeOutline.Events.ElementAttached, this.treeElementAdded, this);
this.contentElement.appendChild(this.sidebarTree.element);
const applicationSectionTitle = i18nString(UIStrings.application);
this.applicationTreeElement = this.addSidebarSection(applicationSectionTitle, 'application');
const applicationPanelSidebar = this.applicationTreeElement.treeOutline?.contentElement;
if (applicationPanelSidebar) {
applicationPanelSidebar.ariaLabel = i18nString(UIStrings.applicationSidebarPanel);
}
const manifestTreeElement = new AppManifestTreeElement(panel);
this.applicationTreeElement.appendChild(manifestTreeElement);
manifestTreeElement.generateChildren();
this.serviceWorkersTreeElement = new ServiceWorkersTreeElement(panel);
this.applicationTreeElement.appendChild(this.serviceWorkersTreeElement);
const clearStorageTreeElement = new ClearStorageTreeElement(panel);
this.applicationTreeElement.appendChild(clearStorageTreeElement);
const storageSectionTitle = i18nString(UIStrings.storage);
const storageTreeElement = this.addSidebarSection(storageSectionTitle, 'storage');
this.localStorageListTreeElement = new ExpandableApplicationPanelTreeElement(
panel, i18nString(UIStrings.localStorage), i18nString(UIStrings.noLocalStorage),
i18nString(UIStrings.localStorageDescription), 'local-storage');
this.localStorageListTreeElement.setLink(
'https://developer.chrome.com/docs/devtools/storage/localstorage/' as Platform.DevToolsPath.UrlString);
const localStorageIcon = createIcon('table');
this.localStorageListTreeElement.setLeadingIcons([localStorageIcon]);
storageTreeElement.appendChild(this.localStorageListTreeElement);
this.sessionStorageListTreeElement = new ExpandableApplicationPanelTreeElement(
panel, i18nString(UIStrings.sessionStorage), i18nString(UIStrings.noSessionStorage),
i18nString(UIStrings.sessionStorageDescription), 'session-storage');
this.sessionStorageListTreeElement.setLink(
'https://developer.chrome.com/docs/devtools/storage/sessionstorage/' as Platform.DevToolsPath.UrlString);
const sessionStorageIcon = createIcon('table');
this.sessionStorageListTreeElement.setLeadingIcons([sessionStorageIcon]);
storageTreeElement.appendChild(this.sessionStorageListTreeElement);
this.extensionStorageListTreeElement = new ExpandableApplicationPanelTreeElement(
panel, i18nString(UIStrings.extensionStorage), i18nString(UIStrings.noExtensionStorage),
i18nString(UIStrings.extensionStorageDescription), 'extension-storage');
this.extensionStorageListTreeElement.setLink(
'https://developer.chrome.com/docs/extensions/reference/api/storage/' as Platform.DevToolsPath.UrlString);
const extensionStorageIcon = createIcon('table');
this.extensionStorageListTreeElement.setLeadingIcons([extensionStorageIcon]);
storageTreeElement.appendChild(this.extensionStorageListTreeElement);
this.indexedDBListTreeElement = new IndexedDBTreeElement(panel);
this.indexedDBListTreeElement.setLink(
'https://developer.chrome.com/docs/devtools/storage/indexeddb/' as Platform.DevToolsPath.UrlString);
storageTreeElement.appendChild(this.indexedDBListTreeElement);
this.cookieListTreeElement = new ExpandableApplicationPanelTreeElement(
panel, i18nString(UIStrings.cookies), i18nString(UIStrings.noCookies), i18nString(UIStrings.cookiesDescription),
'cookies');
this.cookieListTreeElement.setLink(
'https://developer.chrome.com/docs/devtools/storage/cookies/' as Platform.DevToolsPath.UrlString);
const cookieIcon = createIcon('cookie');
this.cookieListTreeElement.setLeadingIcons([cookieIcon]);
storageTreeElement.appendChild(this.cookieListTreeElement);
this.trustTokensTreeElement = new TrustTokensTreeElement(panel);
storageTreeElement.appendChild(this.trustTokensTreeElement);
this.interestGroupTreeElement = new InterestGroupTreeElement(panel);
storageTreeElement.appendChild(this.interestGroupTreeElement);
this.sharedStorageListTreeElement = new SharedStorageListTreeElement(panel);
storageTreeElement.appendChild(this.sharedStorageListTreeElement);
this.cacheStorageListTreeElement = new ServiceWorkerCacheTreeElement(panel);
storageTreeElement.appendChild(this.cacheStorageListTreeElement);
this.storageBucketsTreeElement = new StorageBucketsTreeParentElement(panel);
storageTreeElement.appendChild(this.storageBucketsTreeElement);
const backgroundServiceSectionTitle = i18nString(UIStrings.backgroundServices);
const backgroundServiceTreeElement = this.addSidebarSection(backgroundServiceSectionTitle, 'background-services');
this.backForwardCacheListTreeElement = new BackForwardCacheTreeElement(panel);
backgroundServiceTreeElement.appendChild(this.backForwardCacheListTreeElement);
this.backgroundFetchTreeElement =
new BackgroundServiceTreeElement(panel, Protocol.BackgroundService.ServiceName.BackgroundFetch);
backgroundServiceTreeElement.appendChild(this.backgroundFetchTreeElement);
this.backgroundSyncTreeElement =
new BackgroundServiceTreeElement(panel, Protocol.BackgroundService.ServiceName.BackgroundSync);
backgroundServiceTreeElement.appendChild(this.backgroundSyncTreeElement);
this.bounceTrackingMitigationsTreeElement = new BounceTrackingMitigationsTreeElement(panel);
backgroundServiceTreeElement.appendChild(this.bounceTrackingMitigationsTreeElement);
this.notificationsTreeElement =
new BackgroundServiceTreeElement(panel, Protocol.BackgroundService.ServiceName.Notifications);
backgroundServiceTreeElement.appendChild(this.notificationsTreeElement);
this.paymentHandlerTreeElement =
new BackgroundServiceTreeElement(panel, Protocol.BackgroundService.ServiceName.PaymentHandler);
backgroundServiceTreeElement.appendChild(this.paymentHandlerTreeElement);
this.periodicBackgroundSyncTreeElement =
new BackgroundServiceTreeElement(panel, Protocol.BackgroundService.ServiceName.PeriodicBackgroundSync);
backgroundServiceTreeElement.appendChild(this.periodicBackgroundSyncTreeElement);
this.preloadingSummaryTreeElement = new PreloadingSummaryTreeElement(panel);
backgroundServiceTreeElement.appendChild(this.preloadingSummaryTreeElement);
this.preloadingSummaryTreeElement.constructChildren(panel);
this.pushMessagingTreeElement =
new BackgroundServiceTreeElement(panel, Protocol.BackgroundService.ServiceName.PushMessaging);
backgroundServiceTreeElement.appendChild(this.pushMessagingTreeElement);
this.reportingApiTreeElement = new ReportingApiTreeElement(panel);
backgroundServiceTreeElement.appendChild(this.reportingApiTreeElement);
const resourcesSectionTitle = i18nString(UIStrings.frames);
const resourcesTreeElement = this.addSidebarSection(resourcesSectionTitle, 'frames');
this.resourcesSection = new ResourcesSection(panel, resourcesTreeElement);
this.domStorageTreeElements = new Map();
this.extensionIdToStorageTreeParentElement = new Map();
this.extensionStorageTreeElements = new Map();
this.extensionStorageModels = [];
this.sharedStorageTreeElements = new Map();
this.domains = {};
this.sidebarTree.contentElement.addEventListener('mousemove', this.onmousemove.bind(this), false);
this.sidebarTree.contentElement.addEventListener('mouseleave', this.onmouseleave.bind(this), false);
SDK.TargetManager.TargetManager.instance().observeTargets(this, {scoped: true});
SDK.TargetManager.TargetManager.instance().addModelListener(
SDK.ResourceTreeModel.ResourceTreeModel, SDK.ResourceTreeModel.Events.FrameNavigated, this.frameNavigated, this,
{scoped: true});
const selection = this.panel.lastSelectedItemPath();
if (!selection.length) {
manifestTreeElement.select();
}
SDK.TargetManager.TargetManager.instance().observeModels(
DOMStorageModel, {
modelAdded: (model: DOMStorageModel) => this.domStorageModelAdded(model),
modelRemoved: (model: DOMStorageModel) => this.domStorageModelRemoved(model),
},
{scoped: true});
SDK.TargetManager.TargetManager.instance().observeModels(
ExtensionStorageModel, {
modelAdded: (model: ExtensionStorageModel) => this.extensionStorageModelAdded(model),
modelRemoved: (model: ExtensionStorageModel) => this.extensionStorageModelRemoved(model),
},
{scoped: true});
SDK.TargetManager.TargetManager.instance().observeModels(
IndexedDBModel, {
modelAdded: (model: IndexedDBModel) => this.indexedDBModelAdded(model),
modelRemoved: (model: IndexedDBModel) => this.indexedDBModelRemoved(model),
},
{scoped: true});
SDK.TargetManager.TargetManager.instance().observeModels(
InterestGroupStorageModel, {
modelAdded: (model: InterestGroupStorageModel) => this.interestGroupModelAdded(model),
modelRemoved: (model: InterestGroupStorageModel) => this.interestGroupModelRemoved(model),
},
{scoped: true});
SDK.TargetManager.TargetManager.instance().observeModels(
SharedStorageModel, {
modelAdded: (model: SharedStorageModel) => this.sharedStorageModelAdded(model).catch(err => {
console.error(err);
}),
modelRemoved: (model: SharedStorageModel) => this.sharedStorageModelRemoved(model),
},
{scoped: true});
SDK.TargetManager.TargetManager.instance().observeModels(
SDK.StorageBucketsModel.StorageBucketsModel, {
modelAdded: (model: SDK.StorageBucketsModel.StorageBucketsModel) => this.storageBucketsModelAdded(model),
modelRemoved: (model: SDK.StorageBucketsModel.StorageBucketsModel) => this.storageBucketsModelRemoved(model),
},
{scoped: true});
this.sharedStorageTreeElementDispatcher =
new Common.ObjectWrapper.ObjectWrapper<SharedStorageTreeElementDispatcher.EventTypes>();
this.contentElement.style.contain = 'layout style';
}
private addSidebarSection(title: string, jslogContext: string): UI.TreeOutline.TreeElement {
const treeElement = new UI.TreeOutline.TreeElement(title, true, jslogContext);
treeElement.listItemElement.classList.add('storage-group-list-item');
treeElement.setCollapsible(false);
treeElement.selectable = false;
this.sidebarTree.appendChild(treeElement);
UI.ARIAUtils.markAsHeading(treeElement.listItemElement, 3);
UI.ARIAUtils.setLabel(treeElement.childrenListElement, title);
return treeElement;
}
targetAdded(target: SDK.Target.Target): void {
if (target !== target.outermostTarget()) {
return;
}
this.target = target;
const interestGroupModel = target.model(InterestGroupStorageModel);
if (interestGroupModel) {
interestGroupModel.addEventListener(
InterestGroupModelEvents.INTEREST_GROUP_ACCESS, this.interestGroupAccess, this);
}
const resourceTreeModel = target.model(SDK.ResourceTreeModel.ResourceTreeModel);
if (!resourceTreeModel) {
return;
}
if (resourceTreeModel.cachedResourcesLoaded()) {
this.initialize();
}
resourceTreeModel.addEventListener(SDK.ResourceTreeModel.Events.CachedResourcesLoaded, this.initialize, this);
resourceTreeModel.addEventListener(
SDK.ResourceTreeModel.Events.WillLoadCachedResources, this.resetWithFrames, this);
}
targetRemoved(target: SDK.Target.Target): void {
if (target !== this.target) {
return;
}
delete this.target;
const resourceTreeModel = target.model(SDK.ResourceTreeModel.ResourceTreeModel);
if (resourceTreeModel) {
resourceTreeModel.removeEventListener(SDK.ResourceTreeModel.Events.CachedResourcesLoaded, this.initialize, this);
resourceTreeModel.removeEventListener(
SDK.ResourceTreeModel.Events.WillLoadCachedResources, this.resetWithFrames, this);
}
const interestGroupModel = target.model(InterestGroupStorageModel);
if (interestGroupModel) {
interestGroupModel.removeEventListener(
InterestGroupModelEvents.INTEREST_GROUP_ACCESS, this.interestGroupAccess, this);
}
this.resetWithFrames();
}
override focus(): void {
this.sidebarTree.focus();
}
private initialize(): void {
for (const frame of SDK.ResourceTreeModel.ResourceTreeModel.frames()) {
this.addCookieDocument(frame);
}
const interestGroupModel = this.target?.model(InterestGroupStorageModel);
if (interestGroupModel) {
interestGroupModel.enable();
}
this.cacheStorageListTreeElement.initialize();
const backgroundServiceModel = this.target?.model(BackgroundServiceModel) || null;
this.backgroundFetchTreeElement.initialize(backgroundServiceModel);
this.backgroundSyncTreeElement.initialize(backgroundServiceModel);
this.notificationsTreeElement.initialize(backgroundServiceModel);
this.paymentHandlerTreeElement.initialize(backgroundServiceModel);
this.periodicBackgroundSyncTreeElement.initialize(backgroundServiceModel);
this.pushMessagingTreeElement.initialize(backgroundServiceModel);
this.storageBucketsTreeElement?.initialize();
const preloadingModel = this.target?.model(SDK.PreloadingModel.PreloadingModel);
if (preloadingModel) {
this.preloadingSummaryTreeElement?.initialize(preloadingModel);
}
}
private domStorageModelAdded(model: DOMStorageModel): void {
model.enable();
model.storages().forEach(this.addDOMStorage.bind(this));
model.addEventListener(DOMStorageModelEvents.DOM_STORAGE_ADDED, this.domStorageAdded, this);
model.addEventListener(DOMStorageModelEvents.DOM_STORAGE_REMOVED, this.domStorageRemoved, this);
}
private domStorageModelRemoved(model: DOMStorageModel): void {
model.storages().forEach(this.removeDOMStorage.bind(this));
model.removeEventListener(DOMStorageModelEvents.DOM_STORAGE_ADDED, this.domStorageAdded, this);
model.removeEventListener(DOMStorageModelEvents.DOM_STORAGE_REMOVED, this.domStorageRemoved, this);
}
private extensionStorageModelAdded(model: ExtensionStorageModel): void {
this.extensionStorageModels.push(model);
model.enable();
model.storages().forEach(this.addExtensionStorage.bind(this));
model.addEventListener(ExtensionStorageModelEvents.EXTENSION_STORAGE_ADDED, this.extensionStorageAdded, this);
model.addEventListener(ExtensionStorageModelEvents.EXTENSION_STORAGE_REMOVED, this.extensionStorageRemoved, this);
}
private extensionStorageModelRemoved(model: ExtensionStorageModel): void {
console.assert(this.extensionStorageModels.includes(model));
this.extensionStorageModels.splice(this.extensionStorageModels.indexOf(model), 1);
model.storages().forEach(this.removeExtensionStorage.bind(this));
model.removeEventListener(ExtensionStorageModelEvents.EXTENSION_STORAGE_ADDED, this.extensionStorageAdded, this);
model.removeEventListener(
ExtensionStorageModelEvents.EXTENSION_STORAGE_REMOVED, this.extensionStorageRemoved, this);
}
private indexedDBModelAdded(model: IndexedDBModel): void {
model.enable();
this.indexedDBListTreeElement.addIndexedDBForModel(model);
}
private indexedDBModelRemoved(model: IndexedDBModel): void {
this.indexedDBListTreeElement.removeIndexedDBForModel(model);
}
private interestGroupModelAdded(model: InterestGroupStorageModel): void {
model.enable();
model.addEventListener(InterestGroupModelEvents.INTEREST_GROUP_ACCESS, this.interestGroupAccess, this);
}
private interestGroupModelRemoved(model: InterestGroupStorageModel): void {
model.disable();
model.removeEventListener(InterestGroupModelEvents.INTEREST_GROUP_ACCESS, this.interestGroupAccess, this);
}
private async sharedStorageModelAdded(model: SharedStorageModel): Promise<void> {
await model.enable();
for (const storage of model.storages()) {
await this.addSharedStorage(storage);
}
model.addEventListener(SharedStorageModelEvents.SHARED_STORAGE_ADDED, this.sharedStorageAdded, this);
model.addEventListener(SharedStorageModelEvents.SHARED_STORAGE_REMOVED, this.sharedStorageRemoved, this);
model.addEventListener(SharedStorageModelEvents.SHARED_STORAGE_ACCESS, this.sharedStorageAccess, this);
}
private sharedStorageModelRemoved(model: SharedStorageModel): void {
model.disable();
for (const storage of model.storages()) {
this.removeSharedStorage(storage);
}
model.removeEventListener(SharedStorageModelEvents.SHARED_STORAGE_ADDED, this.sharedStorageAdded, this);
model.removeEventListener(SharedStorageModelEvents.SHARED_STORAGE_REMOVED, this.sharedStorageRemoved, this);
model.removeEventListener(SharedStorageModelEvents.SHARED_STORAGE_ACCESS, this.sharedStorageAccess, this);
}
private storageBucketsModelAdded(model: SDK.StorageBucketsModel.StorageBucketsModel): void {
model.enable();
}
private storageBucketsModelRemoved(model: SDK.StorageBucketsModel.StorageBucketsModel): void {
this.storageBucketsTreeElement?.removeBucketsForModel(model);
}
private resetWithFrames(): void {
this.resourcesSection.reset();
this.reset();
}
private treeElementAdded(event: Common.EventTarget.EventTargetEvent<UI.TreeOutline.TreeElement>): void {
// On tree item selection its itemURL and those of its parents are persisted.
// On reload/navigation we check for matches starting from the root on the
// path to the current element. Matching nodes are expanded until we hit a
// mismatch. This way we ensure that the longest matching path starting from
// the root is expanded, even if we cannot match the whole path.
const selection = this.panel.lastSelectedItemPath();
if (!selection.length) {
return;
}
const element = event.data;
const elementPath = [element as UI.TreeOutline.TreeElement | ApplicationPanelTreeElement];
for (let parent = element.parent as UI.TreeOutline.TreeElement | ApplicationPanelTreeElement | null;
parent && 'itemURL' in parent && parent.itemURL; parent = parent.parent) {
elementPath.push(parent);
}
let i = selection.length - 1;
let j = elementPath.length - 1;
while (i >= 0 && j >= 0 && selection[i] === (elementPath[j] as ApplicationPanelTreeElement).itemURL) {
if (!elementPath[j].expanded) {
if (i > 0) {
elementPath[j].expand();
}
if (!elementPath[j].selected) {
elementPath[j].select();
}
}
i--;
j--;
}
}
private reset(): void {
this.domains = {};
this.cookieListTreeElement.removeChildren();
this.interestGroupTreeElement.clearEvents();
}
private frameNavigated(event: Common.EventTarget.EventTargetEvent<SDK.ResourceTreeModel.ResourceTreeFrame>): void {
const frame = event.data;
if (frame.isOutermostFrame()) {
this.reset();
}
this.addCookieDocument(frame);
}
private interestGroupAccess(event: Common.EventTarget.EventTargetEvent<Protocol.Storage.InterestGroupAccessedEvent>):
void {
this.interestGroupTreeElement.addEvent(event.data);
}
private addCookieDocument(frame: SDK.ResourceTreeModel.ResourceTreeFrame): void {
// In case the current frame was unreachable, show its cookies
// instead of the error interstitials because they might help to
// debug why the frame was unreachable.
const urlToParse = frame.unreachableUrl() || frame.url;
const parsedURL = Common.ParsedURL.ParsedURL.fromString(urlToParse);
if (!parsedURL || (parsedURL.scheme !== 'http' && parsedURL.scheme !== 'https' && parsedURL.scheme !== 'file')) {
return;
}
const domain = parsedURL.securityOrigin();
if (!this.domains[domain]) {
this.domains[domain] = true;
const cookieDomainTreeElement = new CookieTreeElement(this.panel, frame, parsedURL);
this.cookieListTreeElement.appendChild(cookieDomainTreeElement);
}
}
private domStorageAdded(event: Common.EventTarget.EventTargetEvent<DOMStorage>): void {
const domStorage = (event.data);
this.addDOMStorage(domStorage);
}
private addDOMStorage(domStorage: DOMStorage): void {
console.assert(!this.domStorageTreeElements.get(domStorage));
console.assert(Boolean(domStorage.storageKey));
const domStorageTreeElement = new DOMStorageTreeElement(this.panel, domStorage);
this.domStorageTreeElements.set(domStorage, domStorageTreeElement);
if (domStorage.isLocalStorage) {
this.localStorageListTreeElement.appendChild(domStorageTreeElement, comparator);
} else {
this.sessionStorageListTreeElement.appendChild(domStorageTreeElement, comparator);
}
function comparator(a: UI.TreeOutline.TreeElement, b: UI.TreeOutline.TreeElement): number {
const aTitle = a.titleAsText().toLocaleLowerCase();
const bTitle = b.titleAsText().toLocaleUpperCase();
return aTitle.localeCompare(bTitle);
}
}
private domStorageRemoved(event: Common.EventTarget.EventTargetEvent<DOMStorage>): void {
const domStorage = (event.data);
this.removeDOMStorage(domStorage);
}
private removeDOMStorage(domStorage: DOMStorage): void {
const treeElement = this.domStorageTreeElements.get(domStorage);
if (!treeElement) {
return;
}
const wasSelected = treeElement.selected;
const parentListTreeElement = treeElement.parent;
if (parentListTreeElement) {
parentListTreeElement.removeChild(treeElement);
if (wasSelected) {
parentListTreeElement.select();
}
}
this.domStorageTreeElements.delete(domStorage);
}
private extensionStorageAdded(event: Common.EventTarget.EventTargetEvent<ExtensionStorage>): void {
const extensionStorage = event.data;
this.addExtensionStorage(extensionStorage);
}
private useTreeViewForExtensionStorage(extensionStorage: ExtensionStorage): boolean {
// If the origin the storage is associated with matches the top-level
// target (e.g, an extension service worker or top-level
// chrome-extension:// page), there is likely only one extension in the
// context we are inspecting and we can show the storage as a direct child.
// In other contexts (where multiple extensions may be injected) use a tree
// view where storage areas are children of the extension they are
// associated with.
return !extensionStorage.matchesTarget(this.target);
}
private getExtensionStorageAreaParent(extensionStorage: ExtensionStorage): ApplicationPanelTreeElement|undefined {
if (!this.useTreeViewForExtensionStorage(extensionStorage)) {
return this.extensionStorageListTreeElement;
}
const existingParent = this.extensionIdToStorageTreeParentElement.get(extensionStorage.extensionId);
if (existingParent) {
return existingParent;
}
const parent =
new ExtensionStorageTreeParentElement(this.panel, extensionStorage.extensionId, extensionStorage.name);
this.extensionIdToStorageTreeParentElement.set(extensionStorage.extensionId, parent);
this.extensionStorageListTreeElement?.appendChild(parent);
return parent;
}
private addExtensionStorage(extensionStorage: ExtensionStorage): void {
if (this.extensionStorageModels.find(
m => m !== extensionStorage.model &&
m.storageForIdAndArea(extensionStorage.extensionId, extensionStorage.storageArea))) {
// There's at least one model that already has this storage area, so no need
// to do anything.
return;
}
console.assert(Boolean(this.extensionStorageListTreeElement));
console.assert(!this.extensionStorageTreeElements.get(extensionStorage.key));
const extensionStorageTreeElement = new ExtensionStorageTreeElement(this.panel, extensionStorage);
this.extensionStorageTreeElements.set(extensionStorage.key, extensionStorageTreeElement);
this.getExtensionStorageAreaParent(extensionStorage)?.appendChild(extensionStorageTreeElement, comparator);
function comparator(a: UI.TreeOutline.TreeElement, b: UI.TreeOutline.TreeElement): number {
const getStorageArea = (e: UI.TreeOutline.TreeElement): Protocol.Extensions.StorageArea =>
(e as ExtensionStorageTreeElement).storageArea;
const order = [
Protocol.Extensions.StorageArea.Session,
Protocol.Extensions.StorageArea.Local,
Protocol.Extensions.StorageArea.Sync,
Protocol.Extensions.StorageArea.Managed,
];
return order.indexOf(getStorageArea(a)) - order.indexOf(getStorageArea(b));
}
}
private extensionStorageRemoved(event: Common.EventTarget.EventTargetEvent<ExtensionStorage>): void {
const extensionStorage = event.data;
this.removeExtensionStorage(extensionStorage);
}
private removeExtensionStorage(extensionStorage: ExtensionStorage): void {
if (this.extensionStorageModels.find(
(m => m.storageForIdAndArea(extensionStorage.extensionId, extensionStorage.storageArea)))) {
// There's at least one model that still has this storage area, so no need
// to do anything.
return;
}
const treeElement = this.extensionStorageTreeElements.get(extensionStorage.key);
if (!treeElement) {
return;
}
const wasSelected = treeElement.selected;
const parentListTreeElement = treeElement.parent;
if (parentListTreeElement) {
parentListTreeElement.removeChild(treeElement);
if (this.useTreeViewForExtensionStorage(extensionStorage) && parentListTreeElement.childCount() === 0) {
this.extensionStorageListTreeElement?.removeChild(parentListTreeElement);
this.extensionIdToStorageTreeParentElement.delete(extensionStorage.extensionId);
} else if (wasSelected) {
parentListTreeElement.select();
}
}
this.extensionStorageTreeElements.delete(extensionStorage.key);
}
private async sharedStorageAdded(event: Common.EventTarget.EventTargetEvent<SharedStorageForOrigin>): Promise<void> {
await this.addSharedStorage(event.data);
}
private async addSharedStorage(sharedStorage: SharedStorageForOrigin): Promise<void> {
const sharedStorageTreeElement = await SharedStorageTreeElement.createElement(this.panel, sharedStorage);
// A tree element for `sharedStorage.securityOrigin` may have been added while we were waiting for `sharedStorageTreeElement` to be created.
if (this.sharedStorageTreeElements.has(sharedStorage.securityOrigin)) {
return;
}
this.sharedStorageTreeElements.set(sharedStorage.securityOrigin, sharedStorageTreeElement);
this.sharedStorageListTreeElement.appendChild(sharedStorageTreeElement);
this.sharedStorageTreeElementDispatcher.dispatchEventToListeners(
SharedStorageTreeElementDispatcher.Events.SHARED_STORAGE_TREE_ELEMENT_ADDED,
{origin: sharedStorage.securityOrigin});
}
private sharedStorageRemoved(event: Common.EventTarget.EventTargetEvent<SharedStorageForOrigin>): void {
this.removeSharedStorage(event.data);
}
private removeSharedStorage(sharedStorage: SharedStorageForOrigin): void {
const treeElement = this.sharedStorageTreeElements.get(sharedStorage.securityOrigin);
if (!treeElement) {
return;
}
const wasSelected = treeElement.selected;
const parentListTreeElement = treeElement.parent;
if (parentListTreeElement) {
parentListTreeElement.removeChild(treeElement);
parentListTreeElement.setExpandable(parentListTreeElement.childCount() > 0);
if (wasSelected) {
parentListTreeElement.select();
}
}
this.sharedStorageTreeElements.delete(sharedStorage.securityOrigin);
}
private sharedStorageAccess(event: Common.EventTarget.EventTargetEvent<Protocol.Storage.SharedStorageAccessedEvent>):
void {
this.sharedStorageListTreeElement.addEvent(event.data);
}
async showResource(resource: SDK.Resource.Resource, line?: number, column?: number): Promise<void> {
await this.resourcesSection.revealResource(resource, line, column);
}
showFrame(frame: SDK.ResourceTreeModel.ResourceTreeFrame): void {
this.resourcesSection.revealAndSelectFrame(frame);
}
showPreloadingRuleSetView(revealInfo: PreloadingHelper.PreloadingForward.RuleSetView): void {
if (this.preloadingSummaryTreeElement) {
this.preloadingSummaryTreeElement.expandAndRevealRuleSet(revealInfo);
}
}
showPreloadingAttemptViewWithFilter(filter: PreloadingHelper.PreloadingForward.AttemptViewWithFilter): void {
if (this.preloadingSummaryTreeElement) {
this.preloadingSummaryTreeElement.expandAndRevealAttempts(filter);
}
}
private onmousemove(event: MouseEvent): void {
const nodeUnderMouse = (event.target as Node);
if (!nodeUnderMouse) {
return;
}
const listNode = UI.UIUtils.enclosingNodeOrSelfWithNodeName(nodeUnderMouse, 'li');
if (!listNode) {
return;
}
const element = UI.TreeOutline.TreeElement.getTreeElementBylistItemNode(listNode);
if (this.previousHoveredElement === element) {
return;
}
if (this.previousHoveredElement) {
this.previousHoveredElement.hovered = false;
delete this.previousHoveredElement;
}
if (element instanceof FrameTreeElement) {
this.previousHoveredElement = element;
element.hovered = true;
}
}
private onmouseleave(_event: MouseEvent): void {
if (this.previousHoveredElement) {
this.previousHoveredElement.hovered = false;
delete this.previousHoveredElement;
}
}
}
export class BackgroundServiceTreeElement extends ApplicationPanelTreeElement {
private serviceName: Protocol.BackgroundService.ServiceName;
private view: BackgroundServiceView|null;
private model: BackgroundServiceModel|null;
#selected: boolean;
constructor(storagePanel: ResourcesPanel, serviceName: Protocol.BackgroundService.ServiceName) {
super(
storagePanel, BackgroundServiceView.getUIString(serviceName), false,
Platform.StringUtilities.toKebabCase(serviceName));
this.serviceName = serviceName;
/* Whether the element has been selected. */
this.#selected = false;
this.view = null;
this.model = null;
const backgroundServiceIcon = createIcon(this.getIconType());
this.setLeadingIcons([backgroundServiceIcon]);
}
private getIconType(): string {
switch (this.serviceName) {
case Protocol.BackgroundService.ServiceName.BackgroundFetch:
return 'arrow-up-down';
case Protocol.BackgroundService.ServiceName.BackgroundSync:
return 'sync';
case Protocol.BackgroundService.ServiceName.PushMessaging:
return 'cloud';
case Protocol.BackgroundService.ServiceName.Notifications:
return 'bell';
case Protocol.BackgroundService.ServiceName.PaymentHandler:
return 'credit-card';
case Protocol.BackgroundService.ServiceName.PeriodicBackgroundSync:
return 'watch';
default:
console.error(`Service ${this.serviceName} does not have a dedicated icon`);
return 'table';
}
}
initialize(model: BackgroundServiceModel|null): void {
this.model = model;
// Show the view if the model was initialized after selection.
if (this.#selected && !this.view) {
this.onselect(false);
}
}
override get itemURL(): Platform.DevToolsPath.UrlString {
return `background-service://${this.serviceName}` as Platform.DevToolsPath.UrlString;
}
override get selectable(): boolean {
if (!this.model) {
return false;
}
return super.selectable;
}
override onselect(selectedByUser?: boolean): boolean {
super.onselect(selectedByUser);
this.#selected = true;
if (!this.model) {
return false;
}
if (!this.view) {
this.view = new BackgroundServiceView(this.serviceName, this.model);
}
this.showView(this.view);
UI.Context.Context.instance().setFlavor(BackgroundServiceView, this.view);
Host.userMetrics.panelShown('background_service_' + this.serviceName);
return false;
}
}
export class ServiceWorkersTreeElement extends ApplicationPanelTreeElement {
private view?: ServiceWorkersView;
constructor(storagePanel: ResourcesPanel) {
super(storagePanel, i18n.i18n.lockedString('Service workers'), false, 'service-workers');
const icon = createIcon('gears');
this.setLeadingIcons([icon]);
}
override get itemURL(): Platform.DevToolsPath.UrlString {
return 'service-workers://' as Platform.DevToolsPath.UrlString;
}
override onselect(selectedByUser?: boolean): boolean {
super.onselect(selectedByUser);
if (!this.view) {
this.view = new ServiceWorkersView();
}
this.showView(this.view);
Host.userMetrics.panelShown('service-workers');
return false;
}
}
export class AppManifestTreeElement extends ApplicationPanelTreeElement {
private view: AppManifestView;
constructor(storagePanel: ResourcesPanel) {
super(storagePanel, i18nString(UIStrings.manifest), true, 'manifest');
const icon = createIcon('document');
this.setLeadingIcons([icon]);
self.onInvokeElement(this.listItemElement, this.onInvoke.bind(this));
this.view = new AppManifestView();
UI.ARIAUtils.setLabel(this.listItemElement, i18nString(UIStrings.onInvokeManifestAlert));
const handleExpansion = (hasManifest: boolean): void => {
this.setExpandable(hasManifest);
};
this.view.addEventListener(AppManifestViewEvents.MANIFEST_DETECTED, event => handleExpansion(event.data));
}
override get itemURL(): Platform.DevToolsPath.UrlString {
return 'manifest://' as Platform.DevToolsPath.UrlString;
}
override onselect(selectedByUser?: boolean): boolean {
super.onselect(selectedByUser);
this.showView(this.view);
Host.userMetrics.panelShown('app-manifest');
return false;
}
generateChildren(): void {
const staticSections = this.view.getStaticSections();
for (const section of staticSections) {
const childTitle = section.title;
const child = new ApplicationPanelTreeElement(this.resourcesPanel, childTitle, false, section.jslogContext || '');
child.onselect = (selectedByUser?: boolean): boolean => {
if (selectedByUser) {
this.showView(this.view);
this.view.scrollToSection(childTitle);
}
return true;
};
const icon = createIcon('document');
child.setLeadingIcons([icon]);
child.listItemElement.addEventListener('keydown', (event: Event) => {
if ((event as KeyboardEvent).key !== 'Tab' || (event as KeyboardEvent).shiftKey) {
return;
}
if (this.view.focusOnSection(childTitle)) {
event.consume(true);
}
});
UI.ARIAUtils.setLabel(
child.listItemElement, i18nString(UIStrings.beforeInvokeAlert, {PH1: child.listItemElement.title}));
this.appendChild(child);
}
}
onInvoke(): void {
this.view.getManifestElement().scrollIntoView();
UI.ARIAUtils.LiveAnnouncer.alert(i18nString(UIStrings.onInvokeAlert, {PH1: this.listItemElement.title}));
}
}
export class ClearStorageTreeElement extends ApplicationPanelTreeElement {
private view?: StorageView;
constructor(storagePanel: ResourcesPanel) {
super(storagePanel, i18nString(UIStrings.storage), false, 'storage');
const icon = createIcon('database');
this.setLeadingIcons([icon]);
}
override get itemURL(): Platform.DevToolsPath.UrlString {
return 'clear-storage://' as Platform.DevToolsPath.UrlString;
}
override onselect(selectedByUser?: boolean): boolean {
super.onselect(selectedByUser);
if (!this.view) {
this.view = new StorageView();
}
this.showView(this.view);
Host.userMetrics.panelShown(Host.UserMetrics.PanelCodes[Host.UserMetrics.PanelCodes.storage]);
return false;
}
}
export class IndexedDBTreeElement extends ExpandableApplicationPanelTreeElement {
private idbDatabaseTreeElements: IDBDatabaseTreeElement[];
private storageBucket?: Protocol.Storage.StorageBucket;
constructor(storagePanel: ResourcesPanel, storageBucket?: Protocol.Storage.StorageBucket) {
super(
storagePanel, i18nString(UIStrings.indexeddb), i18nString(UIStrings.noIndexeddb),
i18nString(UIStrings.indexeddbDescription), 'indexed-db');
const icon = createIcon('database');
this.setLeadingIcons([icon]);
this.idbDatabaseTreeElements = [];
this.storageBucket = storageBucket;
this.initialize();
}
private initialize(): void {
SDK.TargetManager.TargetManager.instance().addModelListener(
IndexedDBModel, IndexedDBModelEvents.DatabaseAdded, this.indexedDBAdded, this, {scoped: true});
SDK.TargetManager.TargetManager.instance().addModelListener(
IndexedDBModel, IndexedDBModelEvents.DatabaseRemoved, this.indexedDBRemoved, this, {scoped: true});
SDK.TargetManager.TargetManager.instance().addModelListener(
IndexedDBModel, IndexedDBModelEvents.DatabaseLoaded, this.indexedDBLoaded, this, {scoped: true});
SDK.TargetManager.TargetManager.instance().addModelListener(
IndexedDBModel, IndexedDBModelEvents.IndexedDBContentUpdated, this.indexedDBContentUpdated, this,
{scoped: true});
// TODO(szuend): Replace with a Set once two web tests no longer directly access this private
// variable (indexeddb/live-update-indexeddb-content.js, indexeddb/delete-entry.js).
this.idbDatabaseTreeElements = [];
for (const indexedDBModel of SDK.TargetManager.TargetManager.instance().models(IndexedDBModel, {scoped: true})) {
const databases = indexedDBModel.databases();
for (let j = 0; j < databases.length; ++j) {
this.addIndexedDB(indexedDBModel, databases[j]);
}
}
}
addIndexedDBForModel(model: IndexedDBModel): void {
for (const databaseId of model.databases()) {
this.addIndexedDB(model, databaseId);
}
}
removeIndexedDBForModel(model: IndexedDBModel): void {
const idbDatabaseTreeElements = this.idbDatabaseTreeElements.filter(element => element.model === model);
for (const idbDatabaseTreeElement of idbDatabaseTreeElements) {
this.removeIDBDatabaseTreeElement(idbDatabaseTreeElement);
}
}
override onattach(): void {
super.onattach();
this.listItemElement.addEventListener('contextmenu', this.handleContextMenuEvent.bind(this), true);
}
private handleContextMenuEvent(event: MouseEvent): void {
const contextMenu = new UI.ContextMenu.ContextMenu(event);
contextMenu.defaultSection().appendItem(
i18nString(UIStrings.refreshIndexeddb), this.refreshIndexedDB.bind(this), {jslogContext: 'refresh-indexeddb'});
void contextMenu.show();
}
refreshIndexedDB(): void {
for (const indexedDBModel of SDK.TargetManager.TargetManager.instance().models(IndexedDBModel, {scoped: true})) {
void indexedDBModel.refreshDatabaseNames();
}
}
private databaseInTree(databaseId: DatabaseId): boolean {
if (this.storageBucket) {
return databaseId.inBucket(this.storageBucket);
}
return true;
}
private indexedDBAdded({
data: {databaseId, model},
}: Common.EventTarget.EventTargetEvent<{databaseId: DatabaseId, model: IndexedDBModel}>): void {
this.addIndexedDB(model, databaseId);
}
private addIndexedDB(model: IndexedDBModel, databaseId: DatabaseId): void {
if (!this.databaseInTree(databaseId)) {
return;
}
const idbDatabaseTreeElement = new IDBDatabaseTreeElement(this.resourcesPanel, model, databaseId);
this.idbDatabaseTreeElements.push(idbDatabaseTreeElement);
this.appendChild(idbDatabaseTreeElement);
model.refreshDatabase(databaseId);
}
private indexedDBRemoved({
data: {databaseId, model},
}: Common.EventTarget.EventTargetEvent<{databaseId: DatabaseId, model: IndexedDBModel}>): void {
const idbDatabaseTreeElement = this.idbDatabaseTreeElement(model, databaseId);
if (!idbDatabaseTreeElement) {
return;
}
this.removeIDBDatabaseTreeElement(idbDatabaseTreeElement);
}
private removeIDBDatabaseTreeElement(idbDatabaseTreeElement: IDBDatabaseTreeElement): void {
idbDatabaseTreeElement.clear();
this.removeChild(idbDatabaseTreeElement);
Platform.ArrayUtilities.removeElement(this.idbDatabaseTreeElements, idbDatabaseTreeElement);
this.setExpandable(this.childCount() > 0);
}
private indexedDBLoaded(
{data: {database, model, entriesUpdated}}: Common.EventTarget
.EventTargetEvent<{database: IndexedDBModelDatabase, model: IndexedDBModel, entriesUpdated: boolean}>): void {
const idbDatabaseTreeElement = this.idbDatabaseTreeElement(model, database.databaseId);
if (!idbDatabaseTreeElement) {
return;
}
idbDatabaseTreeElement.update(database, entriesUpdated);
this.indexedDBLoadedForTest();
}
private indexedDBLoadedForTest(): void {
// For sniffing in tests.
}
private indexedDBContentUpdated({
data: {databaseId, objectStoreName, model},
}: Common.EventTarget.EventTargetEvent<{databaseId: DatabaseId, objectStoreName: string, model: IndexedDBModel}>):
void {
const idbDatabaseTreeElement = this.idbDatabaseTreeElement(model, databaseId);
if (!idbDatabaseTreeElement) {
return;
}
idbDatabaseTreeElement.indexedDBContentUpdated(objectStoreName);
}
private idbDatabaseTreeElement(model: IndexedDBModel, databaseId: DatabaseId): IDBDatabaseTreeElement|null {
return this.idbDatabaseTreeElements.find(x => x.databaseId.equals(databaseId) && x.model === model) || null;
}
}
export class IDBDatabaseTreeElement extends ApplicationPanelTreeElement {
model: IndexedDBModel;
databaseId: DatabaseId;
private readonly idbObjectStoreTreeElements: Map<string, IDBObjectStoreTreeElement>;
private database?: IndexedDBModelDatabase;
private view?: LegacyWrapper.LegacyWrapper.LegacyWrapper<UI.Widget.VBox, IDBDatabaseView>;
constructor(storagePanel: ResourcesPanel, model: IndexedDBModel, databaseId: DatabaseId) {
super(storagePanel, databaseId.name, false, 'indexed-db-database');
this.model = model;
this.databaseId = databaseId;
this.idbObjectStoreTreeElements = new Map();
const icon = createIcon('database');
this.setLeadingIcons([icon]);
this.model.addEventListener(IndexedDBModelEvents.DatabaseNamesRefreshed, this.refreshIndexedDB, this);
}
override get itemURL(): Platform.DevToolsPath.UrlString {
return 'indexedDB://' + this.databaseId.storageBucket.storageKey + '/' +
(this.databaseId.storageBucket.name ?? '') + '/' + this.databaseId.name as Platform.DevToolsPath.UrlString;
}
override onattach(): void {
super.onattach();
this.listItemElement.addEventListener('contextmenu', this.handleContextMenuEvent.bind(this), true);
}
private handleContextMenuEvent(event: MouseEvent): void {
const contextMenu = new UI.ContextMenu.ContextMenu(event);
contextMenu.defaultSection().appendItem(
i18nString(UIStrings.refreshIndexeddb), this.refreshIndexedDB.bind(this), {jslogContext: 'refresh-indexeddb'});
void contextMenu.show();
}
private refreshIndexedDB(): void {
this.model.refreshDatabase(this.databaseId);
}
indexedDBContentUpdated(objectStoreName: string): void {
const treeElement = this.idbObjectStoreTreeElements.get(objectStoreName);
if (treeElement) {
treeElement.markNeedsRefresh();
}
}
update(database: IndexedDBModelDatabase, entriesUpdated: boolean): void {
this.database = database;
const objectStoreNames = new Set<string>();
for (const objectStoreName of [...this.database.objectStores.keys()].sort()) {
const objectStore = this.database.objectStores.get(objectStoreName);
if (!objectStore) {
continue;
}
objectStoreNames.add(objectStore.name);
let treeElement = this.idbObjectStoreTreeElements.get(objectStore.name);
if (!treeElement) {
treeElement = new IDBObjectStoreTreeElement(this.resourcesPanel, this.model, this.databaseId, objectStore);
this.idbObjectStoreTreeElements.set(objectStore.name, treeElement);
this.appendChild(treeElement);
}
treeElement.update(objectStore, entriesUpdated);
}
for (const objectStoreName of this.idbObjectStoreTreeElements.keys()) {
if (!objectStoreNames.has(objectStoreName)) {
this.objectStoreRemoved(objectStoreName);
}
}
if (this.view) {
this.view.getComponent().update(database);
}
this.updateTooltip();
}
private updateTooltip(): void {
const version = this.database ? this.database.version : '-';
if (Object.keys(this.idbObjectStoreTreeElements).length === 0) {
this.tooltip = i18nString(UIStrings.versionSEmpty, {PH1: version});
} else {
this.tooltip = i18nString(UIStrings.versionS, {PH1: version});
}
}
override get selectable(): boolean {
if (!this.database) {
return false;
}
return super.selectable;
}
override onselect(selectedByUser?: boolean): boolean {
super.onselect(selectedByUser);
if (!this.database) {
return false;
}
if (!this.view) {
this.view = LegacyWrapper.LegacyWrapper.legacyWrapper(
UI.Widget.VBox, new IDBDatabaseView(this.model, this.database), 'indexeddb-data');
}
this.showView(this.view);
Host.userMetrics.panelShown('indexed-db');
return false;
}
private objectStoreRemoved(objectStoreName: string): void {
const objectStoreTreeElement = this.idbObjectStoreTreeElements.get(objectStoreName);
if (objectStoreTreeElement) {
objectStoreTreeElement.clear();
this.removeChild(objectStoreTreeElement);
}
this.idbObjectStoreTreeElements.delete(objectStoreName);
this.updateTooltip();
}
clear(): void {
for (const objectStoreName of this.idbObjectStoreTreeElements.keys()) {
this.objectStoreRemoved(objectStoreName);
}
}
}
export class IDBObjectStoreTreeElement extends ApplicationPanelTreeElement {
private model: IndexedDBModel;
private databaseId: DatabaseId;
private readonly idbIndexTreeElements: Map<string, IDBIndexTreeElement>;
private objectStore: ObjectStore;
private view: IDBDataView|null;
constructor(storagePanel: ResourcesPanel, model: IndexedDBModel, databaseId: DatabaseId, objectStore: ObjectStore) {
super(storagePanel, objectStore.name, false, 'indexed-db-object-store');
this.model = model;
this.databaseId = databaseId;
this.idbIndexTreeElements = new Map();
this.objectStore = objectStore;
this.view = null;
const icon = createIcon('table');
this.setLeadingIcons([icon]);
}
override get itemURL(): Platform.DevToolsPath.UrlString {
return 'indexedDB://' + this.databaseId.storageBucket.storageKey + '/' +
(this.databaseId.storageBucket.name ?? '') + '/' + this.databaseId.name + '/' +
this.objectStore.name as Platform.DevToolsPath.UrlString;
}
override onattach(): void {
super.onattach();
this.listItemElement.addEventListener('contextmenu', this.handleContextMenuEvent.bind(this), true);
}
markNeedsRefresh(): void {
if (this.view) {
this.view.markNeedsRefresh();
}
for (const treeElement of this.idbIndexTreeElements.values()) {
treeElement.markNeedsRefresh();
}
}
private handleContextMenuEvent(event: MouseEvent): void {
const contextMenu = new UI.ContextMenu.ContextMenu(event);
contextMenu.defaultSection().appendItem(
i18nString(UIStrings.clear), this.clearObjectStore.bind(this), {jslogContext: 'clear'});
void contextMenu.show();
}
private refreshObjectStore(): void {
if (this.view) {
this.view.refreshData();
}
for (const treeElement of this.idbIndexTreeElements.values()) {
treeElement.refreshIndex();
}
}
private async clearObjectStore(): Promise<void> {
await this.model.clearObjectStore(this.databaseId, this.objectStore.name);
this.update(this.objectStore, true);
}
update(objectStore: ObjectStore, entriesUpdated: boolean): void {
this.objectStore = objectStore;
const indexNames = new Set<string>();
for (const index of this.objectStore.indexes.values()) {
indexNames.add(index.name);
let treeElement = this.idbIndexTreeElements.get(index.name);
if (!treeElement) {
treeElement = new IDBIndexTreeElement(
this.resourcesPanel, this.model, this.databaseId, this.objectStore, index,
this.refreshObjectStore.bind(this));
this.idbIndexTreeElements.set(index.name, treeElement);
this.appendChild(treeElement);
}
treeElement.update(this.objectStore, index, entriesUpdated);
}
for (const indexName of this.idbIndexTreeElements.keys()) {
if (!indexNames.has(indexName)) {
this.indexRemoved(indexName);
}
}
for (const [indexName, treeElement] of this.idbIndexTreeElements.entries()) {
if (!indexNames.has(indexName)) {
this.removeChild((treeElement));
this.idbIndexTreeElements.delete((indexName));
}
}
if (this.childCount()) {
this.expand();
}
if (this.view && entriesUpdated) {
this.view.update(this.objectStore, null);
}
this.updateTooltip();
}
private updateTooltip(): void {
const keyPathString = this.objectStore.keyPathString;
let tooltipString = keyPathString !== null ? i18nString(UIStrings.keyPathS, {PH1: keyPathString}) : '';
if (this.objectStore.autoIncrement) {
tooltipString += '\n' + i18n.i18n.lockedString('autoIncrement');
}
this.tooltip = tooltipString;
}
override onselect(selectedByUser?: boolean): boolean {
super.onselect(selectedByUser);
if (!this.view) {
this.view =
new IDBDataView(this.model, this.databaseId, this.objectStore, null, this.refreshObjectStore.bind(this));
}
this.showView(this.view);
Host.userMetrics.panelShown('indexed-db');
return false;
}
private indexRemoved(indexName: string): void {
const indexTreeElement = this.idbIndexTreeElements.get(indexName);
if (indexTreeElement) {
indexTreeElement.clear();
this.removeChild(indexTreeElement);
}
this.idbIndexTreeElements.delete(indexName);
}
clear(): void {
for (const indexName of this.idbIndexTreeElements.keys()) {
this.indexRemoved(indexName);
}
if (this.view) {
this.view.clear();
}
}
}
export class IDBIndexTreeElement extends ApplicationPanelTreeElement {
private model: IndexedDBModel;
private databaseId: DatabaseId;
private objectStore: ObjectStore;
private index: Index;
private refreshObjectStore: () => void;
private view?: IDBDataView;
constructor(
storagePanel: ResourcesPanel, model: IndexedDBModel, databaseId: DatabaseId, objectStore: ObjectStore,
index: Index, refreshObjectStore: () => void) {
super(storagePanel, index.name, false, 'indexed-db');
this.model = model;
this.databaseId = databaseId;
this.objectStore = objectStore;
this.index = index;
this.refreshObjectStore = refreshObjectStore;
}
override get itemURL(): Platform.DevToolsPath.UrlString {
return 'indexedDB://' + this.databaseId.storageBucket.storageKey + '/' +
(this.databaseId.storageBucket.name ?? '') + '/' + this.databaseId.name + '/' + this.objectStore.name + '/' +
this.index.name as Platform.DevToolsPath.UrlString;
}
markNeedsRefresh(): void {
if (this.view) {
this.view.markNeedsRefresh();
}
}
refreshIndex(): void {
if (this.view) {
this.view.refreshData();
}
}
update(objectStore: ObjectStore, index: Index, entriesUpdated: boolean): void {
this.objectStore = objectStore;
this.index = index;
if (this.view && entriesUpdated) {
this.view.update(this.objectStore, this.index);
}
this.updateTooltip();
}
private updateTooltip(): void {
const tooltipLines = [];
const keyPathString = this.index.keyPathString;
tooltipLines.push(i18nString(UIStrings.keyPathS, {PH1: keyPathString}));
if (this.index.unique) {
tooltipLines.push(i18n.i18n.lockedString('unique'));
}
if (this.index.multiEntry) {
tooltipLines.push(i18n.i18n.lockedString('multiEntry'));
}
this.tooltip = tooltipLines.join('\n');
}
override onselect(selectedByUser?: boolean): boolean {
super.onselect(selectedByUser);
if (!this.view) {
this.view = new IDBDataView(this.model, this.databaseId, this.objectStore, this.index, this.refreshObjectStore);
}
this.showView(this.view);
Host.userMetrics.panelShown('indexed-db');
return false;
}
clear(): void {
if (this.view) {
this.view.clear();
}
}
}
export class DOMStorageTreeElement extends ApplicationPanelTreeElement {
private readonly domStorage: DOMStorage;
constructor(storagePanel: ResourcesPanel, domStorage: DOMStorage) {
super(
storagePanel,
domStorage.storageKey ? SDK.StorageKeyManager.parseStorageKey(domStorage.storageKey).origin :
i18nString(UIStrings.localFiles),
false, domStorage.isLocalStorage ? 'local-storage-for-domain' : 'session-storage-for-domain');
this.domStorage = domStorage;
const icon = createIcon('table');
this.setLeadingIcons([icon]);
}
override get itemURL(): Platform.DevToolsPath.UrlString {
return 'storage://' + this.domStorage.storageKey + '/' + (this.domStorage.isLocalStorage ? 'local' : 'session') as
Platform.DevToolsPath.UrlString;
}
override onselect(selectedByUser?: boolean): boolean {
super.onselect(selectedByUser);
Host.userMetrics.panelShown('dom-storage');
this.resourcesPanel.showDOMStorage(this.domStorage);
return false;
}
override onattach(): void {
super.onattach();
this.listItemElement.addEventListener('contextmenu', this.handleContextMenuEvent.bind(this), true);
}
private handleContextMenuEvent(event: MouseEvent): void {
const contextMenu = new UI.ContextMenu.ContextMenu(event);
contextMenu.defaultSection().appendItem(
i18nString(UIStrings.clear), () => this.domStorage.clear(), {jslogContext: 'clear'});
void contextMenu.show();
}
}
export class ExtensionStorageTreeElement extends ApplicationPanelTreeElement {
private readonly extensionStorage: ExtensionStorage;
constructor(storagePanel: ResourcesPanel, extensionStorage: ExtensionStorage) {
super(
storagePanel, nameForExtensionStorageArea(extensionStorage.storageArea), false, 'extension-storage-for-domain');
this.extensionStorage = extensionStorage;
const icon = createIcon('table');
this.setLeadingIcons([icon]);
}
get storageArea(): Protocol.Extensions.StorageArea {
return this.extensionStorage.storageArea;
}
override get itemURL(): Platform.DevToolsPath.UrlString {
return 'extension-storage://' + this.extensionStorage.extensionId + '/' + this.extensionStorage.storageArea as
Platform.DevToolsPath.UrlString;
}
override onselect(selectedByUser?: boolean): boolean {
super.onselect(selectedByUser);
this.resourcesPanel.showExtensionStorage(this.extensionStorage);
Host.userMetrics.panelShown('extension-storage');
return false;
}
override onattach(): void {
super.onattach();
this.listItemElement.addEventListener('contextmenu', this.handleContextMenuEvent.bind(this), true);
}
private handleContextMenuEvent(event: MouseEvent): void {
const contextMenu = new UI.ContextMenu.ContextMenu(event);
contextMenu.defaultSection().appendItem(
i18nString(UIStrings.clear), () => this.extensionStorage.clear(), {jslogContext: 'clear'});
void contextMenu.show();
}
}
export class ExtensionStorageTreeParentElement extends ApplicationPanelTreeElement {
private readonly extensionId: string;
constructor(storagePanel: ResourcesPanel, extensionId: string, extensionName: string) {
super(storagePanel, extensionName || extensionId, true, 'extension-storage-for-domain');
this.extensionId = extensionId;
const icon = createIcon('table');
this.setLeadingIcons([icon]);
}
override get itemURL(): Platform.DevToolsPath.UrlString {
return 'extension-storage://' + this.extensionId as Platform.DevToolsPath.UrlString;
}
}
export class CookieTreeElement extends ApplicationPanelTreeElement {
private readonly target: SDK.Target.Target;
readonly #cookieDomain: string;
constructor(
storagePanel: ResourcesPanel, frame: SDK.ResourceTreeModel.ResourceTreeFrame,
cookieUrl: Common.ParsedURL.ParsedURL) {
super(storagePanel, cookieUrl.securityOrigin() || i18nString(UIStrings.localFiles), false, 'cookies-for-frame');
this.target = frame.resourceTreeModel().target();
this.#cookieDomain = cookieUrl.securityOrigin();
this.tooltip = i18nString(UIStrings.cookiesUsedByFramesFromS, {PH1: this.#cookieDomain});
const icon = createIcon('cookie');
// Note that we cannot use `cookieDomainInternal` here since it contains scheme.
if (IssuesManager.RelatedIssue.hasThirdPartyPhaseoutCookieIssueForDomain(cookieUrl.domain())) {
icon.name = 'warning-filled';
this.tooltip = i18nString(UIStrings.thirdPartyPhaseout, {PH1: this.#cookieDomain});
}
this.setLeadingIcons([icon]);
}
override get itemURL(): Platform.DevToolsPath.UrlString {
return 'cookies://' + this.#cookieDomain as Platform.DevToolsPath.UrlString;
}
cookieDomain(): string {
return this.#cookieDomain;
}
override onattach(): void {
super.onattach();
this.listItemElement.addEventListener('contextmenu', this.handleContextMenuEvent.bind(this), true);
}
private handleContextMenuEvent(event: Event): void {
const contextMenu = new UI.ContextMenu.ContextMenu(event);
contextMenu.defaultSection().appendItem(
i18nString(UIStrings.clear), () => this.resourcesPanel.clearCookies(this.target, this.#cookieDomain),
{jslogContext: 'clear'});
void contextMenu.show();
}
override onselect(selectedByUser?: boolean): boolean {
super.onselect(selectedByUser);
this.resourcesPanel.showCookies(this.target, this.#cookieDomain);
Host.userMetrics.panelShown(Host.UserMetrics.PanelCodes[Host.UserMetrics.PanelCodes.cookies]);
return false;
}
}
export class StorageCategoryView extends UI.Widget.VBox {
private emptyWidget: UI.EmptyWidget.EmptyWidget;
constructor() {
super();
this.element.classList.add('storage-view');
this.emptyWidget = new UI.EmptyWidget.EmptyWidget('', '');
this.emptyWidget.show(this.element);
}
setText(text: string): void {
this.emptyWidget.text = text;
}
setHeadline(header: string): void {
this.emptyWidget.header = header;
}
setLink(link: Platform.DevToolsPath.UrlString|null): void {
this.emptyWidget.link = link;
}
}
export class ResourcesSection implements SDK.TargetManager.Observer {
panel: ResourcesPanel;
private readonly treeElement: UI.TreeOutline.TreeElement;
private treeElementForFrameId: Map<string, FrameTreeElement>;
private treeElementForTargetId: Map<string, FrameTreeElement>;
constructor(storagePanel: ResourcesPanel, treeElement: UI.TreeOutline.TreeElement) {
this.panel = storagePanel;
this.treeElement = treeElement;
UI.ARIAUtils.setLabel(this.treeElement.listItemNode, 'Resources Section');
this.treeElementForFrameId = new Map();
this.treeElementForTargetId = new Map();
const frameManager = SDK.FrameManager.FrameManager.instance();
frameManager.addEventListener(
SDK.FrameManager.Events.FRAME_ADDED_TO_TARGET, event => this.frameAdded(event.data.frame), this);
frameManager.addEventListener(
SDK.FrameManager.Events.FRAME_REMOVED, event => this.frameDetached(event.data.frameId), this);
frameManager.addEventListener(
SDK.FrameManager.Events.FRAME_NAVIGATED, event => this.frameNavigated(event.data.frame), this);
frameManager.addEventListener(
SDK.FrameManager.Events.RESOURCE_ADDED, event => this.resourceAdded(event.data.resource), this);
SDK.TargetManager.TargetManager.instance().addModelListener(
SDK.ChildTargetManager.ChildTargetManager, SDK.ChildTargetManager.Events.TARGET_CREATED, this.windowOpened,
this, {scoped: true});
SDK.TargetManager.TargetManager.instance().addModelListener(
SDK.ChildTargetManager.ChildTargetManager, SDK.ChildTargetManager.Events.TARGET_INFO_CHANGED,
this.windowChanged, this, {scoped: true});
SDK.TargetManager.TargetManager.instance().addModelListener(
SDK.ChildTargetManager.ChildTargetManager, SDK.ChildTargetManager.Events.TARGET_DESTROYED, this.windowDestroyed,
this, {scoped: true});
SDK.TargetManager.TargetManager.instance().observeTargets(this, {scoped: true});
}
private initialize(): void {
const frameManager = SDK.FrameManager.FrameManager.instance();
for (const frame of frameManager.getAllFrames()) {
if (!this.treeElementForFrameId.get(frame.id)) {
this.addFrameAndParents(frame);
}
const childTargetManager = frame.resourceTreeModel().target().model(SDK.ChildTargetManager.ChildTargetManager);
if (childTargetManager) {
for (const targetInfo of childTargetManager.targetInfos()) {
this.windowOpened({data: targetInfo});
}
}
}
}
targetAdded(target: SDK.Target.Target): void {
if (target.type() === SDK.Target.Type.Worker || target.type() === SDK.Target.Type.ServiceWorker) {
void this.workerAdded(target);
}
if (target.type() === SDK.Target.Type.FRAME && target === target.outermostTarget()) {
// Process existing frames, e.g. after prerendering activation or
// switching between outermost targets.
this.initialize();
}
}
private async workerAdded(target: SDK.Target.Target): Promise<void> {
const parentTarget = target.parentTarget();
if (!parentTarget) {
return;
}
const parentTargetId = parentTarget.id();
const frameTreeElement = this.treeElementForTargetId.get(parentTargetId);
const targetId = target.id();
assertNotMainTarget(targetId);
const {targetInfo} = await parentTarget.targetAgent().invoke_getTargetInfo({targetId});
if (frameTreeElement && targetInfo) {
frameTreeElement.workerCreated(targetInfo);
}
}
targetRemoved(_target: SDK.Target.Target): void {
}
private addFrameAndParents(frame: SDK.ResourceTreeModel.ResourceTreeFrame): void {
const parentFrame = frame.parentFrame();
if (parentFrame && !this.treeElementForFrameId.get(parentFrame.id)) {
this.addFrameAndParents(parentFrame);
}
this.frameAdded(frame);
}
private expandFrame(frame: SDK.ResourceTreeModel.ResourceTreeFrame|null): boolean {
if (!frame) {
return false;
}
let treeElement = this.treeElementForFrameId.get(frame.id);
if (!treeElement && !this.expandFrame(frame.parentFrame())) {
return false;
}
treeElement = this.treeElementForFrameId.get(frame.id);
if (!treeElement) {
return false;
}
treeElement.expand();
return true;
}
async revealResource(resource: SDK.Resource.Resource, line?: number, column?: number): Promise<void> {
if (!this.expandFrame(resource.frame())) {
return;
}
const resourceTreeElement = FrameResourceTreeElement.forResource(resource);
if (resourceTreeElement) {
await resourceTreeElement.revealResource(line, column);
}
}
revealAndSelectFrame(frame: SDK.ResourceTreeModel.ResourceTreeFrame): void {
const frameTreeElement = this.treeElementForFrameId.get(frame.id);
frameTreeElement?.reveal();
frameTreeElement?.select();
}
private frameAdded(frame: SDK.ResourceTreeModel.ResourceTreeFrame): void {
if (!SDK.TargetManager.TargetManager.instance().isInScope(frame.resourceTreeModel())) {
return;
}
const parentFrame = frame.parentFrame();
const parentTreeElement = parentFrame ? this.treeElementForFrameId.get(parentFrame.id) : this.treeElement;
if (!parentTreeElement) {
return;
}
const existingElement = this.treeElementForFrameId.get(frame.id);
if (existingElement) {
this.treeElementForFrameId.delete(frame.id);
if (existingElement.parent) {
existingElement.parent.removeChild(existingElement);
}
}
const frameTreeElement = new FrameTreeElement(this, frame);
this.treeElementForFrameId.set(frame.id, frameTreeElement);
const targetId = frame.resourceTreeModel().target().id();
if (!this.treeElementForTargetId.get(targetId)) {
this.treeElementForTargetId.set(targetId, frameTreeElement);
}
parentTreeElement.appendChild(frameTreeElement);
for (const resource of frame.resources()) {
this.resourceAdded(resource);
}
}
private frameDetached(frameId: Protocol.Page.FrameId): void {
const frameTreeElement = this.treeElementForFrameId.get(frameId);
if (!frameTreeElement) {
return;
}
this.treeElementForFrameId.delete(frameId);
if (frameTreeElement.parent) {
frameTreeElement.parent.removeChild(frameTreeElement);
}
}
private frameNavigated(frame: SDK.ResourceTreeModel.ResourceTreeFrame): void {
if (!SDK.TargetManager.TargetManager.instance().isInScope(frame.resourceTreeModel())) {
return;
}
const frameTreeElement = this.treeElementForFrameId.get(frame.id);
if (frameTreeElement) {
void frameTreeElement.frameNavigated(frame);
}
}
private resourceAdded(resource: SDK.Resource.Resource): void {
const frame = resource.frame();
if (!frame) {
return;
}
if (!SDK.TargetManager.TargetManager.instance().isInScope(frame.resourceTreeModel())) {
return;
}
const frameTreeElement = this.treeElementForFrameId.get(frame.id);
if (!frameTreeElement) {
// This is a frame's main resource, it will be retained
// and re-added by the resource manager;
return;
}
frameTreeElement.appendResource(resource);
}
private windowOpened(event: Common.EventTarget.EventTargetEvent<Protocol.Target.TargetInfo>): void {
const targetInfo = event.data;
// Events for DevTools windows are ignored because they do not have an openerId
if (targetInfo.openerId && targetInfo.type === 'page') {
const frameTreeElement = this.treeElementForFrameId.get(targetInfo.openerId);
if (frameTreeElement) {
this.treeElementForTargetId.set(targetInfo.targetId, frameTreeElement);
frameTreeElement.windowOpened(targetInfo);
}
}
}
private windowDestroyed(event: Common.EventTarget.EventTargetEvent<Protocol.Target.TargetID>): void {
const targetId = event.data;
const frameTreeElement = this.treeElementForTargetId.get(targetId);
if (frameTreeElement) {
frameTreeElement.windowDestroyed(targetId);
this.treeElementForTargetId.delete(targetId);
}
}
private windowChanged(event: Common.EventTarget.EventTargetEvent<Protocol.Target.TargetInfo>): void {
const targetInfo = event.data;
// Events for DevTools windows are ignored because they do not have an openerId
if (targetInfo.openerId && targetInfo.type === 'page') {
const frameTreeElement = this.treeElementForFrameId.get(targetInfo.openerId);
if (frameTreeElement) {
frameTreeElement.windowChanged(targetInfo);
}
}
}
reset(): void {
this.treeElement.removeChildren();
this.treeElementForFrameId.clear();
this.treeElementForTargetId.clear();
}
}
export class FrameTreeElement extends ApplicationPanelTreeElement {
private section: ResourcesSection;
private frame: SDK.ResourceTreeModel.ResourceTreeFrame;
private readonly categoryElements: Map<string, ExpandableApplicationPanelTreeElement>;
private readonly treeElementForResource: Map<string, FrameResourceTreeElement>;
private treeElementForWindow: Map<Protocol.Target.TargetID, FrameWindowTreeElement>;
private treeElementForWorker: Map<Protocol.Target.TargetID, WorkerTreeElement>;
private view: FrameDetailsReportView|null;
constructor(section: ResourcesSection, frame: SDK.ResourceTreeModel.ResourceTreeFrame) {
super(section.panel, '', false, 'frame');
this.section = section;
this.frame = frame;
this.categoryElements = new Map();
this.treeElementForResource = new Map();
this.treeElementForWindow = new Map();
this.treeElementForWorker = new Map();
void this.frameNavigated(frame);
this.view = null;
}
getIconTypeForFrame(frame: SDK.ResourceTreeModel.ResourceTreeFrame): 'frame-crossed'|'frame'|'iframe-crossed'|
'iframe' {
if (frame.isOutermostFrame()) {
return frame.unreachableUrl() ? 'frame-crossed' : 'frame';
}
return frame.unreachableUrl() ? 'iframe-crossed' : 'iframe';
}
async frameNavigated(frame: SDK.ResourceTreeModel.ResourceTreeFrame): Promise<void> {
const icon = createIcon(this.getIconTypeForFrame(frame));
if (frame.unreachableUrl()) {
icon.classList.add('red-icon');
}
this.setLeadingIcons([icon]);
this.invalidateChildren();
if (this.title !== frame.displayName()) {
this.title = frame.displayName();
UI.ARIAUtils.setLabel(this.listItemElement, this.title);
if (this.parent) {
const parent = this.parent;
// Insert frame at new position to preserve correct alphabetical order
parent.removeChild(this);
parent.appendChild(this);
}
}
this.categoryElements.clear();
this.treeElementForResource.clear();
this.treeElementForWorker.clear();
if (this.selected) {
this.view = new FrameDetailsReportView();
this.view.frame = this.frame;
this.showView(this.view);
} else {
this.view = null;
}
// Service Workers' parent is always the outermost frame. We need to reconstruct
// the service worker tree elements after those navigations which allow
// the service workers to stay alive.
if (frame.isOutermostFrame()) {
const targets = SDK.TargetManager.TargetManager.instance().targets();
for (const target of targets) {
if (target.type() === SDK.Target.Type.ServiceWorker &&
SDK.TargetManager.TargetManager.instance().isInScope(target)) {
const targetId = target.id();
assertNotMainTarget(targetId);
const agent = frame.resourceTreeModel().target().targetAgent();
const targetInfo = (await agent.invoke_getTargetInfo({targetId})).targetInfo;
this.workerCreated(targetInfo);
}
}
}
}
override get itemURL(): Platform.DevToolsPath.UrlString {
// This is used to persist over reloads/navigation which frame was selected.
// A frame's title can change on DevTools refresh, so we resort to using
// the URL instead (even though it is not guaranteed to be unique).
if (this.frame.isOutermostFrame()) {
return 'frame://' as Platform.DevToolsPath.UrlString;
}
return 'frame://' + encodeURI(this.frame.url) as Platform.DevToolsPath.UrlString;
}
override onselect(selectedByUser?: boolean): boolean {
super.onselect(selectedByUser);
if (!this.view) {
this.view = new FrameDetailsReportView();
this.view.frame = this.frame;
}
Host.userMetrics.panelShown('frame-details');
this.showView(this.view);
this.listItemElement.classList.remove('hovered');
SDK.OverlayModel.OverlayModel.hideDOMNodeHighlight();
return false;
}
set hovered(hovered: boolean) {
if (hovered) {
this.listItemElement.classList.add('hovered');
void this.frame.highlight();
} else {
this.listItemElement.classList.remove('hovered');
SDK.OverlayModel.OverlayModel.hideDOMNodeHighlight();
}
}
appendResource(resource: SDK.Resource.Resource): void {
const statusCode = resource.statusCode();
if (statusCode >= 301 && statusCode <= 303) {
return;
}
const resourceType = resource.resourceType();
const categoryName = resourceType.name();
let categoryElement =
resourceType === Common.ResourceType.resourceTypes.Document ? this : this.categoryElements.get(categoryName);
if (!categoryElement) {
categoryElement = new ExpandableApplicationPanelTreeElement(
this.section.panel, resource.resourceType().category().title(), '', i18nString(UIStrings.resourceDescription),
categoryName, categoryName === 'Frames');
this.categoryElements.set(resourceType.name(), categoryElement);
this.appendChild(categoryElement, FrameTreeElement.presentationOrderCompare);
}
const resourceTreeElement = new FrameResourceTreeElement(this.section.panel, resource);
categoryElement.appendChild(resourceTreeElement, FrameTreeElement.presentationOrderCompare);
this.treeElementForResource.set(resource.url, resourceTreeElement);
}
windowOpened(targetInfo: Protocol.Target.TargetInfo): void {
const categoryKey = 'opened-windows';
let categoryElement = this.categoryElements.get(categoryKey);
if (!categoryElement) {
categoryElement = new ExpandableApplicationPanelTreeElement(
this.section.panel, i18nString(UIStrings.openedWindows), '', i18nString(UIStrings.openedWindowsDescription),
categoryKey);
this.categoryElements.set(categoryKey, categoryElement);
this.appendChild(categoryElement, FrameTreeElement.presentationOrderCompare);
}
if (!this.treeElementForWindow.get(targetInfo.targetId)) {
const windowTreeElement = new FrameWindowTreeElement(this.section.panel, targetInfo);
categoryElement.appendChild(windowTreeElement);
this.treeElementForWindow.set(targetInfo.targetId, windowTreeElement);
}
}
workerCreated(targetInfo: Protocol.Target.TargetInfo): void {
const categoryKey = targetInfo.type === 'service_worker' ? 'service-workers' : 'web-workers';
const categoryName = targetInfo.type === 'service_worker' ? i18n.i18n.lockedString('Service workers') :
i18nString(UIStrings.webWorkers);
let categoryElement = this.categoryElements.get(categoryKey);
if (!categoryElement) {
categoryElement = new ExpandableApplicationPanelTreeElement(
this.section.panel, categoryName, '', i18nString(UIStrings.workerDescription), categoryKey);
this.categoryElements.set(categoryKey, categoryElement);
this.appendChild(categoryElement, FrameTreeElement.presentationOrderCompare);
}
if (!this.treeElementForWorker.get(targetInfo.targetId)) {
const workerTreeElement = new WorkerTreeElement(this.section.panel, targetInfo);
categoryElement.appendChild(workerTreeElement);
this.treeElementForWorker.set(targetInfo.targetId, workerTreeElement);
}
}
windowChanged(targetInfo: Protocol.Target.TargetInfo): void {
const windowTreeElement = this.treeElementForWindow.get(targetInfo.targetId);
if (!windowTreeElement) {
return;
}
if (windowTreeElement.title !== targetInfo.title) {
windowTreeElement.title = targetInfo.title;
}
windowTreeElement.update(targetInfo);
}
windowDestroyed(targetId: Protocol.Target.TargetID): void {
const windowTreeElement = this.treeElementForWindow.get(targetId);
if (windowTreeElement) {
windowTreeElement.windowClosed();
}
}
override appendChild(
treeElement: UI.TreeOutline.TreeElement,
comparator: ((arg0: UI.TreeOutline.TreeElement, arg1: UI.TreeOutline.TreeElement) => number)|
undefined = FrameTreeElement.presentationOrderCompare): void {
super.appendChild(treeElement, comparator);
}
/**
* Order elements by type (first frames, then resources, last Document resources)
* and then each of these groups in the alphabetical order.
*/
private static presentationOrderCompare(
treeElement1: UI.TreeOutline.TreeElement, treeElement2: UI.TreeOutline.TreeElement): number {
function typeWeight(treeElement: UI.TreeOutline.TreeElement): number {
if (treeElement instanceof ExpandableApplicationPanelTreeElement) {
return 2;
}
if (treeElement instanceof FrameTreeElement) {
return 1;
}
return 3;
}
const typeWeight1 = typeWeight(treeElement1);
const typeWeight2 = typeWeight(treeElement2);
return typeWeight1 - typeWeight2 || treeElement1.titleAsText().localeCompare(treeElement2.titleAsText());
}
}
const resourceToFrameResourceTreeElement = new WeakMap<SDK.Resource.Resource, FrameResourceTreeElement>();
export class FrameResourceTreeElement extends ApplicationPanelTreeElement {
private readonly panel: ResourcesPanel;
private resource: SDK.Resource.Resource;
private previewPromise: Promise<UI.Widget.Widget>|null;
constructor(storagePanel: ResourcesPanel, resource: SDK.Resource.Resource) {
super(
storagePanel, resource.isGenerated ? i18nString(UIStrings.documentNotAvailable) : resource.displayName, false,
'frame-resource');
this.panel = storagePanel;
this.resource = resource;
this.previewPromise = null;
this.tooltip = resource.url;
resourceToFrameResourceTreeElement.set(this.resource, this);
const icon = createIcon('document', 'navigator-file-tree-item');
icon.classList.add('navigator-' + resource.resourceType().name() + '-tree-item');
this.setLeadingIcons([icon]);
}
static forResource(resource: SDK.Resource.Resource): FrameResourceTreeElement|undefined {
return resourceToFrameResourceTreeElement.get(resource);
}
override get itemURL(): Platform.DevToolsPath.UrlString {
return this.resource.url;
}
private preparePreview(): Promise<UI.Widget.Widget> {
if (this.previewPromise) {
return this.previewPromise;
}
const viewPromise = SourceFrame.PreviewFactory.PreviewFactory.createPreview(this.resource, this.resource.mimeType);
this.previewPromise = viewPromise.then(view => {
if (view) {
return view;
}
return new UI.EmptyWidget.EmptyWidget('', this.resource.url);
});
return this.previewPromise;
}
override onselect(selectedByUser?: boolean): boolean {
super.onselect(selectedByUser);
if (this.resource.isGenerated) {
this.panel.showCategoryView(
'', i18nString(UIStrings.documentNotAvailable), i18nString(UIStrings.theContentOfThisDocumentHasBeen), null);
} else {
void this.panel.scheduleShowView(this.preparePreview());
}
Host.userMetrics.panelShown('frame-resource');
return false;
}
override ondblclick(_event: Event): boolean {
Host.InspectorFrontendHost.InspectorFrontendHostInstance.openInNewTab(this.resource.url);
return false;
}
override onattach(): void {
super.onattach();
this.listItemElement.draggable = true;
this.listItemElement.addEventListener('dragstart', this.ondragstart.bind(this), false);
this.listItemElement.addEventListener('contextmenu', this.handleContextMenuEvent.bind(this), true);
}
private ondragstart(event: DragEvent): boolean {
if (!event.dataTransfer) {
return false;
}
event.dataTransfer.setData('text/plain', this.resource.content || '');
event.dataTransfer.effectAllowed = 'copy';
return true;
}
private handleContextMenuEvent(event: MouseEvent): void {
const contextMenu = new UI.ContextMenu.ContextMenu(event);
contextMenu.appendApplicableItems(this.resource);
void contextMenu.show();
}
async revealResource(lineNumber?: number, columnNumber?: number): Promise<void> {
this.revealAndSelect(true);
const view = await this.panel.scheduleShowView(this.preparePreview());
if (!(view instanceof SourceFrame.ResourceSourceFrame.ResourceSourceFrame) || typeof lineNumber !== 'number') {
return;
}
view.revealPosition({lineNumber, columnNumber}, true);
}
}
class FrameWindowTreeElement extends ApplicationPanelTreeElement {
private targetInfo: Protocol.Target.TargetInfo;
private isWindowClosed: boolean;
private view: OpenedWindowDetailsView|null;
constructor(storagePanel: ResourcesPanel, targetInfo: Protocol.Target.TargetInfo) {
super(storagePanel, targetInfo.title || i18nString(UIStrings.windowWithoutTitle), false, 'window');
this.targetInfo = targetInfo;
this.isWindowClosed = false;
this.view = null;
this.updateIcon(targetInfo.canAccessOpener);
}
updateIcon(canAccessOpener: boolean): void {
const iconType = canAccessOpener ? 'popup' : 'frame';
const icon = createIcon(iconType);
this.setLeadingIcons([icon]);
}
update(targetInfo: Protocol.Target.TargetInfo): void {
if (targetInfo.canAccessOpener !== this.targetInfo.canAccessOpener) {
this.updateIcon(targetInfo.canAccessOpener);
}
this.targetInfo = targetInfo;
if (this.view) {
this.view.setTargetInfo(targetInfo);
this.view.requestUpdate();
}
}
windowClosed(): void {
this.listItemElement.classList.add('window-closed');
this.isWindowClosed = true;
if (this.view) {
this.view.setIsWindowClosed(true);
this.view.requestUpdate();
}
}
override onselect(selectedByUser?: boolean): boolean {
super.onselect(selectedByUser);
if (!this.view) {
this.view = new OpenedWindowDetailsView(this.targetInfo, this.isWindowClosed);
} else {
this.view.requestUpdate();
}
this.showView(this.view);
Host.userMetrics.panelShown('frame-window');
return false;
}
override get itemURL(): Platform.DevToolsPath.UrlString {
return this.targetInfo.url as Platform.DevToolsPath.UrlString;
}
}
class WorkerTreeElement extends ApplicationPanelTreeElement {
private targetInfo: Protocol.Target.TargetInfo;
private view: WorkerDetailsView|null;
constructor(storagePanel: ResourcesPanel, targetInfo: Protocol.Target.TargetInfo) {
super(storagePanel, targetInfo.title || targetInfo.url || i18nString(UIStrings.worker), false, 'worker');
this.targetInfo = targetInfo;
this.view = null;
const icon = createIcon('gears', 'navigator-file-tree-item');
this.setLeadingIcons([icon]);
}
override onselect(selectedByUser?: boolean): boolean {
super.onselect(selectedByUser);
if (!this.view) {
this.view = new WorkerDetailsView(this.targetInfo);
} else {
this.view.requestUpdate();
}
this.showView(this.view);
Host.userMetrics.panelShown('frame-worker');
return false;
}
override get itemURL(): Platform.DevToolsPath.UrlString {
return this.targetInfo.url as Platform.DevToolsPath.UrlString;
}
}