UNPKG

chrome-devtools-frontend

Version:
238 lines (214 loc) • 8.04 kB
// 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/legacy.js'; import * as i18n from '../../core/i18n/i18n.js'; import * as Platform from '../../core/platform/platform.js'; import * as SDK from '../../core/sdk/sdk.js'; import type * as Protocol from '../../generated/protocol.js'; import type * as TextUtils from '../../models/text_utils/text_utils.js'; import * as Buttons from '../../ui/components/buttons/buttons.js'; import * as UI from '../../ui/legacy/legacy.js'; import {html, render} from '../../ui/lit/lit.js'; import * as VisualLogging from '../../ui/visual_logging/visual_logging.js'; import * as ApplicationComponents from './components/components.js'; const UIStrings = { /** * @description Placeholder text when no context is detected. */ noContext: 'No context entries detected across frames.', /** * @description Fallback label when a frame has no URL. */ unknownFrame: 'Unknown Frame', /** * @description Placeholder for a search field in a toolbar */ filterByText: 'Filter by key or value', /** * @description Text to refresh the page */ refresh: 'Refresh', } as const; const str_ = i18n.i18n.registerUIStrings('panels/application/CrashReportContextView.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); export interface FrameContextData { url: string; frameId: string; displayName: string; entries: Protocol.CrashReportContext.CrashReportContextEntry[]; } interface ViewInput { frames: FrameContextData[]; selectedKey: string|null; onRowSelected: (key: string) => void; onRefresh: () => void; onFilterChanged: (e: CustomEvent<string>) => void; filters: TextUtils.TextUtils.ParsedFilter[]; } type View = (input: ViewInput, output: undefined, target: HTMLElement) => void; export const DEFAULT_VIEW = (input: ViewInput, _output: undefined, target: HTMLElement): void => { const {widget} = UI.Widget; // clang-format off render( html` <style>${UI.inspectorCommonStyles}</style> <style> .crash-report-context-view { padding-top: 5px; overflow: auto; } .frame-section { margin-top: var(--sys-size-8); } .frame-section:first-child { margin-top: 0; } .frame-header { display: flex; align-items: center; padding: var(--sys-size-4) var(--sys-size-6); gap: var(--sys-size-6); background-color: var(--sys-color-surface2); border-bottom: 1px solid var(--sys-color-divider); } .frame-url { font-weight: var(--ref-typeface-weight-bold); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--default-font-family); } .toolbar-container { border-bottom: 1px solid var(--sys-color-divider); background-color: var(--sys-color-cdt-base-container); } </style> <div class="vbox flex-auto" jslog=${VisualLogging.pane('crash-report-context')}> <devtools-toolbar class="crash-report-context-toolbar" role="toolbar" jslog=${VisualLogging.toolbar()}> <devtools-button title=${i18nString(UIStrings.refresh)} @click=${input.onRefresh} .iconName=${'refresh'} .variant=${Buttons.Button.Variant.TOOLBAR} jslog=${VisualLogging.action('refresh').track({ click: true })}> </devtools-button> <devtools-toolbar-input type="filter" placeholder=${i18nString(UIStrings.filterByText)} @change=${(e: CustomEvent<string>) => input.onFilterChanged(e)} class="flex-auto"> </devtools-toolbar-input> </devtools-toolbar> ${input.frames.length > 0 ? html` <div class="crash-report-context-view flex-auto"> ${input.frames.map(frame => html` <div class="frame-section"> <div class="frame-header"> <span class="frame-url" title="URL: ${frame.url}\nFrame ID: ${frame.frameId}">${frame.displayName}</span> </div> <div class="grid-container"> <devtools-widget ${widget(ApplicationComponents.CrashReportContextGrid.CrashReportContextGrid, { data: { entries: frame.entries.map(e => ({key: e.key, value: e.value})), selectedKey: input.selectedKey || undefined, filters: input.filters } })} @select=${(e: CustomEvent<string>) => input.onRowSelected(e.detail)}> </devtools-widget> </div> </div> `)} </div> ` : html` ${widget(UI.EmptyWidget.EmptyWidget, { header: i18nString(UIStrings.noContext), })} `} </div> `, target); // clang-format on }; export class CrashReportContextView extends UI.Widget.VBox { private selectedKey: string|null = null; readonly #view: View; #filters: TextUtils.TextUtils.ParsedFilter[] = []; constructor(view: View = DEFAULT_VIEW) { super(); this.#view = view; this.requestUpdate(); } override async performUpdate(): Promise<void> { const models = SDK.TargetManager.TargetManager.instance().models(SDK.CrashReportContextModel.CrashReportContextModel); const allEntries = (await Promise.all(models.map(model => model.getEntries()))) .flat() .filter((entry): entry is Protocol.CrashReportContext.CrashReportContextEntry => entry !== null); const frameData = this.#processFrameData(allEntries); this.#view( { frames: frameData, selectedKey: this.selectedKey, filters: this.#filters, onRowSelected: (key: string) => { this.selectedKey = key; this.requestUpdate(); }, onRefresh: () => { this.requestUpdate(); }, onFilterChanged: (e: CustomEvent<string>) => { const text = e.detail; const textFilterRegExp = text ? Platform.StringUtilities.createPlainTextSearchRegex(text, 'i') : null; if (textFilterRegExp) { this.#filters = [ {key: 'key,value', regex: textFilterRegExp, negative: false}, ]; } else { this.#filters = []; } this.requestUpdate(); }, }, undefined, this.contentElement); } #processFrameData(allEntries: Protocol.CrashReportContext.CrashReportContextEntry[]): FrameContextData[] { if (allEntries.length === 0) { return []; } const entriesByFrame = Map.groupBy(allEntries, entry => entry.frameId); return [...entriesByFrame.entries()] .map(([frameId, frameEntries]) => { const frame = SDK.FrameManager.FrameManager.instance().getFrame(frameId as Protocol.Page.FrameId); const url = frame?.url || i18nString(UIStrings.unknownFrame); const displayName = frame?.displayName() || url; return { url, frameId, displayName, isMain: frame?.isMainFrame() ?? false, origin: frame?.securityOrigin || '', entries: frameEntries, }; }) // Ensure the main (outermost) frame is always listed first at the top of the View .sort((a, b) => { if (a.isMain && !b.isMain) { return -1; } if (!a.isMain && b.isMain) { return 1; } return 0; }) .map(data => ({ url: data.url, frameId: data.frameId, displayName: data.displayName, entries: data.entries, })); } }