UNPKG

chrome-devtools-frontend

Version:
284 lines (258 loc) • 9.22 kB
// Copyright 2025 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/legacy/legacy.js'; import * as i18n from '../../core/i18n/i18n.js'; import * as Platform from '../../core/platform/platform.js'; import * as TextUtils from '../../models/text_utils/text_utils.js'; import type * as Trace from '../../models/trace/trace.js'; import * as Workspace from '../../models/workspace/workspace.js'; import * as Buttons from '../../ui/components/buttons/buttons.js'; import * as UI from '../../ui/legacy/legacy.js'; import {html, nothing, render} from '../../ui/lit/lit.js'; import * as VisualLogging from '../../ui/visual_logging/visual_logging.js'; import {traceJsonGenerator} from './SaveFileFormatter.js'; import timelineStatusDialogStyles from './timelineStatusDialog.css.js'; const UIStrings = { /** * @description Text to download the trace file after an error */ downloadAfterError: 'Download trace', /** * @description Text for the status of something */ status: 'Status', /** * @description Text that refers to the time */ time: 'Time', /** * @description Text for the description of something */ description: 'Description', /** * @description Text of an item that stops the running task */ stop: 'Stop', } as const; const str_ = i18n.i18n.registerUIStrings('panels/timeline/StatusDialog.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); export interface ViewInput { statusText: string; showTimer: boolean; timeText: string; showProgress: boolean; progressActivity: string; progressPercent: number; descriptionText: string|undefined; buttonText: string; hideStopButton: boolean; focusStopButton: boolean; showDownloadButton: boolean; downloadButtonDisabled: boolean; onStopClick: () => void; onDownloadClick: () => void; } export type ViewOutput = Record<string, never>; export type View = (input: ViewInput, output: ViewOutput, target: HTMLElement) => void; // clang-format off export const DEFAULT_VIEW: View = (input, output, target) => { render(html` <style>${timelineStatusDialogStyles}</style> <div class="timeline-status-dialog"> <div class="status-dialog-line status"> <div class="label">${i18nString(UIStrings.status)}</div> <div class="content" role="status">${input.statusText}</div> </div> ${input.showTimer ? html` <div class="status-dialog-line time"> <div class="label">${i18nString(UIStrings.time)}</div> <div class="content">${input.timeText}</div> </div> ` : nothing} ${input.showProgress ? html` <div class="status-dialog-line progress"> <div class="label">${input.progressActivity}</div> <div class="indicator-container"> <div class="indicator" style="width: ${input.progressPercent.toFixed(1)}%" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow=${input.progressPercent}> </div> </div> </div> ` : nothing} ${input.descriptionText !== undefined ? html` <div class="status-dialog-line description"> <div class="label">${i18nString(UIStrings.description)}</div> <div class="content">${input.descriptionText}</div> </div> ` : nothing} <div class="stop-button"> ${input.showDownloadButton ? html` <devtools-button .variant=${Buttons.Button.Variant.OUTLINED} .disabled=${input.downloadButtonDisabled} @click=${input.onDownloadClick} .jslogContext=${'timeline.download-after-error'} >${i18nString(UIStrings.downloadAfterError)}</devtools-button> ` : nothing} ${!input.hideStopButton ? html` <devtools-button .variant=${Buttons.Button.Variant.PRIMARY} @click=${input.onStopClick} .jslogContext=${'timeline.stop-recording'} ?autofocus=${input.focusStopButton} >${input.buttonText}</devtools-button> ` : nothing} </div> </div> `, target); }; // clang-format on /** * This is the dialog shown whilst a trace is being recorded/imported. */ export class StatusDialog extends UI.Widget.VBox { readonly #view: View; #statusText = ''; readonly #showTimer: boolean; #timeText = ''; readonly #showProgress: boolean; #progressActivity = ''; #progressPercent = 0; readonly #descriptionText: string|undefined; readonly #buttonText: string; #hideStopButton: boolean; #focusStopButton = false; #showDownloadButton = false; #downloadButtonDisabled = true; readonly #onButtonClickCallback: () => (Promise<void>| void); #startTime!: number; #timeUpdateTimer?: number; #rawEvents?: Trace.Types.Events.Event[]; constructor( options: { hideStopButton: boolean, showTimer?: boolean, showProgress?: boolean, description?: string, buttonText?: string, }, onButtonClickCallback: () => (Promise<void>| void), view: View = DEFAULT_VIEW) { super({ jslog: `${VisualLogging.dialog('timeline-status').track({resize: true})}`, useShadowDom: true, }); this.#view = view; this.#showTimer = Boolean(options.showTimer); this.#showProgress = Boolean(options.showProgress); this.#descriptionText = options.description; this.#buttonText = options.buttonText || i18nString(UIStrings.stop); this.#hideStopButton = options.hideStopButton; this.#onButtonClickCallback = onButtonClickCallback; } finish(): void { this.stopTimer(); this.#hideStopButton = true; this.requestUpdate(); } async #downloadRawTraceAfterError(): Promise<void> { if (!this.#rawEvents || this.#rawEvents.length === 0) { return; } const traceStart = Platform.DateUtilities.toISO8601Compact(new Date()); const fileName = `Trace-Load-Error-${traceStart}.json` as Platform.DevToolsPath.RawPathString; const formattedTraceIter = traceJsonGenerator(this.#rawEvents, {}); const traceAsString = Array.from(formattedTraceIter).join(''); await Workspace.FileManager.FileManager.instance().save( fileName, new TextUtils.ContentData.ContentData(traceAsString, /* isBase64=*/ false, 'application/json'), /* forceSaveAs=*/ true); Workspace.FileManager.FileManager.instance().close(fileName); } enableDownloadOfEvents(rawEvents: Trace.Types.Events.Event[]): void { this.#rawEvents = rawEvents; this.#showDownloadButton = true; this.#downloadButtonDisabled = false; this.requestUpdate(); } remove(): void { (this.element.parentNode as HTMLElement)?.classList.remove('opaque', 'tinted'); this.stopTimer(); this.element.remove(); } showPane(parent: Element, mode: 'tinted'|'opaque' = 'opaque'): void { this.show(parent); parent.classList.toggle('tinted', mode === 'tinted'); parent.classList.toggle('opaque', mode === 'opaque'); } enableAndFocusButton(): void { this.#hideStopButton = false; this.#focusStopButton = true; this.requestUpdate(); } updateStatus(text: string): void { this.#statusText = text; this.requestUpdate(); } updateProgressBar(activity: string, percent: number): void { this.#progressActivity = activity; this.#progressPercent = percent; this.#updateTimerTick(); this.requestUpdate(); } startTimer(): void { this.#startTime = Date.now(); this.#timeUpdateTimer = window.setInterval(this.#updateTimerTick.bind(this), 100); this.#updateTimerTick(); } private stopTimer(): void { if (!this.#timeUpdateTimer) { return; } clearInterval(this.#timeUpdateTimer); this.#updateTimerTick(); this.#timeUpdateTimer = undefined; } #updateTimerTick(): void { if (!this.#timeUpdateTimer || !this.#showTimer) { return; } const seconds = (Date.now() - this.#startTime) / 1000; this.#timeText = i18n.TimeUtilities.preciseSecondsToString(seconds, 1); this.requestUpdate(); } override performUpdate(): void { this.#view( { statusText: this.#statusText, showTimer: this.#showTimer, timeText: this.#timeText, showProgress: this.#showProgress, progressActivity: this.#progressActivity, progressPercent: this.#progressPercent, descriptionText: this.#descriptionText, buttonText: this.#buttonText, hideStopButton: this.#hideStopButton, focusStopButton: this.#focusStopButton, showDownloadButton: this.#showDownloadButton, downloadButtonDisabled: this.#downloadButtonDisabled, onStopClick: () => { void this.#onButtonClickCallback(); }, onDownloadClick: () => { void this.#downloadRawTraceAfterError(); }, }, {}, this.contentElement, ); this.#focusStopButton = false; } override wasShown(): void { super.wasShown(); this.requestUpdate(); } }