chrome-devtools-frontend
Version:
Chrome DevTools UI
903 lines (819 loc) • 39 kB
text/typescript
// Copyright (c) 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/* eslint-disable rulesdir/no-imperative-dom-api */
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 Logs from '../../models/logs/logs.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 * 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
*/
inspect: 'Inspect',
/**
*@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_);
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> {
currentWorkersView: UI.ReportView.ReportView;
private readonly toolbar: UI.Toolbar.Toolbar;
private readonly sections: Map<SDK.ServiceWorkerManager.ServiceWorkerRegistration, Section>;
private manager: SDK.ServiceWorkerManager.ServiceWorkerManager|null;
private securityOriginManager: SDK.SecurityOriginManager.SecurityOriginManager|null;
private readonly sectionToRegistration:
WeakMap<UI.ReportView.Section, SDK.ServiceWorkerManager.ServiceWorkerRegistration>;
private readonly eventListeners:
Map<SDK.ServiceWorkerManager.ServiceWorkerManager, Common.EventTarget.EventDescriptor[]>;
constructor() {
super(true);
this.registerRequiredCSS(serviceWorkersViewStyles);
// TODO(crbug.com/1156978): Replace UI.ReportView.ReportView with ReportView.ts web component.
this.currentWorkersView = new UI.ReportView.ReportView(i18n.i18n.lockedString('Service workers'));
this.currentWorkersView.setBodyScrollable(false);
this.contentElement.classList.add('service-worker-list');
this.contentElement.setAttribute('jslog', `${VisualLogging.pane('service-workers')}`);
this.currentWorkersView.show(this.contentElement);
this.currentWorkersView.element.classList.add('service-workers-this-origin');
this.currentWorkersView.element.setAttribute('jslog', `${VisualLogging.section('this-origin')}`);
this.toolbar = this.currentWorkersView.createToolbar();
this.sections = new Map();
this.manager = null;
this.securityOriginManager = null;
this.sectionToRegistration = new WeakMap();
const othersDiv = this.contentElement.createChild('div', 'service-workers-other-origin');
othersDiv.setAttribute('jslog', `${VisualLogging.section('other-origin')}`);
// TODO(crbug.com/1156978): Replace UI.ReportView.ReportView with ReportView.ts web component.
const othersView = new UI.ReportView.ReportView();
othersView.setHeaderVisible(false);
othersView.show(othersDiv);
const othersSection = othersView.appendSection(i18nString(UIStrings.serviceWorkersFromOtherOrigins));
const othersSectionRow = othersSection.appendRow();
const seeOthers =
UI.Fragment
.html`<a class="devtools-link" role="link" tabindex="0" href="chrome://serviceworker-internals" target="_blank" style="display: inline; cursor: pointer;">${
i18nString(UIStrings.seeAllRegistrations)}</a>`;
seeOthers.setAttribute('jslog', `${VisualLogging.link('view-all').track({click: true})}`);
self.onInvokeElement(seeOthers, event => {
const rootTarget = SDK.TargetManager.TargetManager.instance().rootTarget();
rootTarget &&
void rootTarget.targetAgent().invoke_createTarget({url: 'chrome://serviceworker-internals?devtools'});
event.consume(true);
});
othersSectionRow.appendChild(seeOthers);
this.toolbar.appendToolbarItem(
MobileThrottling.ThrottlingManager.throttlingManager().createOfflineToolbarCheckbox());
const updateOnReloadSetting =
Common.Settings.Settings.instance().createSetting('service-worker-update-on-reload', false);
updateOnReloadSetting.setTitle(i18nString(UIStrings.updateOnReload));
const forceUpdate =
new UI.Toolbar.ToolbarSettingCheckbox(updateOnReloadSetting, i18nString(UIStrings.onPageReloadForceTheService));
this.toolbar.appendToolbarItem(forceUpdate);
const bypassServiceWorkerSetting =
Common.Settings.Settings.instance().createSetting('bypass-service-worker', false);
bypassServiceWorkerSetting.setTitle(i18nString(UIStrings.bypassForNetwork));
const fallbackToNetwork = new UI.Toolbar.ToolbarSettingCheckbox(
bypassServiceWorkerSetting, i18nString(UIStrings.bypassTheServiceWorkerAndLoad));
this.toolbar.appendToolbarItem(fallbackToNetwork);
this.eventListeners = new Map();
SDK.TargetManager.TargetManager.instance().observeModels(SDK.ServiceWorkerManager.ServiceWorkerManager, this);
this.updateListVisibility();
const drawerChangeHandler = (event: Event): void => {
// @ts-expect-error: No support for custom event listener
const isDrawerOpen = event.detail?.isDrawerOpen;
if (this.manager && !isDrawerOpen) {
const {serviceWorkerNetworkRequestsPanelStatus: {isOpen, openedAt}} = this.manager;
if (isOpen) {
const networkLocation = UI.ViewManager.ViewManager.instance().locationNameForViewId('network');
UI.ViewManager.ViewManager.instance().showViewInLocation('network', networkLocation, false);
void Common.Revealer.reveal(NetworkForward.UIFilter.UIRequestFilter.filters([]));
const currentTime = Date.now();
const timeDifference = currentTime - openedAt;
if (timeDifference < 2000) {
Host.userMetrics.actionTaken(Host.UserMetrics.Action.ServiceWorkerNetworkRequestClosedQuickly);
}
this.manager.serviceWorkerNetworkRequestsPanelStatus = {
isOpen: false,
openedAt: 0,
};
}
}
};
document.body.addEventListener(UI.InspectorView.Events.DRAWER_CHANGE, drawerChangeHandler);
}
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.updateSectionVisibility, this),
this.securityOriginManager.addEventListener(
SDK.SecurityOriginManager.Events.SecurityOriginRemoved, this.updateSectionVisibility, 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 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;
}
private updateSectionVisibility(): void {
let hasThis = false;
const movedSections = [];
for (const section of this.sections.values()) {
const expectedView = this.getReportViewForOrigin(section.registration.securityOrigin);
hasThis = hasThis || expectedView === this.currentWorkersView;
if (section.section.parentWidget() !== expectedView) {
movedSections.push(section);
}
}
for (const section of movedSections) {
const registration = section.registration;
this.removeRegistrationFromList(registration);
this.updateRegistration(registration, true);
}
this.currentWorkersView.sortSections((aSection, bSection) => {
const aRegistration = this.sectionToRegistration.get(aSection);
const bRegistration = this.sectionToRegistration.get(bSection);
const aTimestamp = aRegistration ? this.getTimeStamp(aRegistration) : 0;
const bTimestamp = bRegistration ? this.getTimeStamp(bRegistration) : 0;
// the newest (largest timestamp value) should be the first
return bTimestamp - aTimestamp;
});
for (const section of this.sections.values()) {
if (section.section.parentWidget() === this.currentWorkersView ||
this.isRegistrationVisible(section.registration)) {
section.section.showWidget();
} else {
section.section.hideWidget();
}
}
this.contentElement.classList.toggle('service-worker-has-current', Boolean(hasThis));
this.updateListVisibility();
}
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 getReportViewForOrigin(origin: string): UI.ReportView.ReportView|null {
if (this.securityOriginManager &&
(this.securityOriginManager.securityOrigins().includes(origin) ||
this.securityOriginManager.unreachableMainSecurityOrigin() === origin)) {
return this.currentWorkersView;
}
return null;
}
private updateRegistration(registration: SDK.ServiceWorkerManager.ServiceWorkerRegistration, skipUpdate?: boolean):
void {
let section = this.sections.get(registration);
if (!section) {
const title = registration.scopeURL;
const reportView = this.getReportViewForOrigin(registration.securityOrigin);
if (!reportView) {
return;
}
const uiSection = reportView.appendSection(title);
uiSection.setUiGroupTitle(i18nString(UIStrings.serviceWorkerForS, {PH1: title}));
this.sectionToRegistration.set(uiSection, registration);
section = new Section((this.manager as SDK.ServiceWorkerManager.ServiceWorkerManager), uiSection, registration);
this.sections.set(registration, section);
}
if (skipUpdate) {
return;
}
this.updateSectionVisibility();
section.scheduleUpdate();
}
private registrationDeleted(
event: Common.EventTarget.EventTargetEvent<SDK.ServiceWorkerManager.ServiceWorkerRegistration>): void {
this.removeRegistrationFromList(event.data);
}
private removeRegistrationFromList(registration: SDK.ServiceWorkerManager.ServiceWorkerRegistration): void {
const section = this.sections.get(registration);
if (section) {
section.section.detach();
}
this.sections.delete(registration);
this.updateSectionVisibility();
}
private isRegistrationVisible(registration: SDK.ServiceWorkerManager.ServiceWorkerRegistration): boolean {
if (!registration.scopeURL) {
return true;
}
return false;
}
private updateListVisibility(): void {
this.contentElement.classList.toggle('service-worker-list-empty', this.sections.size === 0);
}
}
export class Section {
private manager: SDK.ServiceWorkerManager.ServiceWorkerManager;
section: UI.ReportView.Section;
registration: SDK.ServiceWorkerManager.ServiceWorkerRegistration;
private fingerprint: symbol|null;
private readonly pushNotificationDataSetting: Common.Settings.Setting<string>;
private readonly syncTagNameSetting: Common.Settings.Setting<string>;
private readonly periodicSyncTagNameSetting: Common.Settings.Setting<string>;
private readonly updateCycleView: ServiceWorkerUpdateCycleView;
private readonly routerView: ApplicationComponents.ServiceWorkerRouterView.ServiceWorkerRouterView;
private readonly networkRequests: Buttons.Button.Button;
private readonly updateButton: Buttons.Button.Button;
private readonly deleteButton: Buttons.Button.Button;
private sourceField: Element;
private readonly statusField: Element;
private readonly clientsField: Element;
private readonly clientInfoCache: Map<string, Protocol.Target.TargetInfo>;
private readonly throttler: Common.Throttler.Throttler;
private updateCycleField?: Element;
private routerField?: Element;
constructor(
manager: SDK.ServiceWorkerManager.ServiceWorkerManager, section: UI.ReportView.Section,
registration: SDK.ServiceWorkerManager.ServiceWorkerRegistration) {
this.manager = manager;
this.section = section;
this.registration = registration;
this.fingerprint = null;
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');
this.updateCycleView = new ServiceWorkerUpdateCycleView(registration);
this.routerView = new ApplicationComponents.ServiceWorkerRouterView.ServiceWorkerRouterView();
this.networkRequests = new Buttons.Button.Button();
this.networkRequests.data = {
iconName: 'bottom-panel-open',
variant: Buttons.Button.Variant.TEXT,
title: i18nString(UIStrings.networkRequests),
jslogContext: 'show-network-requests',
};
this.networkRequests.textContent = i18nString(UIStrings.networkRequests);
this.networkRequests.addEventListener('click', this.networkRequestsClicked.bind(this));
this.section.appendButtonToHeader(this.networkRequests);
this.updateButton = UI.UIUtils.createTextButton(
i18nString(UIStrings.update), this.updateButtonClicked.bind(this),
{variant: Buttons.Button.Variant.TEXT, title: i18nString(UIStrings.update), jslogContext: 'update'});
this.section.appendButtonToHeader(this.updateButton);
this.deleteButton =
UI.UIUtils.createTextButton(i18nString(UIStrings.unregister), this.unregisterButtonClicked.bind(this), {
variant: Buttons.Button.Variant.TEXT,
title: i18nString(UIStrings.unregisterServiceWorker),
jslogContext: 'unregister',
});
this.section.appendButtonToHeader(this.deleteButton);
// Preserve the order.
this.sourceField = this.wrapWidget(this.section.appendField(i18nString(UIStrings.source)));
this.statusField = this.wrapWidget(this.section.appendField(i18nString(UIStrings.status)));
this.clientsField = this.wrapWidget(this.section.appendField(i18nString(UIStrings.clients)));
this.createSyncNotificationField(
i18nString(UIStrings.pushString), this.pushNotificationDataSetting.get(), i18nString(UIStrings.pushData),
this.push.bind(this), 'push-message');
this.createSyncNotificationField(
i18nString(UIStrings.syncString), this.syncTagNameSetting.get(), i18nString(UIStrings.syncTag),
this.sync.bind(this), 'sync-tag');
this.createSyncNotificationField(
i18nString(UIStrings.periodicSync), this.periodicSyncTagNameSetting.get(),
i18nString(UIStrings.periodicSyncTag), tag => this.periodicSync(tag), 'periodic-sync-tag');
this.createUpdateCycleField();
this.maybeCreateRouterField();
this.clientInfoCache = new Map();
this.throttler = new Common.Throttler.Throttler(500);
}
private createSyncNotificationField(
label: string, initialValue: string, placeholder: string, callback: (arg0: string) => void,
jslogContext: string): void {
const form =
this.wrapWidget(this.section.appendField(label)).createChild('form', 'service-worker-editor-with-button');
const editor = UI.UIUtils.createInput('source-code service-worker-notification-editor');
editor.setAttribute('jslog', `${VisualLogging.textField().track({change: true}).context(jslogContext)}`);
form.appendChild(editor);
const button = UI.UIUtils.createTextButton(label, undefined, {jslogContext});
button.type = 'submit';
form.appendChild(button);
editor.value = initialValue;
editor.placeholder = placeholder;
UI.ARIAUtils.setLabel(editor, label);
form.addEventListener('submit', (e: Event) => {
callback(editor.value || '');
e.consume(true);
});
}
scheduleUpdate(): void {
if (throttleDisabledForDebugging) {
void this.update();
return;
}
void this.throttler.schedule(this.update.bind(this));
}
private targetForVersionId(versionId: string): SDK.Target.Target|null {
const version = this.manager.findVersion(versionId);
if (!version?.targetId) {
return null;
}
return SDK.TargetManager.TargetManager.instance().targetById(version.targetId);
}
private addVersion(versionsStack: Element, icon: string, label: string): Element {
const installingEntry = versionsStack.createChild('div', 'service-worker-version');
installingEntry.createChild('div', icon);
const statusString = installingEntry.createChild('span', 'service-worker-version-string');
statusString.textContent = label;
UI.ARIAUtils.markAsAlert(statusString);
return installingEntry;
}
private updateClientsField(version: SDK.ServiceWorkerManager.ServiceWorkerVersion): void {
this.clientsField.removeChildren();
this.section.setFieldVisible(i18nString(UIStrings.clients), Boolean(version.controlledClients.length));
for (const client of version.controlledClients) {
const clientLabelText = this.clientsField.createChild('div', 'service-worker-client');
const info = this.clientInfoCache.get(client);
if (info) {
this.updateClientInfo(clientLabelText, info);
}
void this.manager.target()
.targetAgent()
.invoke_getTargetInfo({targetId: client})
.then(this.onClientInfo.bind(this, clientLabelText));
}
}
private updateSourceField(version: SDK.ServiceWorkerManager.ServiceWorkerVersion): void {
this.sourceField.removeChildren();
const fileName = Common.ParsedURL.ParsedURL.extractName(version.scriptURL);
const name = this.sourceField.createChild('div', 'report-field-value-filename');
const link = Components.Linkifier.Linkifier.linkifyURL(
version.scriptURL, ({text: fileName} as Components.Linkifier.LinkifyURLOptions));
link.tabIndex = 0;
link.setAttribute('jslog', `${VisualLogging.link('source-location').track({click: true})}`);
name.appendChild(link);
if (this.registration.errors.length) {
const errorsLabel = UI.UIUtils.createIconLabel({
title: String(this.registration.errors.length),
iconName: 'cross-circle-filled',
color: 'var(--icon-error)',
});
errorsLabel.classList.add('devtools-link', 'link');
errorsLabel.tabIndex = 0;
UI.ARIAUtils.setLabel(
errorsLabel, i18nString(UIStrings.sRegistrationErrors, {PH1: this.registration.errors.length}));
self.onInvokeElement(errorsLabel, () => Common.Console.Console.instance().show());
name.appendChild(errorsLabel);
}
if (version.scriptResponseTime !== undefined) {
this.sourceField.createChild('div', 'report-field-value-subtitle').textContent =
i18nString(UIStrings.receivedS, {PH1: new Date(version.scriptResponseTime * 1000).toLocaleString()});
}
}
private update(): Promise<void> {
const fingerprint = this.registration.fingerprint();
if (fingerprint === this.fingerprint) {
return Promise.resolve();
}
this.fingerprint = fingerprint;
this.section.setHeaderButtonsState(this.registration.isDeleted);
const versions = this.registration.versionsByMode();
const scopeURL = this.registration.scopeURL;
const title = this.registration.isDeleted ? i18nString(UIStrings.sDeleted, {PH1: scopeURL}) : scopeURL;
this.section.setTitle(title);
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);
this.statusField.removeChildren();
const versionsStack = this.statusField.createChild('div', 'service-worker-version-stack');
versionsStack.createChild('div', 'service-worker-version-stack-bar');
if (active) {
this.updateSourceField(active);
const localizedRunningStatus =
SDK.ServiceWorkerManager.ServiceWorkerVersion.RunningStatus[active.currentState.runningStatus]();
// TODO(l10n): Don't concatenate strings here.
const activeEntry = this.addVersion(
versionsStack, 'service-worker-active-circle',
i18nString(UIStrings.sActivatedAndIsS, {PH1: active.id, PH2: localizedRunningStatus}));
if (active.isRunning() || active.isStarting()) {
const stopButton = UI.UIUtils.createTextButton(
i18nString(UIStrings.stopString), this.stopButtonClicked.bind(this, active.id), {jslogContext: 'stop'});
activeEntry.appendChild(stopButton);
if (!this.targetForVersionId(active.id)) {
const inspectButton = UI.UIUtils.createTextButton(
i18nString(UIStrings.inspect), this.inspectButtonClicked.bind(this, active.id),
{jslogContext: 'inspect'});
activeEntry.appendChild(inspectButton);
}
} else if (active.isStartable()) {
const startButton = UI.UIUtils.createTextButton(
i18nString(UIStrings.startString), this.startButtonClicked.bind(this), {jslogContext: 'start'});
activeEntry.appendChild(startButton);
}
this.updateClientsField(active);
this.maybeCreateRouterField();
} else if (redundant) {
this.updateSourceField(redundant);
this.addVersion(
versionsStack, 'service-worker-redundant-circle', i18nString(UIStrings.sIsRedundant, {PH1: redundant.id}));
this.updateClientsField(redundant);
}
if (waiting) {
const waitingEntry = this.addVersion(
versionsStack, 'service-worker-waiting-circle', i18nString(UIStrings.sWaitingToActivate, {PH1: waiting.id}));
const skipWaitingButton =
UI.UIUtils.createTextButton(i18n.i18n.lockedString('skipWaiting'), this.skipButtonClicked.bind(this), {
title: i18n.i18n.lockedString('skipWaiting'),
jslogContext: 'skip-waiting',
});
waitingEntry.appendChild(skipWaitingButton);
if (waiting.scriptResponseTime !== undefined) {
waitingEntry.createChild('div', 'service-worker-subtitle').textContent =
i18nString(UIStrings.receivedS, {PH1: new Date(waiting.scriptResponseTime * 1000).toLocaleString()});
}
if (!this.targetForVersionId(waiting.id) && (waiting.isRunning() || waiting.isStarting())) {
const inspectButton = UI.UIUtils.createTextButton(
i18nString(UIStrings.inspect), this.inspectButtonClicked.bind(this, waiting.id), {
title: i18nString(UIStrings.inspect),
jslogContext: 'waiting-entry-inspect',
});
waitingEntry.appendChild(inspectButton);
}
}
if (installing) {
const installingEntry = this.addVersion(
versionsStack, 'service-worker-installing-circle',
i18nString(UIStrings.sTryingToInstall, {PH1: installing.id}));
if (installing.scriptResponseTime !== undefined) {
installingEntry.createChild('div', 'service-worker-subtitle').textContent = i18nString(UIStrings.receivedS, {
PH1: new Date(installing.scriptResponseTime * 1000).toLocaleString(),
});
}
if (!this.targetForVersionId(installing.id) && (installing.isRunning() || installing.isStarting())) {
const inspectButton = UI.UIUtils.createTextButton(
i18nString(UIStrings.inspect), this.inspectButtonClicked.bind(this, installing.id), {
title: i18nString(UIStrings.inspect),
jslogContext: 'installing-entry-inspect',
});
installingEntry.appendChild(inspectButton);
}
}
this.updateCycleView.refresh();
return Promise.resolve();
}
private unregisterButtonClicked(): void {
this.manager.deleteRegistration(this.registration.id);
}
private createUpdateCycleField(): void {
this.updateCycleField = this.wrapWidget(this.section.appendField(i18nString(UIStrings.updateCycle)));
this.updateCycleField.appendChild(this.updateCycleView.tableElement);
}
private maybeCreateRouterField(): void {
const versions = this.registration.versionsByMode();
const active = versions.get(SDK.ServiceWorkerManager.ServiceWorkerVersion.Modes.ACTIVE);
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.
if (!this.routerField) {
this.routerField = this.wrapWidget(this.section.appendField(title));
}
if (!this.routerField.lastElementChild) {
this.routerField.appendChild(this.routerView);
}
this.routerView.update(active.routerRules);
} else {
// If no active worker or no registered rules, remove the field.
this.section.removeField(title);
this.routerField = undefined;
}
}
private updateButtonClicked(): void {
void this.manager.updateRegistration(this.registration.id);
}
private networkRequestsClicked(): void {
const applicationTabLocation = UI.ViewManager.ViewManager.instance().locationNameForViewId('resources');
const networkTabLocation = applicationTabLocation === 'drawer-view' ? 'panel' : 'drawer-view';
UI.ViewManager.ViewManager.instance().showViewInLocation('network', networkTabLocation);
void Common.Revealer.reveal(NetworkForward.UIFilter.UIRequestFilter.filters([
{
filterType: NetworkForward.UIFilter.FilterType.Is,
filterValue: NetworkForward.UIFilter.IsFilterType.SERVICE_WORKER_INTERCEPTED,
},
]));
const requests = Logs.NetworkLog.NetworkLog.instance().requests();
let lastRequest: SDK.NetworkRequest.NetworkRequest|null = null;
if (Array.isArray(requests)) {
for (const request of requests) {
if (!lastRequest && request.fetchedViaServiceWorker) {
lastRequest = request;
}
if (request.fetchedViaServiceWorker && lastRequest &&
lastRequest.responseReceivedTime < request.responseReceivedTime) {
lastRequest = request;
}
}
}
if (lastRequest) {
const requestLocation = NetworkForward.UIRequestLocation.UIRequestLocation.tab(
lastRequest, NetworkForward.UIRequestLocation.UIRequestTabs.TIMING, {clearFilter: false});
void Common.Revealer.reveal(requestLocation);
}
this.manager.serviceWorkerNetworkRequestsPanelStatus = {
isOpen: true,
openedAt: Date.now(),
};
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 onClientInfo(element: Element, targetInfoResponse: Protocol.Target.GetTargetInfoResponse): void {
const targetInfo = targetInfoResponse.targetInfo;
if (!targetInfo) {
return;
}
this.clientInfoCache.set(targetInfo.targetId, targetInfo);
this.updateClientInfo(element, targetInfo);
}
private updateClientInfo(element: Element, targetInfo: Protocol.Target.TargetInfo): void {
if (targetInfo.type !== 'page' && targetInfo.type === 'iframe') {
const clientString = element.createChild('span', 'service-worker-client-string');
UI.UIUtils.createTextChild(clientString, i18nString(UIStrings.workerS, {PH1: targetInfo.url}));
return;
}
element.removeChildren();
const clientString = element.createChild('span', 'service-worker-client-string');
UI.UIUtils.createTextChild(clientString, targetInfo.url);
const focusButton = new Buttons.Button.Button();
focusButton.data = {
iconName: 'select-element',
variant: Buttons.Button.Variant.ICON,
size: Buttons.Button.Size.SMALL,
title: i18nString(UIStrings.focus),
jslogContext: 'client-focus',
};
focusButton.className = 'service-worker-client-focus-link';
focusButton.addEventListener('click', this.activateTarget.bind(this, targetInfo.targetId));
element.appendChild(focusButton);
}
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);
}
private inspectButtonClicked(versionId: string): void {
void this.manager.inspectWorker(versionId);
}
private wrapWidget(container: Element): Element {
const shadowRoot = UI.UIUtils.createShadowRootWithCoreStyles(container, {
cssFile: [
serviceWorkersViewStyles,
/* These styles are for the timing table in serviceWorkerUpdateCycleView but this is the widget that it is rendered
* inside so we are registering the files here. */
serviceWorkerUpdateCycleViewStyles,
],
});
const contentElement = document.createElement('div');
shadowRoot.appendChild(contentElement);
return contentElement;
}
}