chrome-devtools-frontend
Version:
Chrome DevTools UI
241 lines (215 loc) • 10.1 kB
text/typescript
// Copyright 2023 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 '../../../../ui/kit/kit.js';
import * as Common from '../../../../core/common/common.js';
import * as i18n from '../../../../core/i18n/i18n.js';
import type * as Platform from '../../../../core/platform/platform.js';
import {assertNotNullOrUndefined} from '../../../../core/platform/platform.js';
import * as SDK from '../../../../core/sdk/sdk.js';
import * as Protocol from '../../../../generated/protocol.js';
import * as UI from '../../../../ui/legacy/legacy.js';
import {Directives, html, type LitTemplate, 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 * as PreloadingHelper from '../helper/helper.js';
import * as PreloadingString from './PreloadingString.js';
import ruleSetGridStyles from './ruleSetGrid.css.js';
const {styleMap} = Directives;
const UIStrings = {
/**
* @description Column header: Short URL of rule set.
*/
ruleSet: 'Rule set',
/**
* @description Column header: Show how many preloads are associated if valid, error counts if invalid.
*/
status: 'Status',
/**
* @description button: Title of button to reveal the corresponding request of rule set in Elements panel
*/
clickToOpenInElementsPanel: 'Click to open in Elements panel',
/**
* @description button: Title of button to reveal the corresponding request of rule set in Network panel
*/
clickToOpenInNetworkPanel: 'Click to open in Network panel',
/**
* @description Value of status, specifying rule set contains how many errors.
*/
errors: '{errorCount, plural, =1 {# error} other {# errors}}',
/**
* @description button: Title of button to reveal preloading attempts with filter by selected rule set
*/
buttonRevealPreloadsAssociatedWithRuleSet: 'Reveal speculative loads associated with this rule set',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/application/preloading/components/RuleSetGrid.ts', UIStrings);
export const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export interface RuleSetGridData {
rows: RuleSetGridRow[];
pageURL: Platform.DevToolsPath.UrlString;
}
export interface RuleSetGridRow {
ruleSet: Protocol.Preload.RuleSet;
preloadsStatusSummary: string;
}
export type View = (input: ViewInput, output: ViewOutput, target: HTMLElement) => void;
export interface ViewInput {
data: RuleSetGridData|null;
onSelect: (ruleSetId: Protocol.Preload.RuleSetId) => void;
onRevealInElements: (ruleSet: Protocol.Preload.RuleSet) => void;
onRevealInNetwork: (ruleSet: Protocol.Preload.RuleSet) => void;
onRevealPreloadsAssociatedWithRuleSet: (ruleSet: Protocol.Preload.RuleSet) => void;
}
export type ViewOutput = unknown;
export const DEFAULT_VIEW: View = (input, _output, target) => {
let template: LitTemplate = nothing;
if (input.data !== null) {
const {rows, pageURL} = input.data;
// Disabled until https://crbug.com/1079231 is fixed.
// clang-format off
template =
html`
<style>${ruleSetGridStyles}</style>
<div class="ruleset-container" jslog=${VisualLogging.pane('preloading-rules')}>
<devtools-data-grid striped>
<table>
<tr>
<th id="rule-set" weight="20" sortable>
${i18nString(UIStrings.ruleSet)}
</th>
<th id="status" weight="80" sortable>
${i18nString(UIStrings.status)}
</th>
</tr>
${rows.map(({ruleSet, preloadsStatusSummary}) => {
const location = PreloadingString.ruleSetTagOrLocationShort(ruleSet, pageURL);
const revealInElements = ruleSet.backendNodeId !== undefined;
const revealInNetwork = ruleSet.url !== undefined && ruleSet.requestId;
return html`
<tr @select=${() => input.onSelect(ruleSet.id)}>
<td>
${revealInElements || revealInNetwork ? html`
<button class="link" role="link"
@click=${() => {
if (revealInElements) {
input.onRevealInElements(ruleSet);
} else {
input.onRevealInNetwork(ruleSet);
}
}}
title=${revealInElements ? i18nString(UIStrings.clickToOpenInElementsPanel)
: i18nString(UIStrings.clickToOpenInNetworkPanel)}
style=${styleMap({
border: 'none',
background: 'none',
color: 'var(--icon-link)',
cursor: 'pointer',
'text-decoration': 'underline',
'padding-inline-start': '0',
'padding-inline-end': '0',
})}
jslog=${VisualLogging
.action(revealInElements ? 'reveal-in-elements' : 'reveal-in-network')
.track({click: true})}
>
<devtools-icon name=${revealInElements ? 'code-circle' : 'arrow-up-down-circle'} class="medium"
style=${styleMap({
color: 'var(--icon-link)',
'vertical-align': 'sub',
})}
></devtools-icon>
${location}
</button>`
: location}
</td>
<td>
${ruleSet.errorType !== undefined ? html`
<span style=${styleMap({color: 'var(--sys-color-error)'})}>
${i18nString(UIStrings.errors, {errorCount: 1})}
</span>` : ''} ${ruleSet.errorType !== Protocol.Preload.RuleSetErrorType.SourceIsNotJsonObject &&
ruleSet.errorType !== Protocol.Preload.RuleSetErrorType.InvalidRulesetLevelTag ?
html`
<button class="link" role="link"
@click=${() => input.onRevealPreloadsAssociatedWithRuleSet(ruleSet)}
title=${i18nString(UIStrings.buttonRevealPreloadsAssociatedWithRuleSet)}
style=${styleMap({
color: 'var(--sys-color-primary)',
'text-decoration': 'underline',
cursor: 'pointer',
border: 'none',
background: 'none',
'padding-inline-start': '0',
'padding-inline-end': '0',
})}
jslog=${VisualLogging.action('reveal-preloads').track({click: true})}>
${preloadsStatusSummary}
</button>` : ''}
</td>
</tr>
`;})}
</table>
</devtools-data-grid>
</div>`;
// clang-format on
}
render(template, target);
};
/** Grid component to show SpeculationRules rule sets. **/
export class RuleSetGrid extends Common.ObjectWrapper.eventMixin<EventTypes, typeof UI.Widget.VBox>(UI.Widget.VBox) {
readonly #view: View;
#data: RuleSetGridData|null = null;
constructor(view: View = DEFAULT_VIEW) {
super({useShadowDom: true});
this.#view = view;
}
get data(): RuleSetGridData|null {
return this.#data;
}
set data(data: RuleSetGridData|null) {
this.#data = data;
this.requestUpdate();
}
override performUpdate(): void {
const input: ViewInput = {
data: this.#data,
onSelect: this.dispatchEventToListeners.bind(this, Events.SELECT),
onRevealInElements: this.#revealSpeculationRulesInElements.bind(this),
onRevealInNetwork: this.#revealSpeculationRulesInNetwork.bind(this),
onRevealPreloadsAssociatedWithRuleSet: this.#revealAttemptViewWithFilter.bind(this),
};
const output = undefined;
this.#view(input, output, this.contentElement);
}
#revealSpeculationRulesInElements(ruleSet: Protocol.Preload.RuleSet): void {
assertNotNullOrUndefined(ruleSet.backendNodeId);
const target = SDK.TargetManager.TargetManager.instance().scopeTarget();
if (target === null) {
return;
}
void Common.Revealer.reveal(new SDK.DOMModel.DeferredDOMNode(target, ruleSet.backendNodeId));
}
#revealSpeculationRulesInNetwork(ruleSet: Protocol.Preload.RuleSet): void {
assertNotNullOrUndefined(ruleSet.requestId);
const request = SDK.TargetManager.TargetManager.instance()
.scopeTarget()
?.model(SDK.NetworkManager.NetworkManager)
?.requestForId(ruleSet.requestId) ||
null;
if (request === null) {
return;
}
const requestLocation = NetworkForward.UIRequestLocation.UIRequestLocation.tab(
request, NetworkForward.UIRequestLocation.UIRequestTabs.PREVIEW, {clearFilter: false});
void Common.Revealer.reveal(requestLocation);
}
#revealAttemptViewWithFilter(ruleSet: Protocol.Preload.RuleSet): void {
void Common.Revealer.reveal(new PreloadingHelper.PreloadingForward.AttemptViewWithFilter(ruleSet.id));
}
}
export const enum Events {
SELECT = 'select',
}
export interface EventTypes {
[Events.SELECT]: Protocol.Preload.RuleSetId;
}