UNPKG

chrome-devtools-frontend

Version:
375 lines (336 loc) • 12.9 kB
// 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. import * as i18n from '../../core/i18n/i18n.js'; import * as SDK from '../../core/sdk/sdk.js'; import * as Protocol from '../../generated/protocol.js'; import * as UI from '../../ui/legacy/legacy.js'; import {html, type LitTemplate, nothing, render} from '../../ui/lit/lit.js'; import * as VisualLogging from '../../ui/visual_logging/visual_logging.js'; import serviceWorkerUpdateCycleViewStyles from './serviceWorkerUpdateCycleView.css.js'; const UIStrings = { /** * @description Text in Indexed DBViews of the Application panel */ version: 'Version', /** * @description Table heading for Service Workers update information. Update is a noun. */ updateActivity: 'Update Activity', /** * @description Title for the timeline tab. */ timeline: 'Timeline', /** * @description Text in Service Workers Update Life Cycle * @example {2} PH1 */ startTimeS: 'Start time: {PH1}', /** * @description Text for end time of an event * @example {2} PH1 */ endTimeS: 'End time: {PH1}', } as const; const str_ = i18n.i18n.registerUIStrings('panels/application/ServiceWorkerUpdateCycleView.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); export interface ViewInput { timeRanges: ServiceWorkerUpdateRange[]; expandedRows: Set<string>; onFocus: (event: Event) => void; onKeydown: (event: Event, key: string) => void; onClick: (event: Event, key: string) => void; } export type View = (input: ViewInput, output: unknown, target: HTMLElement) => void; export const DEFAULT_VIEW: View = (input, _output, target) => { let tableRows: LitTemplate|typeof nothing = nothing; if (input.timeRanges.length > 0) { const startTimes = input.timeRanges.map(r => r.start); const endTimes = input.timeRanges.map(r => r.end); const startTime = startTimes.reduce((a, b) => Math.min(a, b)); const endTime = endTimes.reduce((a, b) => Math.max(a, b)); const scale = 100 / (endTime - startTime); tableRows = html`${input.timeRanges.map(range => { const phaseName = range.phase; const left = (scale * (range.start - startTime)); const right = (scale * (endTime - range.end)); const key = `${range.id}-${range.phase}`; const expanded = input.expandedRows.has(key); // clang-format off return html` <tr class="service-worker-update-timeline" jslog=${VisualLogging.treeItem('update-timeline').track({ click: true, resize: true, keydown: 'ArrowLeft|ArrowRight|ArrowUp|ArrowDown|Enter|Space', })}> <td class="service-worker-update-timing-bar-clickable" tabindex="0" role="switch" aria-checked=${expanded ? 'true' : 'false'} @focus=${input.onFocus} @keydown=${(e: Event) => input.onKeydown(e, key)} @click=${(e: Event) => input.onClick(e, key)} jslog=${VisualLogging.expand('timing-info').track({click: true})}> #${range.id} </td> <td>${phaseName}</td> <td> <div class="service-worker-update-timing-row"> <span class="service-worker-update-timing-bar ${phaseName.toLowerCase()}" style="left: ${left}%; right: ${right}%;">\u200B</span> </div> </td> </tr> <tr class="service-worker-update-timing-bar-details ${expanded ? 'service-worker-update-timing-bar-details-expanded' : 'service-worker-update-timing-bar-details-collapsed'}" tabindex="0"> <td colspan="3"><span>${i18nString(UIStrings.startTimeS, {PH1: new Date(range.start).toISOString()})}</span></td> </tr> <tr class="service-worker-update-timing-bar-details ${expanded ? 'service-worker-update-timing-bar-details-expanded' : 'service-worker-update-timing-bar-details-collapsed'}" tabindex="0"> <td colspan="3"><span>${i18nString(UIStrings.endTimeS, {PH1: new Date(range.end).toISOString()})}</span></td> </tr> `; // clang-format on })}`; } // clang-format off render(html` <style>${serviceWorkerUpdateCycleViewStyles}</style> <table class="service-worker-update-timing-table" jslog=${VisualLogging.tree('update-timing-table')}> <tr class="service-worker-update-timing-table-header"> <td>${i18nString(UIStrings.version)}</td> <td>${i18nString(UIStrings.updateActivity)}</td> <td>${i18nString(UIStrings.timeline)}</td> </tr> ${tableRows} </table> `, target); // clang-format on }; export class ServiceWorkerUpdateCycleView extends UI.Widget.Widget { #registration?: SDK.ServiceWorkerManager.ServiceWorkerRegistration; private rows: HTMLTableRowElement[]; private selectedRowIndex: number; private expandedRows = new Set<string>(); #view: View; constructor(element?: HTMLElement, view = DEFAULT_VIEW) { super(element); this.#view = view; this.rows = []; this.selectedRowIndex = -1; } set registration(registration: SDK.ServiceWorkerManager.ServiceWorkerRegistration|undefined) { this.#registration = registration; this.requestUpdate(); } get registration(): SDK.ServiceWorkerManager.ServiceWorkerRegistration|undefined { return this.#registration; } set registrationFingerprint(_fingerprint: symbol|undefined) { this.requestUpdate(); } calculateServiceWorkerUpdateRanges(): ServiceWorkerUpdateRange[] { function addRange(ranges: ServiceWorkerUpdateRange[], range: ServiceWorkerUpdateRange): void { if (range.start < Number.MAX_VALUE && range.start <= range.end) { ranges.push(range); } } /** * Add ranges representing Install, Wait or Activate of a sw version represented by id. */ function addNormalizedRanges( ranges: ServiceWorkerUpdateRange[], id: string, startInstallTime: number, endInstallTime: number, startActivateTime: number, endActivateTime: number, status: Protocol.ServiceWorker.ServiceWorkerVersionStatus): void { addRange(ranges, {id, phase: ServiceWorkerUpdateNames.INSTALL, start: startInstallTime, end: endInstallTime}); if (status === Protocol.ServiceWorker.ServiceWorkerVersionStatus.Activating || status === Protocol.ServiceWorker.ServiceWorkerVersionStatus.Activated || status === Protocol.ServiceWorker.ServiceWorkerVersionStatus.Redundant) { addRange(ranges, { id, phase: ServiceWorkerUpdateNames.WAIT, start: endInstallTime, end: startActivateTime, }); addRange( ranges, {id, phase: ServiceWorkerUpdateNames.ACTIVATE, start: startActivateTime, end: endActivateTime}); } } function rangesForVersion(version: SDK.ServiceWorkerManager.ServiceWorkerVersion): ServiceWorkerUpdateRange[] { let state: SDK.ServiceWorkerManager.ServiceWorkerVersionState|null = version.currentState; let endActivateTime = 0; let beginActivateTime = 0; let endInstallTime = 0; let beginInstallTime = 0; const currentStatus = state.status; if (currentStatus === Protocol.ServiceWorker.ServiceWorkerVersionStatus.New) { return []; } while (state) { // find the earliest timestamp of different stage on record. if (state.status === Protocol.ServiceWorker.ServiceWorkerVersionStatus.Activated) { endActivateTime = state.lastUpdatedTimestamp; } else if (state.status === Protocol.ServiceWorker.ServiceWorkerVersionStatus.Activating) { if (endActivateTime === 0) { endActivateTime = state.lastUpdatedTimestamp; } beginActivateTime = state.lastUpdatedTimestamp; } else if (state.status === Protocol.ServiceWorker.ServiceWorkerVersionStatus.Installed) { endInstallTime = state.lastUpdatedTimestamp; } else if (state.status === Protocol.ServiceWorker.ServiceWorkerVersionStatus.Installing) { if (endInstallTime === 0) { endInstallTime = state.lastUpdatedTimestamp; } beginInstallTime = state.lastUpdatedTimestamp; } state = state.previousState; } const ranges: ServiceWorkerUpdateRange[] = []; addNormalizedRanges( ranges, version.id, beginInstallTime, endInstallTime, beginActivateTime, endActivateTime, currentStatus); return ranges; } if (!this.#registration) { return []; } const versions = this.#registration.versionsByMode(); const modes = [ SDK.ServiceWorkerManager.ServiceWorkerVersion.Modes.ACTIVE, SDK.ServiceWorkerManager.ServiceWorkerVersion.Modes.WAITING, SDK.ServiceWorkerManager.ServiceWorkerVersion.Modes.INSTALLING, SDK.ServiceWorkerManager.ServiceWorkerVersion.Modes.REDUNDANT, ]; for (const mode of modes) { const version = versions.get(mode); if (version) { const ranges = rangesForVersion(version); return ranges; } } return []; } override performUpdate(): void { const timeRanges = this.calculateServiceWorkerUpdateRanges(); const input: ViewInput = { timeRanges, expandedRows: this.expandedRows, onFocus: this.onFocus.bind(this), onKeydown: this.onKeydown.bind(this), onClick: this.onClick.bind(this), }; this.#view(input, this, this.contentElement); this.rows = Array.from(this.contentElement.querySelectorAll<HTMLTableRowElement>('.service-worker-update-timeline')); if (this.selectedRowIndex >= this.rows.length) { this.selectedRowIndex = -1; } } private toggle(key: string, expanded: boolean): void { if (expanded) { this.expandedRows.delete(key); } else { this.expandedRows.add(key); } this.requestUpdate(); } private onFocus(event: Event): void { const target = event.target as HTMLElement | null; if (!target) { return; } const tr = target.parentElement as HTMLTableRowElement | null; if (!tr) { return; } this.selectedRowIndex = this.rows.indexOf(tr); } private onKeydown(event: Event, key: string): void { if (!event.target) { return; } const keyboardEvent = event as KeyboardEvent; const expanded = this.expandedRows.has(key); if (keyboardEvent.key === 'Enter' || keyboardEvent.key === ' ') { this.toggle(key, expanded); event.preventDefault(); return; } if ((!expanded && keyboardEvent.key === 'ArrowRight') || (expanded && keyboardEvent.key === 'ArrowLeft')) { this.toggle(key, expanded); event.preventDefault(); return; } if (keyboardEvent.key === 'ArrowDown') { if (this.selectedRowIndex >= 0) { this.selectNextRow(); } else { this.selectFirstRow(); } event.preventDefault(); } if (keyboardEvent.key === 'ArrowUp') { if (this.selectedRowIndex >= 0) { this.selectPreviousRow(); } else { this.selectLastRow(); } event.preventDefault(); } } private focusRow(row: HTMLTableRowElement): void { row.cells[0].focus(); } private blurRow(row: HTMLTableRowElement): void { row.cells[0].blur(); } private selectFirstRow(): void { if (this.rows.length === 0) { return; } this.selectedRowIndex = 0; this.focusRow(this.rows[0]); } private selectLastRow(): void { if (this.rows.length === 0) { return; } this.selectedRowIndex = this.rows.length - 1; this.focusRow(this.rows[this.selectedRowIndex]); } private selectNextRow(): void { if (this.rows.length === 0) { return; } const previousRowIndex = this.selectedRowIndex; this.selectedRowIndex++; if (this.selectedRowIndex >= this.rows.length) { this.selectedRowIndex = 0; } this.blurRow(this.rows[previousRowIndex]); this.focusRow(this.rows[this.selectedRowIndex]); } private selectPreviousRow(): void { if (this.rows.length === 0) { return; } const previousRowIndex = this.selectedRowIndex; this.selectedRowIndex--; if (this.selectedRowIndex < 0) { this.selectedRowIndex = this.rows.length - 1; } this.blurRow(this.rows[previousRowIndex]); this.focusRow(this.rows[this.selectedRowIndex]); } private onClick(event: Event, key: string): void { const expanded = this.expandedRows.has(key); this.toggle(key, expanded); event.preventDefault(); } } export const enum ServiceWorkerUpdateNames { INSTALL = 'Install', WAIT = 'Wait', ACTIVATE = 'Activate', } export interface ServiceWorkerUpdateRange { id: string; phase: ServiceWorkerUpdateNames; start: number; end: number; }