chrome-devtools-frontend
Version:
Chrome DevTools UI
325 lines (281 loc) • 13 kB
text/typescript
// Copyright 2024 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-lit-render-outside-of-view */
/* eslint-disable rulesdir/no-imperative-dom-api */
import '../../../ui/components/menus/menus.js';
import * as Common from '../../../core/common/common.js';
import * as i18n from '../../../core/i18n/i18n.js';
import * as Platform from '../../../core/platform/platform.js';
import * as Bindings from '../../../models/bindings/bindings.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 ignoreListSettingStyles from './ignoreListSetting.css.js';
const {html} = Lit;
const UIStrings = {
/**
* @description Text title for the button to open the ignore list setting.
*/
showIgnoreListSettingDialog: 'Show ignore list setting dialog',
/**
* @description Text title for ignore list setting.
*/
ignoreList: 'Ignore list',
/**
* @description Text description for ignore list setting.
*/
ignoreListDescription: 'Add regular expression rules to remove matching scripts from the flame chart.',
/**
*@description Pattern title in Framework Ignore List Settings Tab of the Settings
*@example {ad.*?} regex
*/
ignoreScriptsWhoseNamesMatchS: 'Ignore scripts whose names match \'\'{regex}\'\'',
/**
*@description Label for the button to remove an regex
*@example {ad.*?} regex
*/
removeRegex: 'Remove the regex: \'\'{regex}\'\'',
/**
*@description Aria accessible name in Ignore List Settings Dialog in Performance panel. It labels the input
* field used to add new or edit existing regular expressions that match file names to ignore in the debugger.
*/
addNewRegex: 'Add a regular expression rule for the script\'s URL',
/**
* @description Aria accessible name in Ignore List Settings Dialog in Performance panel. It labels the checkbox of
* the input field used to enable the new regular expressions that match file names to ignore in the debugger.
*/
ignoreScriptsWhoseNamesMatchNewRegex: 'Ignore scripts whose names match the new regex',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/timeline/components/IgnoreListSetting.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export class IgnoreListSetting extends HTMLElement {
readonly #shadow = this.attachShadow({mode: 'open'});
readonly #ignoreListEnabled: Common.Settings.Setting<boolean> =
Common.Settings.Settings.instance().moduleSetting('enable-ignore-listing');
readonly #regexPatterns = this.#getSkipStackFramesPatternSetting().getAsArray();
#newRegexCheckbox = UI.UIUtils.CheckboxLabel.create(
/* title*/ undefined, /* checked*/ false, /* subtitle*/ undefined,
/* jslogContext*/ 'timeline.ignore-list-new-regex.checkbox');
#newRegexInput = UI.UIUtils.createInput(
/* className*/ 'new-regex-text-input', /* type*/ 'text', /* jslogContext*/ 'timeline.ignore-list-new-regex.text');
#editingRegexSetting: Common.Settings.RegExpSettingItem|null = null;
constructor() {
super();
this.#initAddNewItem();
Common.Settings.Settings.instance()
.moduleSetting('skip-stack-frames-pattern')
.addChangeListener(this.#scheduleRender.bind(this));
Common.Settings.Settings.instance()
.moduleSetting('enable-ignore-listing')
.addChangeListener(this.#scheduleRender.bind(this));
}
connectedCallback(): void {
this.#scheduleRender();
// Prevent the event making its way to the TimelinePanel element which will
// cause the "Load Profile" context menu to appear.
this.addEventListener('contextmenu', e => {
e.stopPropagation();
});
}
#scheduleRender(): void {
void ComponentHelpers.ScheduledRender.scheduleRender(this, this.#render);
}
#getSkipStackFramesPatternSetting(): Common.Settings.RegExpSetting {
return Common.Settings.Settings.instance().moduleSetting('skip-stack-frames-pattern') as
Common.Settings.RegExpSetting;
}
#startEditing(): void {
// Do not need to trim here because this is a temporary one, we will trim the input when finish editing,
this.#editingRegexSetting = {pattern: this.#newRegexInput.value, disabled: false, disabledForUrl: undefined};
// We need to push the temp regex here to update the flame chart.
// We are using the "skip-stack-frames-pattern" setting to determine which is rendered on flame chart. And the push
// here will update the setting's value.
this.#regexPatterns.push(this.#editingRegexSetting);
}
#finishEditing(): void {
if (!this.#editingRegexSetting) {
return;
}
const lastRegex = this.#regexPatterns.pop();
// Add a sanity check to make sure the last one is the editing one.
// In case the check fails, add back the last element.
if (lastRegex && lastRegex !== this.#editingRegexSetting) {
console.warn('The last regex is not the editing one.');
this.#regexPatterns.push(lastRegex);
}
this.#editingRegexSetting = null;
this.#getSkipStackFramesPatternSetting().setAsArray(this.#regexPatterns);
}
#resetInput(): void {
this.#newRegexCheckbox.checked = false;
this.#newRegexInput.value = '';
}
#addNewRegexToIgnoreList(): void {
const newRegex = this.#newRegexInput.value.trim();
this.#finishEditing();
if (!regexInputIsValid(newRegex)) {
// It the new regex is invalid, let's skip it.
return;
}
Bindings.IgnoreListManager.IgnoreListManager.instance().addRegexToIgnoreList(newRegex);
this.#resetInput();
}
#handleKeyDown(event: KeyboardEvent): void {
// When user press the 'Enter', the current regex will be added and user can keep adding more regexes.
if (event.key === Platform.KeyboardUtilities.ENTER_KEY) {
this.#addNewRegexToIgnoreList();
this.#startEditing();
return;
}
// When user press the 'Escape', it means cancel the editing, so the current regex won't be added and the input will
// lose focus.
if (event.key === Platform.KeyboardUtilities.ESCAPE_KEY) {
// Escape key will close the dialog, and toggle the `Console` drawer. So we need to ignore other listeners.
event.stopImmediatePropagation();
this.#finishEditing();
this.#resetInput();
this.#newRegexInput.blur();
}
}
/**
* When it is in the 'preview' mode, the last regex in the array is the editing one.
* So we want to remove it for some usage, like rendering the existed rules or validating the rules.
*/
#getExistingRegexes(): Common.Settings.RegExpSettingItem[] {
if (this.#editingRegexSetting) {
const lastRegex = this.#regexPatterns[this.#regexPatterns.length - 1];
// Add a sanity check to make sure the last one is the editing one.
if (lastRegex && lastRegex === this.#editingRegexSetting) {
// We don't want to modify the array itself, so just return a shadow copy of it.
return this.#regexPatterns.slice(0, -1);
}
}
return this.#regexPatterns;
}
#handleInputChange(): void {
const newRegex = this.#newRegexInput.value.trim();
if (this.#editingRegexSetting && regexInputIsValid(newRegex)) {
this.#editingRegexSetting.pattern = newRegex;
this.#editingRegexSetting.disabled = !Boolean(newRegex);
this.#getSkipStackFramesPatternSetting().setAsArray(this.#regexPatterns);
}
}
#initAddNewItem(): void {
this.#newRegexInput.placeholder = '/framework\\.js$';
const checkboxHelpText = i18nString(UIStrings.ignoreScriptsWhoseNamesMatchNewRegex);
const inputHelpText = i18nString(UIStrings.addNewRegex);
UI.Tooltip.Tooltip.install(this.#newRegexCheckbox, checkboxHelpText);
UI.Tooltip.Tooltip.install(this.#newRegexInput, inputHelpText);
this.#newRegexInput.addEventListener('blur', this.#addNewRegexToIgnoreList.bind(this), false);
this.#newRegexInput.addEventListener('keydown', this.#handleKeyDown.bind(this), false);
this.#newRegexInput.addEventListener('input', this.#handleInputChange.bind(this), false);
this.#newRegexInput.addEventListener('focus', this.#startEditing.bind(this), false);
}
#renderNewRegexRow(): Lit.TemplateResult {
// clang-format off
return html`
<div class='new-regex-row'>${this.#newRegexCheckbox}${this.#newRegexInput}</div>
`;
// clang-format on
}
/**
* Deal with an existing regex being toggled. Note that this handler only
* deals with enabling/disabling regexes already in the ignore list, it does
* not deal with enabling/disabling the new regex.
*/
#onExistingRegexEnableToggle(regex: Common.Settings.RegExpSettingItem, checkbox: UI.UIUtils.CheckboxLabel): void {
regex.disabled = !checkbox.checked;
// Technically we don't need to call the set function, because the regex is a reference, so it changed the setting
// value directly.
// But we need to call the set function to trigger the setting change event. which is needed by view update of flame
// chart.
this.#getSkipStackFramesPatternSetting().setAsArray(this.#regexPatterns);
// There is no need to update this component, since the only UI change is this checkbox, which is already done by
// the user.
}
#removeRegexByIndex(index: number): void {
this.#regexPatterns.splice(index, 1);
// Call the set function to trigger the setting change event. we listen to this event and will update this component
// and the flame chart.
this.#getSkipStackFramesPatternSetting().setAsArray(this.#regexPatterns);
}
#renderItem(regex: Common.Settings.RegExpSettingItem, index: number): Lit.TemplateResult {
const checkboxWithLabel = UI.UIUtils.CheckboxLabel.createWithStringLiteral(
regex.pattern, !regex.disabled, /* jslogContext*/ 'timeline.ignore-list-pattern');
const helpText = i18nString(UIStrings.ignoreScriptsWhoseNamesMatchS, {regex: regex.pattern});
UI.Tooltip.Tooltip.install(checkboxWithLabel, helpText);
checkboxWithLabel.ariaLabel = helpText;
checkboxWithLabel.addEventListener(
'change', this.#onExistingRegexEnableToggle.bind(this, regex, checkboxWithLabel), false);
// clang-format off
return html`
<div class='regex-row'>
${checkboxWithLabel}
<devtools-button
=${this.#removeRegexByIndex.bind(this, index)}
.data=${{
variant: Buttons.Button.Variant.ICON,
iconName: 'bin',
title: i18nString(UIStrings.removeRegex, {regex: regex.pattern}),
jslogContext: 'timeline.ignore-list-pattern.remove',
} as Buttons.Button.ButtonData}></devtools-button>
</div>
`;
// clang-format on
}
#render(): void {
if (!ComponentHelpers.ScheduledRender.isScheduledRender(this)) {
throw new Error('Ignore List setting dialog render was not scheduled');
}
// clang-format off
const output = html`
<style>${ignoreListSettingStyles}</style>
<devtools-button-dialog .data=${{
openOnRender: false,
jslogContext: 'timeline.ignore-list',
variant: Buttons.Button.Variant.TOOLBAR,
iconName: 'compress',
disabled: !this.#ignoreListEnabled.get(),
iconTitle: i18nString(UIStrings.showIgnoreListSettingDialog),
horizontalAlignment: Dialogs.Dialog.DialogHorizontalAlignment.AUTO,
closeButton: true,
dialogTitle: i18nString(UIStrings.ignoreList),
} as Dialogs.ButtonDialog.ButtonDialogData}>
<div class='ignore-list-setting-content'>
<div class='ignore-list-setting-description'>${i18nString(UIStrings.ignoreListDescription)}</div>
${this.#getExistingRegexes().map(this.#renderItem.bind(this))}
${this.#renderNewRegexRow()}
</div>
</devtools-button-dialog>
`;
// clang-format on
Lit.render(output, this.#shadow, {host: this});
}
}
customElements.define('devtools-perf-ignore-list-setting', IgnoreListSetting);
declare global {
interface HTMLElementTagNameMap {
'devtools-perf-ignore-list-setting': IgnoreListSetting;
}
}
/**
* Returns if a new regex string is valid to be added to the ignore list.
* Note that things like duplicates are handled by the IgnoreList for us.
*
* @param inputValue the text input from the user we need to validate.
*/
export function regexInputIsValid(inputValue: string): boolean {
const pattern = inputValue.trim();
if (!pattern.length) {
return false;
}
let regex;
try {
regex = new RegExp(pattern);
} catch {
}
return Boolean(regex);
}