// 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 Root from '../../core/root/root.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 {DeviceBoundSessionsModel} from './DeviceBoundSessionsModel.js';
import {RootTreeElement as DeviceBoundSessionsRootTreeElement} from './DeviceBoundSessionsTreeElement.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';
import {WebMCPTreeElement} from './WebMCPTreeElement.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;
  webMcpTreeElement?: WebMCPTreeElement;
  deviceBoundSessionsRootTreeElement: DeviceBoundSessionsRootTreeElement|undefined;
  deviceBoundSessionsModel: DeviceBoundSessionsModel|undefined;
  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);
    if (Root.Runtime.hostConfig.devToolsWebMCPSupport?.enabled) {
      this.webMcpTreeElement = new WebMCPTreeElement(panel);
      this.applicationTreeElement.appendChild(this.webMcpTreeElement);
    }

    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);

    if (Root.Runtime.hostConfig.deviceBoundSessionsDebugging?.enabled) {
      this.deviceBoundSessionsModel = new DeviceBoundSessionsModel();
      this.deviceBoundSessionsRootTreeElement =
          new DeviceBoundSessionsRootTreeElement(panel, this.deviceBoundSessionsModel);
      backgroundServiceTreeElement.appendChild(this.deviceBoundSessionsRootTreeElement);
    }

    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();
    this.deviceBoundSessionsModel?.clearVisibleSites();
    this.deviceBoundSessionsModel?.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);

      // Update device bound session visibility for new cookie domain.
      if (this.deviceBoundSessionsModel) {
        const target = frame.resourceTreeModel().target();
        const networkAgent = target.networkAgent();
        void networkAgent.invoke_fetchSchemefulSite({origin: domain}).then(response => {
          if (response.getError() || !this.deviceBoundSessionsModel) {
            return;
          }
          // Confirm the domain is still present first.
          if (this.domains[domain]) {
            this.deviceBoundSessionsModel.addVisibleSite(response.schemefulSite);
          }
        });
      }
    }
  }

  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);
    }
  }

  showStorageBucket(bucketInfo: Protocol.Storage.StorageBucketInfo): void {
    const bucketsModel = SDK.TargetManager.TargetManager.instance().primaryPageTarget()?.model(
        SDK.StorageBucketsModel.StorageBucketsModel);
    if (bucketsModel) {
      this.storageBucketsTreeElement?.getBucketTreeElement(bucketsModel, bucketInfo)?.revealAndSelect(true);
    }
  }

  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);

    if (this.panel.mode !== 'node') {
      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;
  }
}
