blob: 88db1fbb2cbc8f49f088553a5e02dd34ca0be635 [file] [log] [blame]
// Copyright 2020 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.
import type * as Common from '../../core/common/common.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as UI from '../../ui/legacy/legacy.js';
import playerMessagesViewStyles from './playerMessagesView.css.js';
import type * as Protocol from '../../generated/protocol.js';
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 message level info of level Labels 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.
*/
filterLogMessages: 'Filter 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 stacktrace associated with the error.
*/
errorStackLabel: 'Stacktrace:',
/**
*@description The label for a root cause error associated with this error.
*/
errorCauseLabel: 'Caused by:',
};
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;
private readonly savedBitFieldValue: MessageLevelBitfield;
private readonly defaultTitleInternal: Common.UIString.LocalizedString;
private readonly customTitle: Common.UIString.LocalizedString;
private readonly allTitle: Common.UIString.LocalizedString;
elementsForItems: WeakMap<SelectableLevel, HTMLElement>;
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.savedBitFieldValue = MessageLevelBitfield.Default;
this.defaultTitleInternal = i18nString(UIStrings.default);
this.customTitle = i18nString(UIStrings.custom);
this.allTitle = i18nString(UIStrings.all);
this.elementsForItems = new WeakMap();
}
defaultTitle(): Common.UIString.LocalizedString {
return this.defaultTitleInternal;
}
setDefault(dropdown: UI.SoftDropDown.SoftDropDown<SelectableLevel>): void {
dropdown.selectItem(this.items.at(0));
}
populate(): void {
this.items.insert(this.items.length, {
title: this.defaultTitleInternal,
overwrite: true,
stringValue: '',
value: MessageLevelBitfield.Default,
selectable: undefined,
});
this.items.insert(this.items.length, {
title: this.allTitle,
overwrite: true,
stringValue: '',
value: MessageLevelBitfield.All,
selectable: undefined,
});
this.items.insert(this.items.length, {
title: i18nString(UIStrings.error),
overwrite: false,
stringValue: 'error',
value: MessageLevelBitfield.Error,
selectable: undefined,
});
this.items.insert(this.items.length, {
title: i18nString(UIStrings.warning),
overwrite: false,
stringValue: 'warning',
value: MessageLevelBitfield.Warning,
selectable: undefined,
});
this.items.insert(this.items.length, {
title: i18nString(UIStrings.info),
overwrite: false,
stringValue: 'info',
value: MessageLevelBitfield.Info,
selectable: undefined,
});
this.items.insert(this.items.length, {
title: i18nString(UIStrings.debug),
overwrite: false,
stringValue: 'debug',
value: MessageLevelBitfield.Debug,
selectable: undefined,
});
}
private updateCheckMarks(): void {
this.hiddenLevels = [];
for (const [key, item] of this.itemMap) {
if (!item.overwrite) {
const elementForItem = this.elementsForItems.get(item as SelectableLevel);
if (elementForItem && elementForItem.firstChild) {
elementForItem.firstChild.remove();
}
if (elementForItem && key & this.bitFieldValue) {
UI.UIUtils.createTextChild(elementForItem.createChild('div'), '✓');
} else {
this.hiddenLevels.push(item.stringValue);
}
}
}
}
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.defaultTitleInternal;
}
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.Utils.createShadowRootWithCoreStyles(
element, {cssFile: [playerMessagesViewStyles], delegatesFocus: undefined});
const container = shadowRoot.createChild('div', 'media-messages-level-dropdown-element');
const checkBox = container.createChild('div', 'media-messages-level-dropdown-checkbox') as HTMLElement;
const text = container.createChild('span', 'media-messages-level-dropdown-text');
UI.UIUtils.createTextChild(text, item.title);
this.elementsForItems.set(item, checkBox);
this.itemMap.set(item.value, item);
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 {
}
}
export class PlayerMessagesView extends UI.Widget.VBox {
private readonly headerPanel: HTMLElement;
private readonly bodyPanel: HTMLElement;
private messageLevelSelector?: MessageLevelSelector;
constructor() {
super();
this.headerPanel = this.contentElement.createChild('div', 'media-messages-header');
this.bodyPanel = this.contentElement.createChild('div', 'media-messages-body');
this.buildToolbar();
}
private buildToolbar(): void {
const toolbar = new UI.Toolbar.Toolbar('media-messages-toolbar', this.headerPanel);
toolbar.appendText(i18nString(UIStrings.logLevel));
toolbar.appendToolbarItem(this.createDropdown());
toolbar.appendSeparator();
toolbar.appendToolbarItem(this.createFilterInput());
}
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);
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());
return dropDownItem;
}
private createFilterInput(): UI.Toolbar.ToolbarInput {
const filterInput = new UI.Toolbar.ToolbarInput(i18nString(UIStrings.filterLogMessages));
filterInput.addEventListener(UI.Toolbar.ToolbarInput.Event.TextChanged, (data: {data: string}) => {
this.filterByString(data as {
data: string,
});
}, this);
return filterInput;
}
regenerateMessageDisplayCss(hiddenLevels: string[]): void {
const messages = this.bodyPanel.getElementsByClassName('media-messages-message-container');
for (const message of messages) {
if (this.matchesHiddenLevels(message, hiddenLevels)) {
message.classList.add('media-messages-message-unselected');
} else {
message.classList.remove('media-messages-message-unselected');
}
}
}
private matchesHiddenLevels(element: Element, hiddenLevels: string[]): boolean {
for (const level of hiddenLevels) {
if (element.classList.contains('media-message-' + level)) {
return true;
}
}
return false;
}
private filterByString(userStringData: {data: string}): void {
const userString = userStringData.data;
const messages = this.bodyPanel.getElementsByClassName('media-messages-message-container');
for (const message of messages) {
if (userString === '') {
message.classList.remove('media-messages-message-filtered');
} else if (message.textContent && message.textContent.includes(userString)) {
message.classList.remove('media-messages-message-filtered');
} else {
message.classList.add('media-messages-message-filtered');
}
}
}
addMessage(message: Protocol.Media.PlayerMessage): void {
const container =
this.bodyPanel.createChild('div', 'media-messages-message-container media-message-' + message.level);
UI.UIUtils.createTextChild(container, message.message);
}
private errorToDiv(error: Protocol.Media.PlayerError): Element {
const entry = UI.Fragment.Fragment.build`
<div class="status-error-box">
<div class="status-error-field-labeled">
<span class="status-error-field-label" $="status-error-group"></span>
<span>${error.errorType}</span>
</div>
<div class="status-error-field-labeled">
<span class="status-error-field-label" $="status-error-code"></span>
<span>${error.code}</span>
</div>
<div class="status-error-field-labeled" $="status-error-data">
</div>
<div class="status-error-field-labeled" $="status-error-stack">
</div>
<div class="status-error-field-labeled" $="status-error-cause">
</div>
`;
entry.$('status-error-group').textContent = i18nString(UIStrings.errorGroupLabel);
entry.$('status-error-code').textContent = i18nString(UIStrings.errorCodeLabel);
if (Object.keys(error.data).length !== 0) {
const label = entry.$('status-error-data').createChild('span', 'status-error-field-label');
UI.UIUtils.createTextChild(label, i18nString(UIStrings.errorDataLabel));
const dataContent = entry.$('status-error-data').createChild('div');
for (const [key, value] of Object.entries(error.data)) {
const datumContent = dataContent.createChild('div');
UI.UIUtils.createTextChild(datumContent, `${key}: ${value}`);
}
}
if (error.stack.length !== 0) {
const label = entry.$('status-error-stack').createChild('span', 'status-error-field-label');
UI.UIUtils.createTextChild(label, i18nString(UIStrings.errorStackLabel));
const stackContent = entry.$('status-error-stack').createChild('div');
for (const stackEntry of error.stack) {
const frameBox = stackContent.createChild('div');
UI.UIUtils.createTextChild(frameBox, `${stackEntry.file}:${stackEntry.line}`);
}
}
if (error.cause.length !== 0) {
const label = entry.$('status-error-cause').createChild('span', 'status-error-field-label');
UI.UIUtils.createTextChild(label, i18nString(UIStrings.errorCauseLabel));
entry.$('status-error-cause').appendChild(this.errorToDiv(error.cause[0]));
}
return entry.element();
}
addError(error: Protocol.Media.PlayerError): void {
const container = this.bodyPanel.createChild('div', 'media-messages-message-container media-message-error');
container.appendChild(this.errorToDiv(error));
}
wasShown(): void {
super.wasShown();
this.registerCSSFiles([playerMessagesViewStyles]);
}
}