chrome-devtools-frontend
Version:
Chrome DevTools UI
492 lines (437 loc) • 15.4 kB
text/typescript
// Copyright 2020 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 @devtools/no-imperative-dom-api */
/* eslint-disable @devtools/no-lit-render-outside-of-view */
import '../../ui/legacy/legacy.js';
import type * as Common from '../../core/common/common.js';
import * as i18n from '../../core/i18n/i18n.js';
import type * as Protocol from '../../generated/protocol.js';
import * as UI from '../../ui/legacy/legacy.js';
import {Directives, html, type LitTemplate, nothing, render} from '../../ui/lit/lit.js';
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
import playerMessagesViewStyles from './playerMessagesView.css.js';
const {classMap} = Directives;
const UIStrings = {
/**
* @description A context menu item in the Console view of the Console panel.
*/
default: 'Default',
/**
* @description Text in Network throttling selector of the Network panel.
*/
custom: 'Custom',
/**
* @description Text for everything.
*/
all: 'All',
/**
* @description Text for errors.
*/
error: 'Error',
/**
* @description Text to indicate an item is a warning.
*/
warning: 'Warning',
/**
* @description Sdk console message level info in Console view of the Console panel.
*/
info: 'Info',
/**
* @description Debug log level.
*/
debug: 'Debug',
/**
* @description Label for selecting between the set of log levels to show.
*/
logLevel: 'Log level:',
/**
* @description Default text for user text entry for searching log messages.
*/
filterByLogMessages: 'Filter by log messages',
/**
* @description The label for the group name that this error belongs to.
*/
errorGroupLabel: 'Error group:',
/**
* @description The label for the numeric code associated with this error.
*/
errorCodeLabel: 'Error code:',
/**
* @description The label for extra data associated with an error.
*/
errorDataLabel: 'Data:',
/**
* @description The label for the stack trace associated with the error.
*/
errorStackLabel: 'Stack trace:',
/**
* @description The label for a root cause error associated with this error.
*/
errorCauseLabel: 'Caused by:',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/media/PlayerMessagesView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
const enum MessageLevelBitfield {
ERROR = 0b0001,
WARNING = 0b0010,
INFO = 0b0100,
DEBUG = 0b1000,
DEFAULT = 0b0111, // Error, Warning, Info
ALL = 0b1111, // Error, Warning, Info, Debug
CUSTOM = 0,
}
interface SelectableLevel {
title: string;
value: MessageLevelBitfield;
stringValue: string;
selectable?: boolean;
overwrite?: boolean;
}
class MessageLevelSelector implements UI.SoftDropDown.Delegate<SelectableLevel> {
private readonly items: UI.ListModel.ListModel<SelectableLevel>;
private readonly view: PlayerMessagesView;
private readonly itemMap: Map<number, SelectableLevel>;
private hiddenLevels: string[];
private bitFieldValue: MessageLevelBitfield;
readonly #defaultTitle: Common.UIString.LocalizedString;
private readonly customTitle: Common.UIString.LocalizedString;
private readonly allTitle: Common.UIString.LocalizedString;
elementsForItems: WeakMap<SelectableLevel, ShadowRoot>;
constructor(items: UI.ListModel.ListModel<SelectableLevel>, view: PlayerMessagesView) {
this.items = items;
this.view = view;
this.itemMap = new Map();
this.hiddenLevels = [];
this.bitFieldValue = MessageLevelBitfield.DEFAULT;
this.#defaultTitle = i18nString(UIStrings.default);
this.customTitle = i18nString(UIStrings.custom);
this.allTitle = i18nString(UIStrings.all);
this.elementsForItems = new WeakMap();
}
defaultTitle(): Common.UIString.LocalizedString {
return this.#defaultTitle;
}
setDefault(dropdown: UI.SoftDropDown.SoftDropDown<SelectableLevel>): void {
dropdown.selectItem(this.items.at(0));
}
populate(): void {
const defaultLevel = {
title: this.#defaultTitle,
overwrite: true,
stringValue: '',
value: MessageLevelBitfield.DEFAULT,
};
this.items.insert(this.items.length, defaultLevel);
this.itemMap.set(defaultLevel.value, defaultLevel);
const allLevel = {
title: this.allTitle,
overwrite: true,
stringValue: '',
value: MessageLevelBitfield.ALL,
};
this.items.insert(this.items.length, allLevel);
this.itemMap.set(allLevel.value, allLevel);
const errorLevel = {
title: i18nString(UIStrings.error),
overwrite: false,
stringValue: 'error',
value: MessageLevelBitfield.ERROR,
};
this.items.insert(this.items.length, errorLevel);
this.itemMap.set(errorLevel.value, errorLevel);
const warningLevel = {
title: i18nString(UIStrings.warning),
overwrite: false,
stringValue: 'warning',
value: MessageLevelBitfield.WARNING,
};
this.items.insert(this.items.length, warningLevel);
this.itemMap.set(warningLevel.value, warningLevel);
const infoLevel = {
title: i18nString(UIStrings.info),
overwrite: false,
stringValue: 'info',
value: MessageLevelBitfield.INFO,
};
this.items.insert(this.items.length, infoLevel);
this.itemMap.set(infoLevel.value, infoLevel);
const debugLevel = {
title: i18nString(UIStrings.debug),
overwrite: false,
stringValue: 'debug',
value: MessageLevelBitfield.DEBUG,
};
this.items.insert(this.items.length, debugLevel);
this.itemMap.set(debugLevel.value, debugLevel);
}
#renderItem(item: SelectableLevel, target: ShadowRoot|HTMLElement): void {
const checked = Boolean(item.value & this.bitFieldValue);
// clang-format off
render(html`
<div class="media-messages-level-dropdown-element">
<div class="media-messages-level-dropdown-checkbox">
${!item.overwrite && checked ? html`<div>✓</div>` : nothing}
</div>
<span class="media-messages-level-dropdown-text">${item.title}</span>
</div>
`, target, {host: this});
// clang-format on
}
private updateCheckMarks(): void {
this.hiddenLevels = [];
for (const [key, item] of this.itemMap) {
if (!item.overwrite) {
if (!(key & this.bitFieldValue)) {
this.hiddenLevels.push(item.stringValue);
}
const target = this.elementsForItems.get(item);
if (target) {
this.#renderItem(item, target);
}
}
}
}
titleFor(item: SelectableLevel): string {
// This would make a lot more sense to have in |itemSelected|, but this
// method gets called first.
if (item.overwrite) {
this.bitFieldValue = item.value;
} else {
this.bitFieldValue ^= item.value;
}
if (this.bitFieldValue === MessageLevelBitfield.DEFAULT) {
return this.#defaultTitle;
}
if (this.bitFieldValue === MessageLevelBitfield.ALL) {
return this.allTitle;
}
const potentialMatch = this.itemMap.get(this.bitFieldValue);
if (potentialMatch) {
return potentialMatch.title;
}
return this.customTitle;
}
createElementForItem(item: SelectableLevel): Element {
const element = document.createElement('div');
const shadowRoot = UI.UIUtils.createShadowRootWithCoreStyles(element, {cssFile: playerMessagesViewStyles});
this.elementsForItems.set(item, shadowRoot);
this.itemMap.set(item.value, item);
this.#renderItem(item, shadowRoot);
this.updateCheckMarks();
this.view.regenerateMessageDisplayCss(this.hiddenLevels);
return element;
}
isItemSelectable(_item: SelectableLevel): boolean {
return true;
}
itemSelected(_item: SelectableLevel|null): void {
this.updateCheckMarks();
this.view.regenerateMessageDisplayCss(this.hiddenLevels);
}
highlightedItemChanged(
_from: SelectableLevel|null, _to: SelectableLevel|null, _fromElement: Element|null,
_toElement: Element|null): void {
}
}
interface MessageItem {
type: 'message';
level: string;
message: Protocol.Media.PlayerMessage;
}
interface ErrorItem {
type: 'error';
level: 'error';
error: Protocol.Media.PlayerError;
}
type MessageOrErrorItem = MessageItem|ErrorItem;
export class PlayerMessagesView extends UI.Widget.VBox {
private readonly headerPanel: HTMLElement;
private readonly bodyPanel: HTMLElement;
private messageLevelSelector?: MessageLevelSelector;
#items: MessageOrErrorItem[] = [];
#hiddenLevels: string[] = [];
#filterString = '';
#dropDownItem?: UI.Toolbar.ToolbarItem;
#filterInput?: UI.Toolbar.ToolbarInput;
constructor() {
super({jslog: `${VisualLogging.pane('messages')}`});
this.registerRequiredCSS(playerMessagesViewStyles);
this.headerPanel = this.contentElement.createChild('div', 'media-messages-header');
this.bodyPanel = this.contentElement.createChild('div', 'media-messages-body');
this.#dropDownItem = this.createDropdown();
this.#filterInput = this.createFilterInput();
this.performUpdate();
}
private createDropdown(): UI.Toolbar.ToolbarItem {
const items = new UI.ListModel.ListModel<SelectableLevel>();
this.messageLevelSelector = new MessageLevelSelector(items, this);
const dropDown = new UI.SoftDropDown.SoftDropDown<SelectableLevel>(items, this.messageLevelSelector, 'log-level');
dropDown.setRowHeight(18);
this.messageLevelSelector.populate();
this.messageLevelSelector.setDefault(dropDown);
const dropDownItem = new UI.Toolbar.ToolbarItem(dropDown.element);
dropDownItem.element.classList.add('toolbar-has-dropdown');
dropDownItem.setEnabled(true);
dropDownItem.setTitle(this.messageLevelSelector.defaultTitle());
UI.ARIAUtils.setLabel(
dropDownItem.element, `${i18nString(UIStrings.logLevel)} ${this.messageLevelSelector.defaultTitle()}`);
return dropDownItem;
}
private createFilterInput(): UI.Toolbar.ToolbarInput {
const filterInput = new UI.Toolbar.ToolbarFilter(i18nString(UIStrings.filterByLogMessages), 1, 1);
filterInput.addEventListener(UI.Toolbar.ToolbarInput.Event.TEXT_CHANGED, (data: {data: string}) => {
this.filterByString(data as {
data: string,
});
}, this);
return filterInput;
}
override performUpdate(): void {
this.#renderToolbar();
this.#renderMessages();
}
#renderToolbar(): void {
// clang-format off
render(html`
<devtools-toolbar class="media-messages-toolbar">
<div class="toolbar-text">${i18nString(UIStrings.logLevel)}</div>
${this.#dropDownItem?.element}
<div class="toolbar-divider"></div>
${this.#filterInput?.element}
</devtools-toolbar>
`, this.headerPanel, {host: this});
// clang-format on
}
#renderMessages(): void {
// clang-format off
render(html`
${this.#items.map(item => {
const isUnselected = this.#isLevelHidden(item.level);
let isFiltered = false;
if (this.#filterString !== '') {
if (item.type === 'message') {
isFiltered = !item.message.message.includes(this.#filterString);
} else {
isFiltered = !this.#errorMatchesFilter(item.error, this.#filterString);
}
}
const classes = {
'media-messages-message-container': true,
[`media-message-${item.level}`]: true,
'media-messages-message-unselected': isUnselected,
'media-messages-message-filtered': isFiltered,
};
return html`
<div class=${classMap(classes)}>
${item.type === 'message' ? item.message.message : this.renderError(item.error)}
</div>
`;
})}
`, this.bodyPanel, {host: this});
// clang-format on
}
#isLevelHidden(level: string): boolean {
return this.#hiddenLevels.includes(level);
}
#errorMatchesFilter(error: Protocol.Media.PlayerError, filter: string): boolean {
if (error.errorType.includes(filter) || error.code.toString().includes(filter)) {
return true;
}
for (const [key, value] of Object.entries(error.data)) {
if (`${key}: ${value}`.includes(filter)) {
return true;
}
}
for (const stackEntry of error.stack) {
if (`${stackEntry.file}:${stackEntry.line}`.includes(filter)) {
return true;
}
}
for (const cause of error.cause) {
if (this.#errorMatchesFilter(cause, filter)) {
return true;
}
}
return false;
}
regenerateMessageDisplayCss(hiddenLevels: string[]): void {
this.#hiddenLevels = hiddenLevels;
this.performUpdate();
}
private filterByString(userStringData: {data: string}): void {
this.#filterString = userStringData.data;
this.performUpdate();
}
addMessage(message: Protocol.Media.PlayerMessage): void {
this.#items.push({type: 'message', level: message.level, message});
this.performUpdate();
}
private renderError(error: Protocol.Media.PlayerError): LitTemplate {
// clang-format off
return html`
<div class="status-error-box">
<div class="status-error-field-labeled">
<span class="status-error-field-label"
>${i18nString(UIStrings.errorGroupLabel)}</span
>
<span>${error.errorType}</span>
</div>
<div class="status-error-field-labeled">
<span class="status-error-field-label"
>${i18nString(UIStrings.errorCodeLabel)}</span
>
<span>${error.code}</span>
</div>
<div class="status-error-field-labeled">
${
Object.keys(error.data).length !== 0
? html`<span class="status-error-field-label"
>${i18nString(UIStrings.errorDataLabel)}</span
>
<div>
${Object.entries(error.data).map(
([key, value]) => html`<div>${key}: ${value}</div>`,
)}
</div>`
: nothing
}
</div>
<div class="status-error-field-labeled">
${
error.stack.length !== 0
? html`<span class="status-error-field-label"
>${i18nString(UIStrings.errorStackLabel)}</span
>
<div>
${error.stack.map(
stackEntry =>
html`<div>${stackEntry.file}:${stackEntry.line}</div>`,
)}
</div>`
: nothing
}
</div>
<div class="status-error-field-labeled">
${
error.cause.length !== 0
? html`
<span class="status-error-field-label"
>${i18nString(UIStrings.errorCauseLabel)}</span
>
<div>
${error.cause.map(cause => this.renderError(cause))}
</div>
`
: nothing
}
</div>
</div>
`;
// clang-format on
}
addError(error: Protocol.Media.PlayerError): void {
this.#items.push({type: 'error', level: 'error', error});
this.performUpdate();
}
}