UNPKG

chrome-devtools-frontend

Version:
950 lines (867 loc) • 37.1 kB
// Copyright 2015 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import '../../ui/components/report_view/report_view.js'; import '../../ui/kit/kit.js'; 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 SDK from '../../core/sdk/sdk.js'; import type * as Protocol from '../../generated/protocol.js'; import * as NetworkForward from '../../panels/network/forward/forward.js'; import * as Buttons from '../../ui/components/buttons/buttons.js'; import * as Components from '../../ui/legacy/components/utils/utils.js'; import * as UI from '../../ui/legacy/legacy.js'; import {Directives, html, type LitTemplate, nothing, render} from '../../ui/lit/lit.js'; import * as VisualLogging from '../../ui/visual_logging/visual_logging.js'; import * as MobileThrottling from '../mobile_throttling/mobile_throttling.js'; import * as ApplicationComponents from './components/components.js'; import serviceWorkersViewStyles from './serviceWorkersView.css.js'; import serviceWorkerUpdateCycleViewStyles from './serviceWorkerUpdateCycleView.css.js'; import {ServiceWorkerUpdateCycleView} from './ServiceWorkerUpdateCycleView.js'; const UIStrings = { /** * @description Text for linking to other Service Worker registrations */ serviceWorkersFromOtherOrigins: 'Service workers from other origins', /** * @description Title of update on reload setting in service workers view of the application panel */ updateOnReload: 'Update on reload', /** * @description Tooltip text that appears on the setting when hovering over it in Service Workers View of the Application panel */ onPageReloadForceTheService: 'On page reload, force the `service worker` to update, and activate it', /** * @description Title of bypass service worker setting in service workers view of the application panel */ bypassForNetwork: 'Bypass for network', /** * @description Tooltip text that appears on the setting when hovering over it in Service Workers View of the Application panel */ bypassTheServiceWorkerAndLoad: 'Bypass the `service worker` and load resources from the network', /** * @description Screen reader title for a section of the Service Workers view of the Application panel * @example {https://example.com} PH1 */ serviceWorkerForS: '`Service worker` for {PH1}', /** * @description Text in Service Workers View of the Application panel */ testPushMessageFromDevtools: 'Test push message from DevTools.', /** * @description Button label for service worker network requests */ networkRequests: 'Network requests', /** * @description Label for a button in the Service Workers View of the Application panel. * Imperative noun. Clicking the button will refresh the list of service worker registrations. */ update: 'Update', /** * @description Text in Service Workers View of the Application panel */ unregisterServiceWorker: 'Unregister service worker', /** * @description Text in Service Workers View of the Application panel */ unregister: 'Unregister', /** * @description Text for the source of something */ source: 'Source', /** * @description Text for the status of something */ status: 'Status', /** * @description Text in Service Workers View of the Application panel */ clients: 'Clients', /** * @description Text in Service Workers View of the Application panel. Label for a section of the * tool which allows the developer to send a test push message to the service worker. */ pushString: 'Push', /** * @description Text in Service Workers View of the Application panel. Placeholder text for where * the user can type in the data they want to push to the service worker i.e. the 'push data'. Noun * phrase. */ pushData: 'Push data', /** * @description Text in Service Workers View of the Application panel */ syncString: 'Sync', /** * @description Placeholder text for the input box where a user is asked for a test tag to sync. This is used as a compound noun, not as a verb. */ syncTag: 'Sync tag', /** * @description Text for button in Service Workers View of the Application panel that dispatches a periodicsync event */ periodicSync: 'Periodic sync', /** * @description Default tag for a periodicsync event in Service Workers View of the Application panel */ periodicSyncTag: 'Periodic sync tag', /** * @description Aria accessible name in Service Workers View of the Application panel * @example {3} PH1 */ sRegistrationErrors: '{PH1} registration errors', /** * @description Text in Service Workers View of the Application panel. The Date/time that a service * worker version update was received by the webpage. * @example {7/3/2019, 3:38:37 PM} PH1 */ receivedS: 'Received {PH1}', /** **@description Text in Service Workers View of the Application panel. */ routers: 'Routers', /** * @description Text in Service Workers View of the Application panel * @example {example.com} PH1 */ sDeleted: '{PH1} - deleted', /** * @description Text in Service Workers View of the Application panel * @example {1} PH1 * @example {stopped} PH2 */ sActivatedAndIsS: '#{PH1} activated and is {PH2}', /** * @description Text in Service Workers View of the Application panel */ stopString: 'Stop', /** * @description Text in Service Workers View of the Application panel */ startString: 'Start', /** * @description Text in Service Workers View of the Application panel. Service workers have * different versions, which are labelled with numbers e.g. version #2. This text indicates that a * particular version is now redundant (it was replaced by a newer version). # means 'number' here. * @example {2} PH1 */ sIsRedundant: '#{PH1} is redundant', /** * @description Text in Service Workers View of the Application panel * @example {2} PH1 */ sWaitingToActivate: '#{PH1} waiting to activate', /** * @description Text in Service Workers View of the Application panel * @example {2} PH1 */ sTryingToInstall: '#{PH1} trying to install', /** * @description Text in Service Workers Update Timeline. Update is a noun. */ updateCycle: 'Update Cycle', /** * @description Text of a DOM element in Service Workers View of the Application panel * @example {example.com} PH1 */ workerS: 'Worker: {PH1}', /** * @description Link text in Service Workers View of the Application panel. When the link is clicked, * the focus is moved to the service worker's client page. */ focus: 'focus', /** * @description Link to view all the Service Workers that have been registered. */ seeAllRegistrations: 'See all registrations', } as const; const str_ = i18n.i18n.registerUIStrings('panels/application/ServiceWorkersView.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); const {until} = Directives; const {widget} = UI.Widget; const {bindToSetting} = UI.UIUtils; export interface SectionData { manager: SDK.ServiceWorkerManager.ServiceWorkerManager; registration: SDK.ServiceWorkerManager.ServiceWorkerRegistration; } export interface ServiceWorkersViewInput { canManageServiceWorkers: boolean; sections: SectionData[]; } function renderToolbar(): LitTemplate { const updateOnReloadSetting = Common.Settings.Settings.instance().createSetting('service-worker-update-on-reload', false); const bypassServiceWorkerSetting = Common.Settings.Settings.instance().createSetting('bypass-service-worker', false); // clang-format off return html`<devtools-toolbar class="service-worker-toolbar"> ${MobileThrottling.ThrottlingManager.throttlingManager().createOfflineToolbarCheckbox().element} <devtools-checkbox title=${i18nString(UIStrings.onPageReloadForceTheService)} ${bindToSetting(updateOnReloadSetting)}> ${i18nString(UIStrings.updateOnReload)} </devtools-checkbox> <devtools-checkbox title=${i18nString(UIStrings.bypassTheServiceWorkerAndLoad)} ${bindToSetting(bypassServiceWorkerSetting)}> ${i18nString(UIStrings.bypassForNetwork)} </devtools-checkbox> </devtools-toolbar>`; // clang-format on } function renderOthersOriginView(): LitTemplate { // clang-format off return html`<div class="service-workers-other-origin" jslog=${VisualLogging.section('other-origin')}> <devtools-report> <devtools-report-section-header> ${i18nString(UIStrings.serviceWorkersFromOtherOrigins)} </devtools-report-section-header> <div class="service-worker-section"> <devtools-link href="chrome://serviceworker-internals" jslogcontext="view-all" .allowPrivileged=${true}> ${i18nString(UIStrings.seeAllRegistrations)} </devtools-link> </div> </devtools-report> </div>`; // clang-format on } function getTimeStamp(registration: SDK.ServiceWorkerManager.ServiceWorkerRegistration): number { const versions = registration.versionsByMode(); let timestamp: number|undefined = 0; const active = versions.get(SDK.ServiceWorkerManager.ServiceWorkerVersion.Modes.ACTIVE); const installing = versions.get(SDK.ServiceWorkerManager.ServiceWorkerVersion.Modes.INSTALLING); const waiting = versions.get(SDK.ServiceWorkerManager.ServiceWorkerVersion.Modes.WAITING); const redundant = versions.get(SDK.ServiceWorkerManager.ServiceWorkerVersion.Modes.REDUNDANT); if (active) { timestamp = active.scriptResponseTime; } else if (waiting) { timestamp = waiting.scriptResponseTime; } else if (installing) { timestamp = installing.scriptResponseTime; } else if (redundant) { timestamp = redundant.scriptResponseTime; } return timestamp || 0; } function renderOriginReport(input: ServiceWorkersViewInput): LitTemplate { if (!input.canManageServiceWorkers) { return nothing; } const sortedSections = [...input.sections]; sortedSections.sort((a, b) => { const aTimestamp = getTimeStamp(a.registration); const bTimestamp = getTimeStamp(b.registration); return bTimestamp - aTimestamp; }); // clang-format off return html`<div class="service-workers-this-origin" jslog=${VisualLogging.section('this-origin')}> <devtools-report .data=${{reportTitle: i18n.i18n.lockedString('Service workers')}}> <div class="service-worker-toolbar" slot="toolbar">${renderToolbar()}</div> ${sortedSections.map(section => html`<devtools-widget class="service-worker-section-container" ${widget(Section, {section})}></devtools-widget>`)} </devtools-report> </div>`; // clang-format on } type View = (input: ServiceWorkersViewInput, output: undefined, target: HTMLElement) => void; export const DEFAULT_VIEW: View = (input, _output, target): void => { // clang-format off render(html` <!-- This Origin Report --> ${renderOriginReport(input)} ${renderOthersOriginView()}`, target, { container: { classes: [ 'service-worker-list', (input.sections.length > 0 ? 'service-worker-has-current' : 'service-worker-list-empty') ] } }); // clang-format on }; let throttleDisabledForDebugging = false; export const setThrottleDisabledForDebugging = (enable: boolean): void => { throttleDisabledForDebugging = enable; }; export class ServiceWorkersView extends UI.Widget.VBox implements SDK.TargetManager.SDKModelObserver<SDK.ServiceWorkerManager.ServiceWorkerManager> { private readonly sections: Map<SDK.ServiceWorkerManager.ServiceWorkerRegistration, SectionData>; private manager: SDK.ServiceWorkerManager.ServiceWorkerManager|null; private securityOriginManager: SDK.SecurityOriginManager.SecurityOriginManager|null; private readonly eventListeners: Map<SDK.ServiceWorkerManager.ServiceWorkerManager, Common.EventTarget.EventDescriptor[]>; readonly #output = undefined; #view: (input: ServiceWorkersViewInput, output: undefined, target: HTMLElement) => void; constructor(view: View = DEFAULT_VIEW) { super({ jslog: `${VisualLogging.pane('service-workers')}`, useShadowDom: true, }); this.#view = view; this.registerRequiredCSS(serviceWorkersViewStyles); this.sections = new Map(); this.manager = null; this.securityOriginManager = null; this.eventListeners = new Map(); SDK.TargetManager.TargetManager.instance().observeModels(SDK.ServiceWorkerManager.ServiceWorkerManager, this); } override wasShown(): void { super.wasShown(); this.requestUpdate(); } override async performUpdate(): Promise<void> { if (this.manager) { for (const registration of this.manager.registrations().values()) { const isCurrent = this.isOriginCurrent(registration.securityOrigin); if (isCurrent && !this.sections.has(registration)) { this.sections.set(registration, {manager: this.manager, registration}); } else if (!isCurrent && this.sections.has(registration)) { this.sections.delete(registration); } } } const input: ServiceWorkersViewInput = { canManageServiceWorkers: this.manager !== null, sections: Array.from(this.sections.values()).map(data => ({...data})), }; this.#view(input, this.#output, this.contentElement); } modelAdded(serviceWorkerManager: SDK.ServiceWorkerManager.ServiceWorkerManager): void { if (serviceWorkerManager.target() !== SDK.TargetManager.TargetManager.instance().primaryPageTarget()) { return; } this.manager = serviceWorkerManager; this.securityOriginManager = (serviceWorkerManager.target().model(SDK.SecurityOriginManager.SecurityOriginManager) as SDK.SecurityOriginManager.SecurityOriginManager); for (const registration of this.manager.registrations().values()) { this.updateRegistration(registration); } this.eventListeners.set(serviceWorkerManager, [ this.manager.addEventListener(SDK.ServiceWorkerManager.Events.REGISTRATION_UPDATED, this.registrationUpdated, this), this.manager.addEventListener(SDK.ServiceWorkerManager.Events.REGISTRATION_DELETED, this.registrationDeleted, this), this.securityOriginManager.addEventListener(SDK.SecurityOriginManager.Events.SecurityOriginAdded, this.requestUpdate, this), this.securityOriginManager.addEventListener(SDK.SecurityOriginManager.Events.SecurityOriginRemoved, this.requestUpdate, this), ]); } modelRemoved(serviceWorkerManager: SDK.ServiceWorkerManager.ServiceWorkerManager): void { if (!this.manager || this.manager !== serviceWorkerManager) { return; } Common.EventTarget.removeEventListeners(this.eventListeners.get(serviceWorkerManager) || []); this.eventListeners.delete(serviceWorkerManager); this.manager = null; this.securityOriginManager = null; } private registrationUpdated( event: Common.EventTarget.EventTargetEvent<SDK.ServiceWorkerManager.ServiceWorkerRegistration>): void { this.updateRegistration(event.data); this.gcRegistrations(); } private gcRegistrations(): void { if (!this.manager || !this.securityOriginManager) { return; } let hasNonDeletedRegistrations = false; const securityOrigins = new Set<string>(this.securityOriginManager.securityOrigins()); for (const registration of this.manager.registrations().values()) { if (!securityOrigins.has(registration.securityOrigin) && !this.isRegistrationVisible(registration)) { continue; } if (!registration.canBeRemoved()) { hasNonDeletedRegistrations = true; break; } } if (!hasNonDeletedRegistrations) { return; } for (const registration of this.manager.registrations().values()) { const visible = securityOrigins.has(registration.securityOrigin) || this.isRegistrationVisible(registration); if (!visible && registration.canBeRemoved()) { this.removeRegistrationFromList(registration); } } } private isOriginCurrent(origin: string): boolean { if (this.securityOriginManager && (this.securityOriginManager.securityOrigins().includes(origin) || this.securityOriginManager.unreachableMainSecurityOrigin() === origin)) { return true; } return false; } private updateRegistration(registration: SDK.ServiceWorkerManager.ServiceWorkerRegistration, skipUpdate?: boolean): void { if (!this.manager) { return; } let sectionData = this.sections.get(registration); if (!sectionData) { if (!this.isOriginCurrent(registration.securityOrigin)) { return; } sectionData = {manager: this.manager, registration}; this.sections.set(registration, sectionData); } if (skipUpdate) { return; } this.requestUpdate(); } private registrationDeleted( event: Common.EventTarget.EventTargetEvent<SDK.ServiceWorkerManager.ServiceWorkerRegistration>): void { this.removeRegistrationFromList(event.data); } private removeRegistrationFromList(registration: SDK.ServiceWorkerManager.ServiceWorkerRegistration, skipVisibilityUpdate = false): void { this.sections.delete(registration); if (!skipVisibilityUpdate) { this.requestUpdate(); } } private isRegistrationVisible(registration: SDK.ServiceWorkerManager.ServiceWorkerRegistration): boolean { if (!registration.scopeURL) { return true; } return false; } } export interface SectionViewInput { title: string; isDeleted: boolean; errorsLength: number; pushData: string; syncTag: string; periodicSyncTag: string; updateCycleTable: HTMLElement; activeVersion?: SDK.ServiceWorkerManager.ServiceWorkerVersion; waitingVersion?: SDK.ServiceWorkerManager.ServiceWorkerVersion; installingVersion?: SDK.ServiceWorkerManager.ServiceWorkerVersion; redundantVersion?: SDK.ServiceWorkerManager.ServiceWorkerVersion; renderClientInfo: (clientId: Protocol.Target.TargetID) => Promise<LitTemplate|typeof nothing>; onNetworkRequests: () => void; onUpdate: () => void; onUnregister: () => void; onPush: (data: string) => void; onSync: (tag: string) => void; onPeriodicSync: (tag: string) => void; onStop: (versionId: string) => void; onStart: () => void; onSkipWaiting: () => void; } function renderHeaderButtons(input: SectionViewInput): LitTemplate { // clang-format off return html` <devtools-button .data=${{ variant: Buttons.Button.Variant.TEXT, title: i18nString(UIStrings.networkRequests), jslogContext: 'show-network-requests' } as Buttons.Button.ButtonData} .disabled=${input.isDeleted} @click=${input.onNetworkRequests}> ${i18nString(UIStrings.networkRequests)} </devtools-button> <devtools-button .data=${{ variant: Buttons.Button.Variant.TEXT, title: i18nString(UIStrings.update), jslogContext: 'update' } as Buttons.Button.ButtonData} .disabled=${input.isDeleted} @click=${input.onUpdate}> ${i18nString(UIStrings.update)} </devtools-button> <devtools-button .data=${{ variant: Buttons.Button.Variant.TEXT, title: i18nString(UIStrings.unregisterServiceWorker), jslogContext: 'unregister' } as Buttons.Button.ButtonData} .disabled=${input.isDeleted} @click=${input.onUnregister}> ${i18nString(UIStrings.unregister)} </devtools-button>`; // clang-format on } function renderSyncNotificationField(label: string, initialValue: string, placeholder: string, callback: (arg0: string) => void, jslogContext: string): LitTemplate { // clang-format off return html` <div class="report-field"> <div class="report-field-name">${label}</div> <div class="report-field-value"> <form class="service-worker-editor-with-button" @submit=${(e: Event) => { const {editor} = e.target as HTMLFormElement; callback(editor.value || ''); e.consume(true); }}> <input name="editor" class="source-code service-worker-notification-editor harmony-input" type="text" .value=${initialValue} placeholder=${placeholder} aria-label=${label} .spellcheck=${false} jslog=${VisualLogging.textField().track({change: true}).context(jslogContext)} > <devtools-button .data=${{ type: 'submit', variant: Buttons.Button.Variant.OUTLINED, jslogContext} as Buttons.Button.ButtonData}> ${label} </devtools-button> </form> </div> </div>`; // clang-format on } function renderVersion(icon: string, label: string, content: LitTemplate = nothing): LitTemplate { // clang-format off return html` <div class="service-worker-version"> <div class=${icon}></div> <span class="service-worker-version-string" role="alert" aria-live="polite"> ${label} </span> ${content} </div>`; // clang-format on } function renderClientsField(input: SectionViewInput, version?: SDK.ServiceWorkerManager.ServiceWorkerVersion): LitTemplate { if (!version?.controlledClients?.length) { return html`<div class="report-field"> <div class="report-field-name">${i18nString(UIStrings.clients)}</div> <div class="report-field-value"></div> </div>`; } // clang-format off return html`<div class="report-field"> <div class="report-field-name">${i18nString(UIStrings.clients)}</div> <div class="report-field-value"> ${version.controlledClients.map(client => html` <div class="service-worker-client"> ${until(input.renderClientInfo(client))} </div>`)} </div> </div>`; // clang-format on } function renderSourceField(input: SectionViewInput, version?: SDK.ServiceWorkerManager.ServiceWorkerVersion): LitTemplate { if (!version) { return html`<div class="report-field"> <div class="report-field-name">${i18nString(UIStrings.source)}</div> <div class="report-field-value"></div> </div>`; } const fileName = Common.ParsedURL.ParsedURL.extractName(version.scriptURL); // clang-format off return html`<div class="report-field"> <div class="report-field-name">${i18nString(UIStrings.source)}</div> <div class="report-field-value"> <div class="report-field-value-filename"> ${Components.Linkifier.Linkifier.renderLinkifiedUrl(version.scriptURL, { text: fileName, tabStop: true, jslogContext: 'source-location'})} ${input.errorsLength ? html` <button class="devtools-link link" tabindex="0" aria-label=${i18nString(UIStrings.sRegistrationErrors, {PH1: input.errorsLength})} @click=${() => Common.Console.Console.instance().show()}> <devtools-icon name="cross-circle-filled" class="error-icon"> </devtools-icon> ${input.errorsLength} </button>` : nothing} </div> ${version.scriptResponseTime !== undefined ? html` <div class="report-field-value-subtitle"> ${i18nString(UIStrings.receivedS, {PH1: new Date(version.scriptResponseTime * 1000).toLocaleString()})} </div> ` : nothing} </div> </div>`; // clang-format on } function renderStatusField(input: SectionViewInput, active?: SDK.ServiceWorkerManager.ServiceWorkerVersion, waiting?: SDK.ServiceWorkerManager.ServiceWorkerVersion, installing?: SDK.ServiceWorkerManager.ServiceWorkerVersion, redundant?: SDK.ServiceWorkerManager.ServiceWorkerVersion): LitTemplate { // clang-format off return html`<div class="report-field"> <div class="report-field-name">${i18nString(UIStrings.status)}</div> <div class="report-field-value"> <div class="service-worker-version-stack"> <div class="service-worker-version-stack-bar"></div> ${active ? renderVersion( 'service-worker-active-circle', i18nString(UIStrings.sActivatedAndIsS, { PH1: active.id, PH2: SDK.ServiceWorkerManager.ServiceWorkerVersion.RunningStatus[active.currentState.runningStatus](), }), active.isRunning() || active.isStarting() ? html` <devtools-button .data=${{jslogContext: 'stop', variant: Buttons.Button.Variant.OUTLINED} as Buttons.Button.ButtonData} @click=${() => input.onStop(active.id)}> ${i18nString(UIStrings.stopString)} </devtools-button>` : active.isStartable() ? html` <devtools-button .data=${{jslogContext: 'start', variant: Buttons.Button.Variant.OUTLINED} as Buttons.Button.ButtonData} @click=${input.onStart}> ${i18nString(UIStrings.startString)} </devtools-button>` : nothing) : redundant ? renderVersion( 'service-worker-redundant-circle', i18nString(UIStrings.sIsRedundant, {PH1: redundant.id})) : nothing} ${waiting ? renderVersion( 'service-worker-waiting-circle', i18nString(UIStrings.sWaitingToActivate, {PH1: waiting.id}), html` <devtools-button .data=${{ jslogContext: 'skip-waiting', title: i18n.i18n.lockedString('skipWaiting'), variant: Buttons.Button.Variant.OUTLINED} as Buttons.Button.ButtonData} @click=${input.onSkipWaiting}> ${i18n.i18n.lockedString('skipWaiting')} </devtools-button> ${waiting.scriptResponseTime !== undefined ? html` <div class="service-worker-subtitle"> ${i18nString(UIStrings.receivedS, {PH1: new Date(waiting.scriptResponseTime * 1000).toLocaleString()})} </div> ` : nothing} `, ) : nothing} ${installing ? renderVersion( 'service-worker-installing-circle', i18nString(UIStrings.sTryingToInstall, {PH1: installing.id}), installing.scriptResponseTime !== undefined ? html` <div class="service-worker-subtitle"> ${i18nString(UIStrings.receivedS, {PH1: new Date(installing.scriptResponseTime * 1000).toLocaleString()})} </div>` : nothing) : nothing} </div> </div> </div>`; // clang-format on } function renderUpdateCycleField(input: SectionViewInput): LitTemplate { return html` <div class="report-field"> <div class="report-field-name">${i18nString(UIStrings.updateCycle)}</div> <div class="report-field-value"> ${input.updateCycleTable} </div> </div>`; } function renderRouterField(input: SectionViewInput): LitTemplate { const active = input.activeVersion; const title = i18nString(UIStrings.routers); if (active?.routerRules && active.routerRules.length > 0) { // If there is at least one registered rule in the active version, append the router filed. // clang-format off return html` <div class="report-field"> <div class="report-field-name">${title}</div> <div class="report-field-value"> ${widget(ApplicationComponents.ServiceWorkerRouterView.ServiceWorkerRouterView, { rules: active.routerRules })} </div> </div>`; // clang-format on } return nothing; } type SectionView = (input: SectionViewInput, _output: undefined, target: HTMLElement) => void; export const DEFAULT_SECTION_VIEW: SectionView = (input: SectionViewInput, _output: undefined, target: HTMLElement): void => { // clang-format off render(html` <style>${serviceWorkersViewStyles}</style> <style>${serviceWorkerUpdateCycleViewStyles}</style> <devtools-report-section-header role="heading" aria-level="2" aria-label=${i18nString(UIStrings.serviceWorkerForS, { PH1: input.title })}> <span style="flex: 1 1 auto">${input.title}</span> ${renderHeaderButtons(input)} </devtools-report-section-header> <div class="service-worker-section"> ${renderSourceField(input, input.activeVersion ?? input.redundantVersion)} ${renderStatusField(input, input.activeVersion, input.waitingVersion, input.installingVersion, input.redundantVersion)} ${renderClientsField(input, input.activeVersion ?? input.redundantVersion)} ${renderSyncNotificationField(i18nString(UIStrings.pushString), input.pushData, i18nString(UIStrings.pushData), input.onPush, 'push-message')} ${renderSyncNotificationField(i18nString(UIStrings.syncString), input.syncTag, i18nString(UIStrings.syncTag), input.onSync, 'sync-tag')} ${renderSyncNotificationField(i18nString(UIStrings.periodicSync), input.periodicSyncTag, i18nString(UIStrings.periodicSyncTag), input.onPeriodicSync, 'periodic-sync-tag')} ${renderUpdateCycleField(input)} ${renderRouterField(input)} </div> `, target); // clang-format on }; export class Section extends UI.Widget.VBox { private manager!: SDK.ServiceWorkerManager.ServiceWorkerManager; registration!: SDK.ServiceWorkerManager.ServiceWorkerRegistration; private sectionInternal!: SectionData; private fingerprint: symbol|null; private pushNotificationDataSetting!: Common.Settings.Setting<string>; private syncTagNameSetting!: Common.Settings.Setting<string>; private periodicSyncTagNameSetting!: Common.Settings.Setting<string>; private updateCycleView!: ServiceWorkerUpdateCycleView; private readonly clientInfoCache: Map<string, Protocol.Target.TargetInfo>; private readonly throttler: Common.Throttler.Throttler; #view: SectionView; constructor(element: HTMLElement, view = DEFAULT_SECTION_VIEW) { super(element); this.fingerprint = null; this.clientInfoCache = new Map(); this.throttler = new Common.Throttler.Throttler(500); this.#view = view; } set section(data: SectionData) { const registrationChanged = !this.registration || this.registration !== data.registration; this.sectionInternal = data; this.manager = data.manager; this.registration = data.registration; if (!this.pushNotificationDataSetting) { this.pushNotificationDataSetting = Common.Settings.Settings.instance().createLocalSetting( 'push-data', i18nString(UIStrings.testPushMessageFromDevtools)); this.syncTagNameSetting = Common.Settings.Settings.instance().createLocalSetting('sync-tag-name', 'test-tag-from-devtools'); this.periodicSyncTagNameSetting = Common.Settings.Settings.instance().createLocalSetting('periodic-sync-tag-name', 'test-tag-from-devtools'); } if (registrationChanged) { this.updateCycleView = new ServiceWorkerUpdateCycleView(this.registration); this.clientInfoCache.clear(); } } get section(): SectionData { return this.sectionInternal; } getTitle(): string { const scopeURL = this.registration.scopeURL; return this.registration.isDeleted ? i18nString(UIStrings.sDeleted, {PH1: scopeURL}) : scopeURL; } override requestUpdate(): void { if (throttleDisabledForDebugging) { super.requestUpdate(); return; } void this.throttler.schedule(() => { super.requestUpdate(); return Promise.resolve(); }); } override performUpdate(): Promise<void> { const fingerprint = this.registration.fingerprint(); if (fingerprint === this.fingerprint) { return Promise.resolve(); } this.fingerprint = fingerprint; const versions = this.registration.versionsByMode(); const active = versions.get(SDK.ServiceWorkerManager.ServiceWorkerVersion.Modes.ACTIVE); const waiting = versions.get(SDK.ServiceWorkerManager.ServiceWorkerVersion.Modes.WAITING); const installing = versions.get(SDK.ServiceWorkerManager.ServiceWorkerVersion.Modes.INSTALLING); const redundant = versions.get(SDK.ServiceWorkerManager.ServiceWorkerVersion.Modes.REDUNDANT); const title = this.getTitle(); const input: SectionViewInput = { title, isDeleted: this.registration.isDeleted, errorsLength: this.registration.errors?.length ?? 0, pushData: this.pushNotificationDataSetting.get(), syncTag: this.syncTagNameSetting.get(), periodicSyncTag: this.periodicSyncTagNameSetting.get(), updateCycleTable: this.updateCycleView.tableElement, activeVersion: active, waitingVersion: waiting, installingVersion: installing, redundantVersion: redundant, renderClientInfo: this.renderClientInfo.bind(this), onNetworkRequests: this.networkRequestsClicked.bind(this), onUpdate: this.updateButtonClicked.bind(this), onUnregister: this.unregisterButtonClicked.bind(this), onPush: this.push.bind(this), onSync: this.sync.bind(this), onPeriodicSync: this.periodicSync.bind(this), onStop: this.stopButtonClicked.bind(this), onStart: this.startButtonClicked.bind(this), onSkipWaiting: this.skipButtonClicked.bind(this), }; this.#view(input, undefined, this.contentElement); this.updateCycleView.refresh(); return Promise.resolve(); } private unregisterButtonClicked(): void { this.manager.deleteRegistration(this.registration.id); } private updateButtonClicked(): void { void this.manager.updateRegistration(this.registration.id); } private networkRequestsClicked(): void { void Common.Revealer.reveal(NetworkForward.UIFilter.UIRequestFilter.filters([ { filterType: NetworkForward.UIFilter.FilterType.Is, filterValue: NetworkForward.UIFilter.IsFilterType.SERVICE_WORKER_INTERCEPTED, }, ])); Host.userMetrics.actionTaken(Host.UserMetrics.Action.ServiceWorkerNetworkRequestClicked); } private push(data: string): void { this.pushNotificationDataSetting.set(data); void this.manager.deliverPushMessage(this.registration.id, data); } private sync(tag: string): void { this.syncTagNameSetting.set(tag); void this.manager.dispatchSyncEvent(this.registration.id, tag, true); } private periodicSync(tag: string): void { this.periodicSyncTagNameSetting.set(tag); void this.manager.dispatchPeriodicSyncEvent(this.registration.id, tag); } private async renderClientInfo(clientId: Protocol.Target.TargetID): Promise<LitTemplate|typeof nothing> { let targetInfo = this.clientInfoCache.get(clientId); if (!targetInfo) { const response = await this.manager.target().targetAgent().invoke_getTargetInfo({targetId: clientId}); if (!response.targetInfo) { return nothing; } targetInfo = response.targetInfo; this.clientInfoCache.set(clientId, targetInfo); } if (targetInfo.type !== 'page' && targetInfo.type !== 'iframe') { // clang-format off return html`<span class="service-worker-client-string"> ${i18nString(UIStrings.workerS, {PH1: targetInfo.url})} </span>`; // clang-format on } // clang-format off return html` <span class="service-worker-client-string">${targetInfo.url}</span> <devtools-button .data=${{ iconName: 'select-element', variant: Buttons.Button.Variant.ICON, size: Buttons.Button.Size.SMALL, title: i18nString(UIStrings.focus), jslogContext: 'client-focus', } as Buttons.Button.ButtonData} class="service-worker-client-focus-link" @click=${this.activateTarget.bind(this, targetInfo.targetId)} ></devtools-button>`; // clang-format on } private activateTarget(targetId: Protocol.Target.TargetID): void { void this.manager.target().targetAgent().invoke_activateTarget({targetId}); } private startButtonClicked(): void { void this.manager.startWorker(this.registration.scopeURL); } private skipButtonClicked(): void { void this.manager.skipWaiting(this.registration.scopeURL); } private stopButtonClicked(versionId: string): void { void this.manager.stopWorker(versionId); } }