| // 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} = 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) => { |
| render( |
| html`<devtools-tree |
| navigation-variant |
| hide-overflow |
| .template=${ |
| html` |
| <ul role="tree"> |
| ${ |
| input.groups.map( |
| group => html` |
| <li |
| role="treeitem" |
| @select=${() => input.onSelectionChanged(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 |
| @select=${() => input.onSelectionChanged(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; |
| } |