// Copyright 2019 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 */
import * as i18n from '../../core/i18n/i18n.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as Bindings from '../../models/bindings/bindings.js';
import * as Logs from '../../models/logs/logs.js';
import type * as StackTrace from '../../models/stack_trace/stack_trace.js';
import * as Components from '../../ui/legacy/components/utils/utils.js';
import * as UI from '../../ui/legacy/legacy.js';
import {html, type LitTemplate, nothing, render, type TemplateResult} from '../../ui/lit/lit.js';
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
import requestInitiatorViewStyles from './requestInitiatorView.css.js';
import requestInitiatorViewTreeStyles from './requestInitiatorViewTree.css.js';
const {widgetConfig} = UI.Widget;
const UIStrings = {
/**
* @description Text in Request Initiator View of the Network panel if the request has no initiator data
*/
noInitiator: 'No initiator data',
/**
* @description Title of a section in Request Initiator view of the Network Panel
*/
requestCallStack: 'Request call stack',
/**
* @description Title of a section in Request Initiator view of the Network Panel
*/
requestInitiatorChain: 'Request initiator chain',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/network/RequestInitiatorView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export interface ViewInput {
initiatorGraph: Logs.NetworkLog.InitiatorGraph;
stackTrace: StackTrace.StackTrace.StackTrace|null;
request: SDK.NetworkRequest.NetworkRequest;
}
export const DEFAULT_VIEW = (input: ViewInput, _output: undefined, target: HTMLElement): void => {
const hasInitiatorData =
input.initiatorGraph.initiators.size > 1 || input.initiatorGraph.initiated.size > 1 || input.stackTrace;
if (!hasInitiatorData) {
render(
html`
${i18nString(UIStrings.noInitiator)}
`,
target);
return;
}
const renderStackTraceSection = (): TemplateResult => {
if (!input.stackTrace) {
return html`${nothing}`;
}
return html`
${i18nString(UIStrings.requestCallStack)}
`;
};
const renderInitiatorNodes =
(initiators: SDK.NetworkRequest.NetworkRequest[], index: number,
initiated: Map,
visited: Set): TemplateResult => {
if (index >= initiators.length) {
return html`${nothing}`;
}
const request = initiators[index];
const isCurrentRequest = (index === initiators.length - 1);
const hasFurtherInitiatedNodes = index + 1 < initiators.length;
const renderedChildren = isCurrentRequest ? renderInitiatedNodes(initiated, request, visited) : nothing;
// clang-format off
return html`
${request.url()}
${hasFurtherInitiatedNodes || renderedChildren !== nothing ? html`
${renderInitiatorNodes(initiators, index + 1, initiated, visited)}
${renderedChildren}
` : nothing}
`;
// clang-format on
};
const renderInitiatedNodes =
(initiated: Map,
parentRequest: SDK.NetworkRequest.NetworkRequest,
visited: Set): LitTemplate => {
const children = [];
for (const [request, initiator] of initiated) {
if (initiator === parentRequest) {
children.push(request);
}
}
if (children.length === 0) {
return nothing;
}
return html`
${children.map(child => {
const shouldRecurse = !visited.has(child);
if (shouldRecurse) {
visited.add(child);
}
const renderedChildren = shouldRecurse ? renderInitiatedNodes(initiated, child, visited) : nothing;
return html`
${child.url()}
${renderedChildren !== nothing ? html`` : nothing}
`;
})}
`;
};
const renderInitiatorChain = (initiatorGraph: Logs.NetworkLog.InitiatorGraph): TemplateResult => {
const initiators = Array.from(initiatorGraph.initiators).reverse();
const visited = new Set();
visited.add(input.request);
const hasInitiatorChain = initiators.length > 0;
// clang-format off
return html`
${i18nString(UIStrings.requestInitiatorChain)}
${hasInitiatorChain ? html`
${renderInitiatorNodes(initiators, 0, initiatorGraph.initiated, visited)}
` : nothing}
`;
// clang-format on
};
const hasInitiatorChain = input.initiatorGraph.initiators.size > 1 || input.initiatorGraph.initiated.size > 1;
// clang-format off
render(html`
${requestInitiatorViewTreeStyles}
${input.stackTrace || hasInitiatorChain ? html`
${renderStackTraceSection()}
${hasInitiatorChain ? renderInitiatorChain(input.initiatorGraph) : nothing}
` : nothing}
`}>
`, target);
// clang-format on
};
type View = typeof DEFAULT_VIEW;
export class RequestInitiatorView extends UI.Widget.VBox {
private readonly request: SDK.NetworkRequest.NetworkRequest;
#view: View;
constructor(request: SDK.NetworkRequest.NetworkRequest, view: View = DEFAULT_VIEW) {
super({jslog: `${VisualLogging.pane('initiator').track({resize: true})}`});
this.element.classList.add('request-initiator-view');
this.request = request;
this.#view = view;
}
static async createStackTracePreview(
request: SDK.NetworkRequest.NetworkRequest, linkifier: Components.Linkifier.Linkifier,
focusableLink?: boolean): Promise<{
preview: Components.JSPresentationUtils.StackTracePreviewContent,
stackTrace: StackTrace.StackTrace.StackTrace|null,
}|null> {
const initiator = request.initiator();
if (!initiator?.stack) {
return null;
}
const targetManager = SDK.TargetManager.TargetManager.instance();
const networkManager = SDK.NetworkManager.NetworkManager.forRequest(request);
const target = networkManager?.target() ?? targetManager.primaryPageTarget() ?? targetManager.rootTarget();
let stackTrace: StackTrace.StackTrace.StackTrace|null = null;
const preview = new Components.JSPresentationUtils.StackTracePreviewContent();
preview.options = {tabStops: focusableLink};
if (target) {
stackTrace = await Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance()
.createStackTraceFromProtocolRuntime(initiator.stack, target);
preview.stackTrace = stackTrace;
}
return {preview, stackTrace};
}
override async performUpdate(): Promise {
const initiatorGraph = Logs.NetworkLog.NetworkLog.instance().initiatorGraphForRequest(this.request);
const targetManager = SDK.TargetManager.TargetManager.instance();
const networkManager = SDK.NetworkManager.NetworkManager.forRequest(this.request);
const target = networkManager?.target() ?? targetManager.primaryPageTarget() ?? targetManager.rootTarget();
const rawStack = this.request.initiator()?.stack;
let stackTrace: StackTrace.StackTrace.StackTrace|null = null;
if (rawStack && target) {
stackTrace = await Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance()
.createStackTraceFromProtocolRuntime(rawStack, target);
}
const viewInput: ViewInput = {
initiatorGraph,
stackTrace,
request: this.request,
};
this.#view(viewInput, undefined, this.contentElement);
}
override wasShown(): void {
super.wasShown();
this.registerRequiredCSS(requestInitiatorViewStyles);
this.requestUpdate();
}
}