chrome-devtools-frontend
Version:
Chrome DevTools UI
347 lines (307 loc) • 13.8 kB
text/typescript
// Copyright 2024 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/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 Workspace from '../../../models/workspace/workspace.js';
import * as Buttons from '../../../ui/components/buttons/buttons.js';
import * as Dialogs from '../../../ui/components/dialogs/dialogs.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, Directives} = Lit;
const {live} = Directives;
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_);
interface ViewInput {
ignoreListEnabled: boolean;
regexes: Common.Settings.RegExpSettingItem[];
newRegexValue: string;
newRegexChecked: boolean;
onExistingRegexEnableToggle: (regex: Common.Settings.RegExpSettingItem, checked: boolean) => void;
onRemoveRegexByIndex: (index: number) => void;
onNewRegexInputBlur: (value: string) => void;
onNewRegexInputChange: (value: string) => void;
onNewRegexInputFocus: (value: string) => void;
onNewRegexAdd: (value: string) => void;
onNewRegexCancel: () => void;
}
type View = (input: ViewInput, output: undefined, target: HTMLElement) => void;
export const DEFAULT_VIEW: View = (input, output, target) => {
const {
ignoreListEnabled,
regexes,
newRegexValue,
newRegexChecked,
onExistingRegexEnableToggle,
onRemoveRegexByIndex,
onNewRegexInputBlur,
onNewRegexInputChange,
onNewRegexInputFocus,
onNewRegexAdd,
onNewRegexCancel,
} = input;
function renderItem(regex: Common.Settings.RegExpSettingItem, index: number): Lit.TemplateResult {
const helpText = i18nString(UIStrings.ignoreScriptsWhoseNamesMatchS, {regex: regex.pattern});
// clang-format off
return html`
<div class='regex-row'>
<devtools-checkbox title=${helpText} aria-label=${helpText} ?checked=${!regex.disabled}
=${(event: Event) => onExistingRegexEnableToggle(regex, (event.currentTarget as UI.UIUtils.CheckboxLabel).checked)}
.jslogContext=${'timeline.ignore-list-pattern'}>${regex.pattern}</devtools-checkbox>
<devtools-button
=${() => onRemoveRegexByIndex(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
}
// clang-format off
Lit.render(html`
<style>${ignoreListSettingStyles}</style>
<devtools-button-dialog
=${(e: Event) => e.stopPropagation() /* Prevent the event making its way to the TimelinePanel element which will cause the "Load Profile" context menu to appear. */}
.data=${{
openOnRender: false,
jslogContext: 'timeline.ignore-list',
variant: Buttons.Button.Variant.TOOLBAR,
iconName: 'compress',
disabled: !ignoreListEnabled,
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>
${regexes.map(renderItem)}
<div class='new-regex-row'>
<devtools-checkbox
title=${i18nString(UIStrings.ignoreScriptsWhoseNamesMatchNewRegex)}
.jslogContext=${'timeline.ignore-list-new-regex.checkbox'}
.checked=${newRegexChecked}
>
</devtools-checkbox>
<input
=${(event: Event) => onNewRegexInputBlur((event.currentTarget as HTMLInputElement).value)}
=${(event: Event) => onNewRegexInputChange((event.currentTarget as HTMLInputElement).value)}
=${(event: Event) => onNewRegexInputFocus((event.currentTarget as HTMLInputElement).value)}
=${(event: KeyboardEvent) => {
const el = event.currentTarget as HTMLInputElement;
if (event.key === Platform.KeyboardUtilities.ENTER_KEY) {
onNewRegexAdd(el.value);
} else if (event.key === Platform.KeyboardUtilities.ESCAPE_KEY) {
onNewRegexCancel();
el.blur();
// Escape key will close the dialog, and toggle the `Console` drawer. So we need to ignore other listeners.
event.stopImmediatePropagation();
}
}}
class="harmony-input new-regex-text-input"
title=${i18nString(UIStrings.addNewRegex)}
placeholder='/framework\\.js$'
.value=${live(newRegexValue)}
.jslogContext=${'timeline.ignore-list-new-regex.text'}>
</input>
</div>
</div>
</devtools-button-dialog>
`, target);
// clang-format on
};
export class IgnoreListSetting extends UI.Widget.Widget {
static createWidgetElement(): UI.Widget.WidgetElement<IgnoreListSetting> {
const widgetElement = document.createElement('devtools-widget') as UI.Widget.WidgetElement<IgnoreListSetting>;
widgetElement.widgetConfig = UI.Widget.widgetConfig(IgnoreListSetting);
return widgetElement;
}
#view: View;
readonly #ignoreListEnabled: Common.Settings.Setting<boolean> =
Common.Settings.Settings.instance().moduleSetting('enable-ignore-listing');
readonly #regexPatterns = this.#getSkipStackFramesPatternSetting().getAsArray();
#newRegexValue = '';
#newRegexChecked = false;
#editingRegexSetting: Common.Settings.RegExpSettingItem|null = null;
constructor(element?: HTMLElement, view: View = DEFAULT_VIEW) {
super(element, {useShadowDom: true});
this.#view = view;
// Otherwise the button in the toolbar is too wide.
this.element.classList.remove('vbox', 'flex-auto');
Common.Settings.Settings.instance()
.moduleSetting('skip-stack-frames-pattern')
.addChangeListener(this.requestUpdate.bind(this));
Common.Settings.Settings.instance()
.moduleSetting('enable-ignore-listing')
.addChangeListener(this.requestUpdate.bind(this));
}
#getSkipStackFramesPatternSetting(): Common.Settings.RegExpSetting {
return Common.Settings.Settings.instance().moduleSetting('skip-stack-frames-pattern') as
Common.Settings.RegExpSetting;
}
#onNewRegexInputFocus(value: string): void {
// Do not need to trim here because this is a temporary one, we will trim the input when finish editing,
this.#editingRegexSetting = {pattern: 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.#newRegexValue = '';
this.#newRegexChecked = false;
this.requestUpdate();
}
#onNewRegexInputBlur(value: string): void {
const newRegex = value.trim();
this.#finishEditing();
if (!regexInputIsValid(newRegex)) {
// It the new regex is invalid, let's skip it.
return;
}
Workspace.IgnoreListManager.IgnoreListManager.instance().addRegexToIgnoreList(newRegex);
this.#resetInput();
}
#onNewRegexAdd(value: string): void {
this.#onNewRegexInputBlur(value);
this.#onNewRegexInputFocus('');
}
#onNewRegexCancel(): void {
this.#finishEditing();
this.#resetInput();
}
/**
* 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;
}
#onNewRegexInputChange(value: string): void {
const newRegex = value.trim();
this.#newRegexValue = newRegex;
if (this.#editingRegexSetting && regexInputIsValid(newRegex)) {
this.#editingRegexSetting.pattern = newRegex;
this.#editingRegexSetting.disabled = !Boolean(newRegex);
this.#getSkipStackFramesPatternSetting().setAsArray(this.#regexPatterns);
}
}
/**
* 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, checked: boolean): void {
regex.disabled = !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.
}
#onRemoveRegexByIndex(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);
}
override performUpdate(): void {
const input: ViewInput = {
ignoreListEnabled: this.#ignoreListEnabled.get(),
regexes: this.#getExistingRegexes(),
newRegexValue: this.#newRegexValue,
newRegexChecked: this.#newRegexChecked,
onExistingRegexEnableToggle: this.#onExistingRegexEnableToggle.bind(this),
onRemoveRegexByIndex: this.#onRemoveRegexByIndex.bind(this),
onNewRegexInputBlur: this.#onNewRegexInputBlur.bind(this),
onNewRegexInputChange: this.#onNewRegexInputChange.bind(this),
onNewRegexInputFocus: this.#onNewRegexInputFocus.bind(this),
onNewRegexAdd: this.#onNewRegexAdd.bind(this),
onNewRegexCancel: this.#onNewRegexCancel.bind(this),
};
this.#view(input, undefined, this.contentElement);
}
}
/**
* 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);
}