chrome-devtools-frontend
Version:
Chrome DevTools UI
353 lines (307 loc) ⢠14.9 kB
text/typescript
// 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.
/* eslint-disable rulesdir/no-lit-render-outside-of-view */
import '../../../ui/components/tooltips/tooltips.js';
import '../../../ui/components/buttons/buttons.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 Root from '../../../core/root/root.js';
import * as Buttons from '../../../ui/components/buttons/buttons.js';
import * as Dialogs from '../../../ui/components/dialogs/dialogs.js';
import * as ComponentHelpers from '../../../ui/components/helpers/helpers.js';
import * as UI from '../../../ui/legacy/legacy.js';
import * as Lit from '../../../ui/lit/lit.js';
import exportTraceOptionsStyles from './exportTraceOptions.css.js';
const {html} = Lit;
const UIStrings = {
/**
* @description Text title for the Save performance trace dialog.
*/
exportTraceOptionsDialogTitle: 'Save performance trace ',
/**
* @description Tooltip for the Save performance trace dialog.
*/
showExportTraceOptionsDialogTitle: 'Save traceā¦',
/**
* @description Text for the include script content option.
*/
includeScriptContent: 'Include script content',
/**
* @description Text for the include script source maps option.
*/
includeSourcemap: 'Include script source maps',
/**
* @description Text for the include annotations option.
*/
includeAnnotations: 'Include annotations',
/**
* @description Text for the compression option.
*/
shouldCompress: 'Compress with gzip',
/**
* @description Text for the save trace button
*/
saveButtonTitle: 'Save',
/**
* @description Text shown in the information pop-up next to the "Include script content" option.
*/
scriptContentPrivacyInfo: 'Includes the full content of all loaded scripts (except extensions).',
/**
* @description Text shown in the information pop-up next to the "Include script sourcemaps" option.
*/
sourceMapsContentPrivacyInfo: 'Includes available source maps, which may expose authored code.',
/**
* @description Text used as the start of the accessible label for the information button which shows additional context when the user focuses / hovers.
*/
moreInfoLabel: 'Additional information:',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/timeline/components/ExportTraceOptions.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export interface ExportTraceOptionsData {
onExport: (config: {
includeScriptContent: boolean,
includeSourceMaps: boolean,
addModifications: boolean,
shouldCompress: boolean,
}) => Promise<void>;
buttonEnabled: boolean;
}
export type ExportTraceDialogState = Dialogs.Dialog.DialogState;
export interface ExportTraceOptionsState {
dialogState: ExportTraceDialogState;
includeAnnotations: boolean;
includeScriptContent: boolean;
includeSourceMaps: boolean;
shouldCompress: boolean;
displayAnnotationsCheckbox?: boolean;
displayScriptContentCheckbox?: boolean;
displaySourceMapsCheckbox?: boolean;
}
type CheckboxId = 'annotations'|'script-content'|'script-source-maps'|'compress-with-gzip';
const checkboxesWithInfoDialog = new Set<CheckboxId>(['script-content', 'script-source-maps']);
export class ExportTraceOptions extends HTMLElement {
readonly #shadow = this.attachShadow({mode: 'open'});
#data: ExportTraceOptionsData|null = null;
static readonly #includeAnnotationsSettingString: string = 'export-performance-trace-include-annotations';
static readonly #includeScriptContentSettingString: string = 'export-performance-trace-include-scripts';
static readonly #includeSourceMapsSettingString: string = 'export-performance-trace-include-sourcemaps';
static readonly #shouldCompressSettingString: string = 'export-performance-trace-should-compress';
#includeAnnotationsSetting: Common.Settings.Setting<boolean> = Common.Settings.Settings.instance().createSetting(
ExportTraceOptions.#includeAnnotationsSettingString, true, Common.Settings.SettingStorageType.SESSION);
#includeScriptContentSetting: Common.Settings.Setting<boolean> = Common.Settings.Settings.instance().createSetting(
ExportTraceOptions.#includeScriptContentSettingString, false, Common.Settings.SettingStorageType.SESSION);
#includeSourceMapsSetting: Common.Settings.Setting<boolean> = Common.Settings.Settings.instance().createSetting(
ExportTraceOptions.#includeSourceMapsSettingString, false, Common.Settings.SettingStorageType.SESSION);
#shouldCompressSetting: Common.Settings.Setting<boolean> = Common.Settings.Settings.instance().createSetting(
ExportTraceOptions.#shouldCompressSettingString, true, Common.Settings.SettingStorageType.SYNCED);
#state: ExportTraceOptionsState = {
dialogState: Dialogs.Dialog.DialogState.COLLAPSED,
includeAnnotations: this.#includeAnnotationsSetting.get(),
includeScriptContent: this.#includeScriptContentSetting.get(),
includeSourceMaps: this.#includeSourceMapsSetting.get(),
shouldCompress: this.#shouldCompressSetting.get(),
};
#includeAnnotationsCheckbox = UI.UIUtils.CheckboxLabel.create(
/* title*/ i18nString(UIStrings.includeAnnotations), /* checked*/ this.#state.includeAnnotations,
/* subtitle*/ undefined,
/* jslogContext*/ 'timeline.export-trace-options.annotations-checkbox');
#includeScriptContentCheckbox = UI.UIUtils.CheckboxLabel.create(
/* title*/ i18nString(UIStrings.includeScriptContent), /* checked*/ this.#state.includeScriptContent,
/* subtitle*/ undefined,
/* jslogContext*/ 'timeline.export-trace-options.script-content-checkbox');
#includeSourceMapsCheckbox = UI.UIUtils.CheckboxLabel.create(
/* title*/ i18nString(UIStrings.includeSourcemap), /* checked*/ this.#state.includeSourceMaps,
/* subtitle*/ undefined,
/* jslogContext*/ 'timeline.export-trace-options.source-maps-checkbox');
#shouldCompressCheckbox = UI.UIUtils.CheckboxLabel.create(
/* title*/ i18nString(UIStrings.shouldCompress), /* checked*/ this.#state.shouldCompress,
/* subtitle*/ undefined,
/* jslogContext*/ 'timeline.export-trace-options.should-compress-checkbox');
set data(data: ExportTraceOptionsData) {
this.#data = data;
this.#scheduleRender();
}
set state(state: ExportTraceOptionsState) {
this.#state = state;
this.#includeAnnotationsSetting.set(state.includeAnnotations);
this.#includeScriptContentSetting.set(state.includeScriptContent);
this.#includeSourceMapsSetting.set(state.includeSourceMaps);
this.#shouldCompressSetting.set(state.shouldCompress);
this.#scheduleRender();
}
get state(): Readonly<ExportTraceOptionsState> {
return this.#state;
}
updateContentVisibility(options: {annotationsExist: boolean}): void {
const showIncludeScriptContentCheckbox =
Root.Runtime.experiments.isEnabled(Root.Runtime.ExperimentName.TIMELINE_ENHANCED_TRACES);
const showIncludeSourceMapCheckbox =
Root.Runtime.experiments.isEnabled(Root.Runtime.ExperimentName.TIMELINE_COMPILED_SOURCES);
const newState = Object.assign({}, this.#state, {
displayAnnotationsCheckbox: options.annotationsExist,
displayScriptContentCheckbox: showIncludeScriptContentCheckbox,
displaySourceMapsCheckbox: showIncludeSourceMapCheckbox
});
this.state = newState;
}
#scheduleRender(): void {
void ComponentHelpers.ScheduledRender.scheduleRender(this, this.#render);
}
#checkboxOptionChanged(checkboxWithLabel: UI.UIUtils.CheckboxLabel, checked: boolean): void {
const newState = Object.assign({}, this.#state, {dialogState: Dialogs.Dialog.DialogState.EXPANDED});
switch (checkboxWithLabel) {
case this.#includeAnnotationsCheckbox: {
newState.includeAnnotations = checked;
break;
}
case this.#includeScriptContentCheckbox: {
newState.includeScriptContent = checked;
// if the `Include Script` is checked off, cascade the change to `Include Script Source`
if (!newState.includeScriptContent) {
newState.includeSourceMaps = false;
}
break;
}
case this.#includeSourceMapsCheckbox: {
newState.includeSourceMaps = checked;
break;
}
case this.#shouldCompressCheckbox: {
newState.shouldCompress = checked;
break;
}
}
this.state = newState;
}
#accessibleLabelForInfoCheckbox(checkboxId: CheckboxId): string {
if (checkboxId === 'script-source-maps') {
return i18nString(UIStrings.moreInfoLabel) + ' ' + i18nString(UIStrings.sourceMapsContentPrivacyInfo);
}
if (checkboxId === 'script-content') {
return i18nString(UIStrings.moreInfoLabel) + ' ' + i18nString(UIStrings.scriptContentPrivacyInfo);
}
return '';
}
#renderCheckbox(
checkboxId: CheckboxId, checkboxWithLabel: UI.UIUtils.CheckboxLabel, title: Common.UIString.LocalizedString,
checked: boolean): Lit.TemplateResult {
UI.Tooltip.Tooltip.install(checkboxWithLabel, title);
checkboxWithLabel.ariaLabel = title;
checkboxWithLabel.checked = checked;
checkboxWithLabel.addEventListener(
'change', this.#checkboxOptionChanged.bind(this, checkboxWithLabel, !checked), false);
// Disable the includeSourceMapsSetting when the includeScriptContentSetting is also disabled.
this.#includeSourceMapsCheckbox.disabled = !this.#state.includeScriptContent;
// clang-format off
return html`
<div class='export-trace-options-row'>
${checkboxWithLabel}
${checkboxesWithInfoDialog.has(checkboxId) ? html`
<devtools-button
aria-details=${`export-trace-tooltip-${checkboxId}`}
aria-label=${this.#accessibleLabelForInfoCheckbox(checkboxId)}
class="pen-icon"
.iconName=${'info'}
.variant=${Buttons.Button.Variant.ICON}
></devtools-button>
` : Lit.nothing}
</div>
`;
// clang-format on
}
#renderInfoTooltip(checkboxId: CheckboxId): Lit.LitTemplate {
if (!checkboxesWithInfoDialog.has(checkboxId)) {
return Lit.nothing;
}
return html`
<devtools-tooltip
variant="rich"
id=${`export-trace-tooltip-${checkboxId}`}
>
<div class="info-tooltip-container">
<p>
${checkboxId === 'script-content' ? i18nString(UIStrings.scriptContentPrivacyInfo) : Lit.nothing}
${checkboxId === 'script-source-maps' ? i18nString(UIStrings.sourceMapsContentPrivacyInfo) : Lit.nothing}
</p>
</div>
</devtools-tooltip>`;
}
#render(): void {
if (!ComponentHelpers.ScheduledRender.isScheduledRender(this)) {
throw new Error('Export trace options dialog render was not scheduled');
}
// clang-format off
const output = html`
<style>${exportTraceOptionsStyles}</style>
<devtools-button-dialog class="export-trace-dialog"
=${this.#onButtonDialogClick.bind(this)}
.data=${{
openOnRender: false,
jslogContext: 'timeline.export-trace-options',
variant: Buttons.Button.Variant.TOOLBAR,
iconName: 'download',
disabled: !this.#data?.buttonEnabled,
iconTitle: i18nString(UIStrings.showExportTraceOptionsDialogTitle),
horizontalAlignment: Dialogs.Dialog.DialogHorizontalAlignment.AUTO,
closeButton: false,
dialogTitle: i18nString(UIStrings.exportTraceOptionsDialogTitle),
state: this.#state.dialogState,
closeOnESC: true,
} as Dialogs.ButtonDialog.ButtonDialogData}>
<div class='export-trace-options-content'>
${this.#state.displayAnnotationsCheckbox ? this.#renderCheckbox('annotations', this.#includeAnnotationsCheckbox,
i18nString(UIStrings.includeAnnotations),
this.#state.includeAnnotations): ''}
${this.#state.displayScriptContentCheckbox ? this.#renderCheckbox('script-content', this.#includeScriptContentCheckbox,
i18nString(UIStrings.includeScriptContent), this.#state.includeScriptContent): ''}
${this.#state.displayScriptContentCheckbox && this.#state.displaySourceMapsCheckbox ? this.#renderCheckbox(
'script-source-maps',
this.#includeSourceMapsCheckbox, i18nString(UIStrings.includeSourcemap), this.#state.includeSourceMaps): ''}
${this.#renderCheckbox('compress-with-gzip', this.#shouldCompressCheckbox, i18nString(UIStrings.shouldCompress), this.#state.shouldCompress)}
<div class='export-trace-options-row'><div class='export-trace-blank'></div><devtools-button
class="setup-button"
data-export-button
=${this.#onExportClick.bind(this)}
.data=${{
variant: Buttons.Button.Variant.PRIMARY,
title: i18nString(UIStrings.saveButtonTitle),
} as Buttons.Button.ButtonData}
>${i18nString(UIStrings.saveButtonTitle)}</devtools-button>
</div>
${this.#state.displayScriptContentCheckbox ? this.#renderInfoTooltip('script-content') : Lit.nothing}
${this.#state.displayScriptContentCheckbox && this.#state.displaySourceMapsCheckbox ? this.#renderInfoTooltip('script-source-maps') : Lit.nothing}
</div>
</devtools-button-dialog>
`;
// clang-format on
Lit.render(output, this.#shadow, {host: this});
}
async #onButtonDialogClick(): Promise<void> {
this.state = Object.assign({}, this.#state, {dialogState: Dialogs.Dialog.DialogState.EXPANDED});
}
async #onExportCallback(): Promise<void> {
// Calls passed onExport function with current settings.
await this.#data?.onExport({
includeScriptContent: this.#state.includeScriptContent,
includeSourceMaps: this.#state.includeSourceMaps,
// Note: this also includes track configuration ...
addModifications: this.#state.includeAnnotations,
shouldCompress: this.#state.shouldCompress,
});
Host.userMetrics.actionTaken(Host.UserMetrics.Action.PerfPanelTraceExported);
}
async #onExportClick(): Promise<void> {
// Handles save button click that lived inside the dialog.
// Exports trace and collapses dialog.
await this.#onExportCallback();
this.state = Object.assign({}, this.#state, {dialogState: Dialogs.Dialog.DialogState.COLLAPSED});
}
}
customElements.define('devtools-perf-export-trace-options', ExportTraceOptions);
declare global {
interface HTMLElementTagNameMap {
'devtools-perf-export-trace-options': ExportTraceOptions;
}
}