chrome-devtools-frontend
Version:
Chrome DevTools UI
310 lines (292 loc) • 13.8 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/components/data_grid/data_grid.js';
import * as Common from '../../core/common/common.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as UI from '../../ui/legacy/legacy.js';
import {Directives, html, nothing, render} from '../../ui/lit/lit.js';
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
import * as NetworkForward from '../network/forward/forward.js';
import backendLinkingSettingsTabStyles from './backendLinkingSettingsTab.css.js';
const {ifDefined} = Directives;
const UIStrings = {
/**
* @description Title for the backend linking settings tab card in DevTools Settings.
*/
rulesSectionHeading: 'Backend linking rules',
/**
* @description Title for the backend linking settings tab card in DevTools Settings.
*/
placeholderDocHeading: 'Available placeholders',
/**
* @description Description explaining what backend linking rules do.
*/
description:
'Configure links to external backend debugging, APM, or tracing tools for matching network requests. Use placeholders to insert correlation IDs extracted from request and response headers.',
/**
* @description Header column title in the backend linking rules table for the URL pattern.
*/
urlPatternColumn: 'URL pattern',
/**
* @description Header column title in the backend linking rules table for the target URL template.
*/
targetUrlTemplateColumn: 'Target URL template',
/**
* @description Header column title in the backend linking rules table for the label.
*/
labelColumn: 'Label',
/**
* @description Header column title in the placeholder reference table.
*/
placeholderColumn: 'Placeholder',
/**
* @description Header source column title in the placeholder reference table.
*/
headerColumn: 'Referenced header / property',
/**
* @description Description of the devtoolsDebugId placeholder. The placeholder values will always be `desc`, `devtools-debug-id`, and `Server-Timing`.
*/
devtoolsDebugIdDescription: 'Value of the `desc` parameter in the `devtools-debug-id` `Server-Timing` entry',
/**
* @description Description of the requestId placeholder. The placeholder value will always be `X-Request-ID`.
*/
requestIdDescription: 'Value of the `X-Request-ID` response header',
/**
* @description Description of the correlationId placeholder. The placeholder values will always be `X-Correlation-ID` and `Correlation-ID`.
*/
correlationIdDescription: 'Value of the `X-Correlation-ID` or `Correlation-ID` response header',
/**
* @description Description of the traceId placeholder. The placeholder values will always be `trace-id`, `Server-Timing`, and `traceparent`.
*/
traceIdDescription: '16-byte hex trace ID from the `trace-id` response header or `Server-Timing` `traceparent` entry',
/**
* @description Description of the spanId placeholder. The placeholder values will always be `Server-Timing` and `traceparent`.
*/
spanIdDescription: '8-byte hex parent span ID from the `Server-Timing` `traceparent` entry',
/**
* @description Error message in the backend linking settings tab when the URL pattern is empty.
*/
urlPatternCannotBeEmpty: 'URL pattern can’t be empty',
/**
* @description Error message in the backend linking settings tab when the URL pattern is invalid.
*/
invalidUrlPattern: 'URL pattern must be a valid URL pattern',
/**
* @description Error message in the backend linking settings tab when the target URL template is empty.
*/
targetUrlTemplateCannotBeEmpty: 'Target URL template can’t be empty',
/**
* @description Error message in the backend linking settings tab when the target URL template does not contain a
* placeholder.
*/
templateRequiresPlaceholder: 'Target URL template must contain at least one placeholder',
/**
* @description Error message in the backend linking settings tab when the label is empty.
*/
labelCannotBeEmpty: 'Label can’t be empty',
/**
* @description Warning message displayed below the backend linking rules table when one or more rules are invalid.
*/
invalidRulesWarning: 'Invalid rules will not be applied or saved',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/settings/BackendLinkingSettingsTab.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
function validPattern(urlPattern: string): boolean {
try {
new URLPattern(urlPattern);
return true;
} catch {
return false;
}
}
function hasPlaceholder(targetUrlTemplate: string): boolean {
return NetworkForward.BackendLinking.BACKEND_LINKING_PLACEHOLDERS.some(placeholder =>
targetUrlTemplate.includes(placeholder));
}
function isRuleValid(rule: NetworkForward.BackendLinking.BackendLinkingRule): boolean {
return Boolean(rule.urlPattern) && validPattern(rule.urlPattern) && Boolean(rule.targetUrlTemplate) &&
hasPlaceholder(rule.targetUrlTemplate) && Boolean(rule.label);
}
export interface ViewInput {
rules: NetworkForward.BackendLinking.BackendLinkingRule[];
onAddRule: (rule: NetworkForward.BackendLinking.BackendLinkingRule) => void;
onUpdateRule: (oldRule: NetworkForward.BackendLinking.BackendLinkingRule,
newRule: NetworkForward.BackendLinking.BackendLinkingRule) => void;
onDeleteRule: (rule: NetworkForward.BackendLinking.BackendLinkingRule) => void;
}
export type View = (input: ViewInput, output: object, target: HTMLElement) => void;
export const DEFAULT_VIEW: View = (input, output, target) => {
const onCreate = (event: CustomEvent<{urlPattern?: string, targetUrlTemplate?: string, label?: string}>): void => {
const data = event.detail;
const urlPattern = data.urlPattern?.trim();
const targetUrlTemplate = data.targetUrlTemplate?.trim();
const label = data.label?.trim();
if (!urlPattern && !targetUrlTemplate && !label) {
return;
}
input.onAddRule({
urlPattern: urlPattern ?? '',
targetUrlTemplate: targetUrlTemplate ?? '',
label: label ?? '',
});
};
const onEdit = (rule: NetworkForward.BackendLinking.BackendLinkingRule,
event: CustomEvent<{columnId: string, valueBeforeEditing: string, newText: string}>): void => {
const {columnId, newText} = event.detail;
const newRule = {...rule};
if (columnId === 'urlPattern') {
newRule.urlPattern = newText.trim();
} else if (columnId === 'targetUrlTemplate') {
newRule.targetUrlTemplate = newText.trim();
} else if (columnId === 'label') {
newRule.label = newText.trim();
}
input.onUpdateRule(rule, newRule);
};
// clang-format off
render(html`
<style>${backendLinkingSettingsTabStyles}</style>
<style>${UI.inspectorCommonStyles}</style>
<div class="settings-card-container-wrapper" jslog=${VisualLogging.pane('backend-linking')}>
<div class="settings-card-container">
<devtools-card heading=${i18nString(UIStrings.rulesSectionHeading)}>
<p class="description-text">${i18nString(UIStrings.description)}</p>
<devtools-data-grid
name=${i18nString(UIStrings.rulesSectionHeading)}
striped
inline
deletable
@delete=${() => {}}
@create=${onCreate}>
<table>
<style>${backendLinkingSettingsTabStyles}</style>
<thead>
<tr>
<th id="urlPattern" editable>${i18nString(UIStrings.urlPatternColumn)}</th>
<th id="targetUrlTemplate" editable>${i18nString(UIStrings.targetUrlTemplateColumn)}</th>
<th id="label" editable>${i18nString(UIStrings.labelColumn)}</th>
</tr>
</thead>
<tbody>
${input.rules.map(rule => {
return html`
<tr @edit=${
(event: CustomEvent<{columnId: string, valueBeforeEditing: string, newText: string}>) =>
onEdit(rule, event)}
@delete=${() => input.onDeleteRule(rule)}>
<td title=${ifDefined(
!rule.urlPattern ? i18nString(UIStrings.urlPatternCannotBeEmpty) :
!validPattern(rule.urlPattern) ? i18nString(UIStrings.invalidUrlPattern) :
undefined)}>
<devtools-icon name="warning-filled" class="small warning-icon"
?hidden=${Boolean(rule.urlPattern) && validPattern(rule.urlPattern)}></devtools-icon>
${rule.urlPattern}
</td>
<td title=${ifDefined(
!rule.targetUrlTemplate ? i18nString(UIStrings.targetUrlTemplateCannotBeEmpty) :
!hasPlaceholder(rule.targetUrlTemplate) ? i18nString(UIStrings.templateRequiresPlaceholder) :
undefined)}>
<devtools-icon name="warning-filled" class="small warning-icon"
?hidden=${Boolean(rule.targetUrlTemplate) && hasPlaceholder(rule.targetUrlTemplate)}></devtools-icon>
${rule.targetUrlTemplate}
</td>
<td title=${ifDefined(!rule.label ? i18nString(UIStrings.labelCannotBeEmpty) : undefined)}>
<devtools-icon name="warning-filled" class="small warning-icon"
?hidden=${Boolean(rule.label)}></devtools-icon>
${rule.label}
</td>
</tr>
`;
})}
<tr placeholder></tr>
</tbody>
</table>
</devtools-data-grid>
${input.rules.some(rule => !isRuleValid(rule)) ? html`
<div class="warning-footer">
<devtools-icon name="warning-filled" class="small"></devtools-icon>
<span>${i18nString(UIStrings.invalidRulesWarning)}</span>
</div>
` : nothing}
</devtools-card>
<devtools-card heading=${i18nString(UIStrings.placeholderDocHeading)}>
<dl class="intro-section">
<div class="column-header">${i18nString(UIStrings.placeholderColumn)}</div>
<div class="column-header">${i18nString(UIStrings.headerColumn)}</div>
<dt class="placeholder">${'${devtoolsDebugId}'}</dt>
<dd class="source-header">Server-Timing: devtools-debug-id</dd>
<dd class="placeholder-description">${i18nString(UIStrings.devtoolsDebugIdDescription)}</dd>
<dt class="placeholder">${'${requestId}'}</dt>
<dd class="source-header">X-Request-ID</dd>
<dd class="placeholder-description">${i18nString(UIStrings.requestIdDescription)}</dd>
<dt class="placeholder">${'${correlationId}'}</dt>
<dd class="source-header">X-Correlation-ID / Correlation-ID</dd>
<dd class="placeholder-description">${i18nString(UIStrings.correlationIdDescription)}</dd>
<dt class="placeholder">${'${traceId}'}</dt>
<dd class="source-header">trace-id / Server-Timing: traceparent</dd>
<dd class="placeholder-description">${i18nString(UIStrings.traceIdDescription)}</dd>
<dt class="placeholder">${'${spanId}'}</dt>
<dd class="source-header">Server-Timing: traceparent</dd>
<dd class="placeholder-description">${i18nString(UIStrings.spanIdDescription)}</dd>
</dl>
</devtools-card>
</div>
</div>`,
// clang-format on
target);
};
export class BackendLinkingSettingsTab extends UI.Widget.VBox {
readonly #view: View;
readonly #rulesSetting: Common.Settings.Setting<NetworkForward.BackendLinking.BackendLinkingRule[]>;
constructor(target?: HTMLElement, view: View = DEFAULT_VIEW) {
super(target);
this.#view = view;
const res = Common.Settings.Settings.instance().maybeResolve(
NetworkForward.BackendLinking.backendLinkingRulesSettingDescriptor);
if (!('setting' in res)) {
throw new Error('Backend linking setting is not available');
}
this.#rulesSetting = res.setting;
}
override wasShown(): void {
super.wasShown();
this.#rulesSetting.addChangeListener(this.requestUpdate, this);
this.requestUpdate();
}
override willHide(): void {
super.willHide();
this.#rulesSetting.removeChangeListener(this.requestUpdate, this);
}
override performUpdate(): void {
const input: ViewInput = {
rules: this.#rulesSetting.get(),
onAddRule: rule => {
if (!isRuleValid(rule)) {
return;
}
this.#rulesSetting.set([...this.#rulesSetting.get(), rule]);
},
onUpdateRule: (oldRule, newRule) => {
if (!isRuleValid(newRule)) {
return;
}
const rules = [...this.#rulesSetting.get()];
const index = rules.indexOf(oldRule);
if (index !== -1) {
rules[index] = newRule;
this.#rulesSetting.set(rules);
}
},
onDeleteRule: rule => {
const rules = [...this.#rulesSetting.get()];
const index = rules.indexOf(rule);
if (index !== -1) {
rules.splice(index, 1);
this.#rulesSetting.set(rules);
}
},
};
this.#view(input, {}, this.contentElement);
}
}