| // Copyright 2026 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/legacy/components/data_grid/data_grid.js'; | |
| import * as Host from '../../../core/host/host.js'; | |
| import * as i18n from '../../../core/i18n/i18n.js'; | |
| import type * as TextUtils from '../../../core/text_utils/text_utils.js'; | |
| import * as UI from '../../../ui/legacy/legacy.js'; | |
| import {html, render} from '../../../ui/lit/lit.js'; | |
| const UIStrings = { | |
| /** | |
| * @description Column header for the key in the crash report context datagrid in the Application panel. | |
| */ | |
| key: 'Key', | |
| /** | |
| * @description Column header for the value in the crash report context datagrid in the Application panel. | |
| */ | |
| value: 'Value', | |
| /** | |
| * @description Context menu item to copy the key of a crash report context entry in the Application panel. | |
| */ | |
| copyKey: 'Copy key', | |
| /** | |
| * @description Context menu item to copy the value of a crash report context entry in the Application panel. | |
| */ | |
| copyValue: 'Copy value', | |
| } as const; | |
| const str_ = i18n.i18n.registerUIStrings('panels/application/components/CrashReportContextGrid.ts', UIStrings); | |
| export const i18nString: i18n.LocalizeString = i18n.i18n.getLocalizedString.bind(undefined, str_); | |
| export interface CrashReportContextGridData { | |
| entries: Array<{key: string, value: string}>; | |
| selectedKey?: string; | |
| filters?: TextUtils.TextUtils.ParsedFilter[]; | |
| } | |
| export interface ViewInput { | |
| entries: Array<{key: string, value: string}>; | |
| selectedKey?: string; | |
| onSelect: (key: string) => void; | |
| onContextMenu: (e: Event, key: string, value: string) => void; | |
| } | |
| export const DEFAULT_VIEW = (input: ViewInput, output: undefined, target: HTMLElement|ShadowRoot): void => { | |
| // clang-format off | |
| render( | |
| html` | |
| <style> | |
| :host { | |
| display: block; | |
| } | |
| div { | |
| overflow: auto; | |
| } | |
| td { | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| } | |
| </style> | |
| <style>${UI.inspectorCommonStyles}</style> | |
| <div> | |
| <devtools-data-grid striped inline> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th id="key" weight="50">${i18nString(UIStrings.key)}</th> | |
| <th id="value" weight="50">${i18nString(UIStrings.value)}</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| ${input.entries.map(entry => html` | |
| <tr class=${input.selectedKey === entry.key ? 'selected' : ''} | |
| @select=${() => input.onSelect(entry.key)} | |
| @contextmenu=${(e: Event) => input.onContextMenu(e, entry.key, entry.value)}> | |
| <td title=${entry.key}>${entry.key}</td> | |
| <td title=${entry.value}>${entry.value}</td> | |
| </tr> | |
| `)} | |
| </tbody> | |
| </table> | |
| </devtools-data-grid> | |
| </div> | |
| `, | |
| target); | |
| // clang-format on | |
| }; | |
| type View = (input: ViewInput, output: undefined, target: HTMLElement|ShadowRoot) => void; | |
| export class CrashReportContextGrid extends UI.Widget.Widget { | |
| #entries: Array<{key: string, value: string}> = []; | |
| #filteredEntries: Array<{key: string, value: string}> = []; | |
| #selectedKey?: string; | |
| #filters: TextUtils.TextUtils.ParsedFilter[] = []; | |
| readonly #view: View; | |
| constructor(element?: HTMLElement, view: View = DEFAULT_VIEW) { | |
| super(element, {useShadowDom: true}); | |
| this.#view = view; | |
| } | |
| set data(data: CrashReportContextGridData) { | |
| this.#entries = data.entries; | |
| this.#selectedKey = data.selectedKey; | |
| this.#filters = data.filters || []; | |
| this.requestUpdate(); | |
| } | |
| #computeFilteredEntries(): void { | |
| if (this.#filters.length === 0) { | |
| this.#filteredEntries = this.#entries; | |
| return; | |
| } | |
| this.#filteredEntries = this.#entries.filter(entry => { | |
| return this.#filters.every(filter => { | |
| const regex = filter.regex; | |
| if (!regex) { | |
| return true; | |
| } | |
| const matches = regex.test(entry.key) || regex.test(entry.value); | |
| return filter.negative ? !matches : matches; | |
| }); | |
| }); | |
| } | |
| #onContextMenu(e: Event, key: string, value: string): void { | |
| const customEvent = e as CustomEvent<UI.ContextMenu.ContextMenu>; | |
| const contextMenu = customEvent.detail; | |
| contextMenu.defaultSection().appendItem(i18nString(UIStrings.copyKey), () => { | |
| Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(key); | |
| }, {jslogContext: 'copy-key'}); | |
| contextMenu.defaultSection().appendItem(i18nString(UIStrings.copyValue), () => { | |
| Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(value); | |
| }, {jslogContext: 'copy-value'}); | |
| } | |
| override performUpdate(): void { | |
| this.#computeFilteredEntries(); | |
| this.#view( | |
| { | |
| entries: this.#filteredEntries, | |
| selectedKey: this.#selectedKey, | |
| onSelect: (key: string) => this.element.dispatchEvent(new CustomEvent('select', {detail: key})), | |
| onContextMenu: (e: Event, key: string, value: string) => this.#onContextMenu(e, key, value), | |
| }, | |
| undefined, this.contentElement); | |
| } | |
| } |