chrome-devtools-frontend
Version:
Chrome DevTools UI
248 lines (223 loc) • 9.6 kB
text/typescript
// Copyright 2017 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 Common from '../../core/common/common.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as Protocol from '../../generated/protocol.js';
import type * as TextUtils from '../../models/text_utils/text_utils.js';
import * as UI from '../../ui/legacy/legacy.js';
import * as Lit from '../../ui/lit/lit.js';
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
import {ConsoleFilter, FilterType, type LevelsMask} from './ConsoleFilter.js';
import consoleSidebarStyles from './consoleSidebar.css.js';
import type {ConsoleViewMessage} from './ConsoleViewMessage.js';
const UIStrings = {
/**
* @description Filter name in Console Sidebar of the Console panel. This is shown when we fail to
* parse a URL when trying to display console messages from each URL separately. This might be
* because the console message does not come from any particular URL. This should be translated as
* a term that indicates 'not one of the other URLs listed here'.
*/
other: '<other>',
/**
* @description Text in Console Sidebar of the Console panel to show how many user messages exist.
*/
dUserMessages: '{n, plural, =0 {No user messages} =1 {# user message} other {# user messages}}',
/**
* @description Text in Console Sidebar of the Console panel to show how many messages exist.
*/
dMessages: '{n, plural, =0 {No messages} =1 {# message} other {# messages}}',
/**
* @description Text in Console Sidebar of the Console panel to show how many errors exist.
*/
dErrors: '{n, plural, =0 {No errors} =1 {# error} other {# errors}}',
/**
* @description Text in Console Sidebar of the Console panel to show how many warnings exist.
*/
dWarnings: '{n, plural, =0 {No warnings} =1 {# warning} other {# warnings}}',
/**
* @description Text in Console Sidebar of the Console panel to show how many info messages exist.
*/
dInfo: '{n, plural, =0 {No info} =1 {# info} other {# info}}',
/**
* @description Text in Console Sidebar of the Console panel to show how many verbose messages exist.
*/
dVerbose: '{n, plural, =0 {No verbose} =1 {# verbose} other {# verbose}}',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/console/ConsoleSidebar.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
const {render, html, nothing, Directives} = Lit;
export const enum GroupName {
CONSOLE_API = 'user message',
ALL = 'message',
ERROR = 'error',
WARNING = 'warning',
INFO = 'info',
VERBOSE = 'verbose',
}
const GROUP_ICONS = {
[GroupName.ALL]: {icon: 'list', label: UIStrings.dMessages},
[GroupName.CONSOLE_API]: {icon: 'profile', label: UIStrings.dUserMessages},
[GroupName.ERROR]: {icon: 'cross-circle', label: UIStrings.dErrors},
[GroupName.WARNING]: {icon: 'warning', label: UIStrings.dWarnings},
[GroupName.INFO]: {icon: 'info', label: UIStrings.dInfo},
[GroupName.VERBOSE]: {icon: 'bug', label: UIStrings.dVerbose},
};
interface ViewInput {
groups: ConsoleFilterGroup[];
selectedFilter: ConsoleFilter;
onSelectionChanged: (selectedFilter: ConsoleFilter) => void;
}
export type View = (input: ViewInput, output: object, target: HTMLElement) => void;
export const DEFAULT_VIEW: View = (input, output, target) => {
const nodeFilterMap = new WeakMap<Element, ConsoleFilter>();
const onSelectionChanged = (event: UI.TreeOutline.TreeViewElement.SelectEvent): void => {
const filter = nodeFilterMap.get(event.detail);
if (filter) {
input.onSelectionChanged(filter);
}
};
render(
html`<devtools-tree
navigation-variant
hide-overflow
@select=${onSelectionChanged}
.template=${
html`
<ul role="tree">
${
input.groups.map(
group => html`
<li
role="treeitem"
${Directives.ref(element => element && nodeFilterMap.set(element, group.filter))}
?selected=${group.filter === input.selectedFilter}>
<style>${consoleSidebarStyles}</style>
<devtools-icon name=${GROUP_ICONS[group.name].icon}></devtools-icon>
${
/* eslint-disable-next-line @devtools/l10n-i18nString-call-only-with-uistrings */
i18nString(GROUP_ICONS[group.name].label, {
n: group.messageCount
})}
${group.messageCount === 0 ? nothing : html`
<ul role="group" hidden>
${group.urlGroups.values().map(urlGroup => html`
<li
${Directives.ref(element => element && nodeFilterMap.set(element, urlGroup.filter))}
role="treeitem"
?selected=${urlGroup.filter === input.selectedFilter}
title=${urlGroup.url ?? ''}>
<devtools-icon name=document></devtools-icon>
${urlGroup.filter.name} <span class=count>${urlGroup.count}</span>
</li>`)}
</ul>`}
</li>`)}
</ul>`}
></devtools-tree>`,
target);
};
export class ConsoleFilterGroup {
readonly urlGroups = new Map<string|null, {filter: ConsoleFilter, url: string|null, count: number}>();
messageCount = 0;
readonly name: GroupName;
readonly filter: ConsoleFilter;
constructor(name: GroupName, parsedFilters: TextUtils.TextUtils.ParsedFilter[], levelsMask: LevelsMask) {
this.name = name;
this.filter = new ConsoleFilter(name, parsedFilters, null, levelsMask);
}
onMessage(viewMessage: ConsoleViewMessage): void {
const message = viewMessage.consoleMessage();
const shouldIncrementCounter = message.type !== SDK.ConsoleModel.FrontendMessageType.Command &&
message.type !== SDK.ConsoleModel.FrontendMessageType.Result && !message.isGroupMessage();
if (!this.filter.shouldBeVisible(viewMessage) || !shouldIncrementCounter) {
return;
}
const child = this.#getUrlGroup(message.url || null);
child.count++;
this.messageCount++;
}
clear(): void {
this.messageCount = 0;
this.urlGroups.clear();
}
#getUrlGroup(url: string|null): {filter: ConsoleFilter, url: string|null, count: number} {
let child = this.urlGroups.get(url);
if (child) {
return child;
}
const filter = this.filter.clone();
child = {filter, url, count: 0};
const parsedURL = url ? Common.ParsedURL.ParsedURL.fromString(url) : null;
if (url) {
filter.name = parsedURL ? parsedURL.displayName : url;
} else {
filter.name = i18nString(UIStrings.other);
}
filter.parsedFilters.push({key: FilterType.Url, text: url, negative: false, regex: undefined});
this.urlGroups.set(url, child);
return child;
}
}
const CONSOLE_API_PARSED_FILTERS = [{
key: FilterType.Source,
text: Common.Console.FrontendMessageSource.ConsoleAPI,
negative: false,
regex: undefined,
}];
export class ConsoleSidebar extends Common.ObjectWrapper.eventMixin<EventTypes, typeof UI.Widget.VBox>(UI.Widget.VBox) {
#view: View;
readonly #groups = [
new ConsoleFilterGroup(GroupName.ALL, [], ConsoleFilter.allLevelsFilterValue()),
new ConsoleFilterGroup(GroupName.CONSOLE_API, CONSOLE_API_PARSED_FILTERS, ConsoleFilter.allLevelsFilterValue()),
new ConsoleFilterGroup(GroupName.ERROR, [], ConsoleFilter.singleLevelMask(Protocol.Log.LogEntryLevel.Error)),
new ConsoleFilterGroup(GroupName.WARNING, [], ConsoleFilter.singleLevelMask(Protocol.Log.LogEntryLevel.Warning)),
new ConsoleFilterGroup(GroupName.INFO, [], ConsoleFilter.singleLevelMask(Protocol.Log.LogEntryLevel.Info)),
new ConsoleFilterGroup(GroupName.VERBOSE, [], ConsoleFilter.singleLevelMask(Protocol.Log.LogEntryLevel.Verbose)),
];
readonly #selectedFilterSetting =
Common.Settings.Settings.instance().createSetting<string|null>('console.sidebar-selected-filter', null);
#selectedFilter = this.#groups.find(group => group.name === this.#selectedFilterSetting.get())?.filter;
constructor(element?: HTMLElement, view = DEFAULT_VIEW) {
super(element, {
jslog: `${VisualLogging.pane('sidebar').track({resize: true})}`,
useShadowDom: true,
});
this.#view = view;
this.setMinimumSize(125, 0);
this.performUpdate();
}
override performUpdate(): void {
const input: ViewInput = {
groups: this.#groups,
selectedFilter: this.#selectedFilter ?? this.#groups[0].filter,
onSelectionChanged: filter => {
this.#selectedFilter = filter;
this.#selectedFilterSetting.set(filter.name);
this.dispatchEventToListeners(Events.FILTER_SELECTED);
},
};
this.#view(input, {}, this.contentElement);
}
clear(): void {
for (const group of this.#groups) {
group.clear();
}
this.requestUpdate();
}
onMessageAdded(viewMessage: ConsoleViewMessage): void {
for (const group of this.#groups) {
group.onMessage(viewMessage);
}
this.requestUpdate();
}
shouldBeVisible(viewMessage: ConsoleViewMessage): boolean {
return this.#selectedFilter?.shouldBeVisible(viewMessage) ?? true;
}
}
export const enum Events {
FILTER_SELECTED = 'FilterSelected',
}
export interface EventTypes {
[Events.FILTER_SELECTED]: void;
}