chrome-devtools-frontend
Version:
Chrome DevTools UI
238 lines (214 loc) • 8.04 kB
text/typescript
// 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,
}));
}
}