chrome-devtools-frontend
Version:
Chrome DevTools UI
468 lines (416 loc) • 16.9 kB
text/typescript
// Copyright 2022 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/components/report_view/report_view.js';
import '../../../../ui/components/request_link_icon/request_link_icon.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 Logs from '../../../../models/logs/logs.js';
import * as Buttons from '../../../../ui/components/buttons/buttons.js';
import * as UI from '../../../../ui/legacy/legacy.js';
import * as Lit from '../../../../ui/lit/lit.js';
import * as VisualLogging from '../../../../ui/visual_logging/visual_logging.js';
import * as PreloadingHelper from '../helper/helper.js';
import preloadingDetailsReportViewStyles from './preloadingDetailsReportView.css.js';
import * as PreloadingString from './PreloadingString.js';
import {prefetchFailureReason, prerenderFailureReason, ruleSetLocationShort} from './PreloadingString.js';
const {html} = Lit;
const UIStrings = {
/**
* @description Text in PreloadingDetailsReportView of the Application panel if no element is selected. An element here is an item in a
* table of target URLs and additional prefetching states. https://developer.chrome.com/docs/devtools/application/debugging-speculation-rules
*/
noElementSelected: 'No element selected',
/**
* @description Text in PreloadingDetailsReportView of the Application panel to prompt user to select an element in a table. An element here is an item in a
* table of target URLs and additional prefetching states. https://developer.chrome.com/docs/devtools/application/debugging-speculation-rules
*/
selectAnElementForMoreDetails: 'Select an element for more details',
/**
* @description Text in details
*/
detailsDetailedInformation: 'Detailed information',
/**
* @description Text in details
*/
detailsAction: 'Action',
/**
* @description Text in details
*/
detailsStatus: 'Status',
/**
* @description Text in details
*/
detailsTargetHint: 'Target hint',
/**
* @description Text in details
*/
detailsFormSubmission: 'Form submission',
/**
* @description Text in details
*/
detailsFailureReason: 'Failure reason',
/**
* @description Header of rule set
*/
detailsRuleSet: 'Rule set',
/**
* @description Text indicating that the preloading field is true.
*/
yes: 'Yes',
/**
* @description Text indicating that the preloading field is false.
*/
no: 'No',
/**
* @description Description: status
*/
automaticallyFellBackToPrefetch: '(automatically fell back to prefetch)',
/**
* @description Description: status
*/
detailedStatusNotTriggered: 'Speculative load attempt is not yet triggered.',
/**
* @description Description: status
*/
detailedStatusPending: 'Speculative load attempt is eligible but pending.',
/**
* @description Description: status
*/
detailedStatusRunning: 'Speculative load is running.',
/**
* @description Description: status
*/
detailedStatusReady: 'Speculative load finished and the result is ready for the next navigation.',
/**
* @description Description: status
*/
detailedStatusSuccess: 'Speculative load finished and used for a navigation.',
/**
* @description Description: status
*/
detailedStatusFailure: 'Speculative load failed.',
/**
* @description Description: status
*/
detailedStatusFallbackToPrefetch: 'Speculative load failed, but fallback to prefetch succeeded.',
/**
* @description button: Contents of button to inspect prerendered page
*/
buttonInspect: 'Inspect',
/**
* @description button: Title of button to inspect prerendered page
*/
buttonClickToInspect: 'Click to inspect prerendered page',
/**
* @description button: Title of button to reveal rule set
*/
buttonClickToRevealRuleSet: 'Click to reveal rule set',
} as const;
const str_ =
i18n.i18n.registerUIStrings('panels/application/preloading/components/PreloadingDetailsReportView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
class PreloadingUIUtils {
static detailedStatus({status}: SDK.PreloadingModel.PreloadingAttempt): string {
// See content/public/browser/preloading.h PreloadingAttemptOutcome.
switch (status) {
case SDK.PreloadingModel.PreloadingStatus.NOT_TRIGGERED:
return i18nString(UIStrings.detailedStatusNotTriggered);
case SDK.PreloadingModel.PreloadingStatus.PENDING:
return i18nString(UIStrings.detailedStatusPending);
case SDK.PreloadingModel.PreloadingStatus.RUNNING:
return i18nString(UIStrings.detailedStatusRunning);
case SDK.PreloadingModel.PreloadingStatus.READY:
return i18nString(UIStrings.detailedStatusReady);
case SDK.PreloadingModel.PreloadingStatus.SUCCESS:
return i18nString(UIStrings.detailedStatusSuccess);
case SDK.PreloadingModel.PreloadingStatus.FAILURE:
return i18nString(UIStrings.detailedStatusFailure);
// NotSupported is used to handle unreachable case. For example,
// there is no code path for
// PreloadingTriggeringOutcome::kTriggeredButPending in prefetch,
// which is mapped to NotSupported. So, we regard it as an
// internal error.
case SDK.PreloadingModel.PreloadingStatus.NOT_SUPPORTED:
return i18n.i18n.lockedString('Internal error');
}
}
static detailedTargetHint(key: Protocol.Preload.PreloadingAttemptKey): string {
assertNotNullOrUndefined(key.targetHint);
switch (key.targetHint) {
case Protocol.Preload.SpeculationTargetHint.Blank:
return '_blank';
case Protocol.Preload.SpeculationTargetHint.Self:
return '_self';
}
}
}
export type PreloadingDetailsReportViewData = PreloadingDetailsReportViewDataInternal|null;
interface PreloadingDetailsReportViewDataInternal {
pipeline: SDK.PreloadingModel.PreloadPipeline;
ruleSets: Protocol.Preload.RuleSet[];
pageURL: Platform.DevToolsPath.UrlString;
requestResolver?: Logs.RequestResolver.RequestResolver;
}
export interface ViewInput {
data: PreloadingDetailsReportViewData;
}
const DEFAULT_VIEW = (input: ViewInput, _output: undefined, target: HTMLElement): void => {
if (input.data === null) {
// clang-format off
Lit.render(html`
<style>${preloadingDetailsReportViewStyles}</style>
<style>${UI.inspectorCommonStyles}</style>
<div class="empty-state">
<span class="empty-state-header">${i18nString(UIStrings.noElementSelected)}</span>
<span class="empty-state-description">${i18nString(UIStrings.selectAnElementForMoreDetails)}</span>
</div>
`, target);
// clang-format on
return;
}
const pipeline = input.data.pipeline;
const pageURL = input.data.pageURL;
const isFallbackToPrefetch = pipeline.getPrerender()?.status === SDK.PreloadingModel.PreloadingStatus.FAILURE &&
(pipeline.getPrefetch()?.status === SDK.PreloadingModel.PreloadingStatus.READY ||
pipeline.getPrefetch()?.status === SDK.PreloadingModel.PreloadingStatus.SUCCESS);
const isPrerenderLike = (speculationAction: Protocol.Preload.SpeculationAction): boolean => {
return [
Protocol.Preload.SpeculationAction.Prerender,
Protocol.Preload.SpeculationAction.PrerenderUntilScript,
].includes(speculationAction);
};
const url = (): Lit.LitTemplate => {
assertNotNullOrUndefined(input.data);
const attempt = input.data.pipeline.getOriginallyTriggered();
const prefetchStatus = input.data.pipeline.getPrefetch()?.status;
let value;
if (attempt.action === Protocol.Preload.SpeculationAction.Prefetch && attempt.requestId !== undefined &&
prefetchStatus !== SDK.PreloadingModel.PreloadingStatus.NOT_TRIGGERED) {
const {requestId, key: {url}} = attempt;
const affectedRequest: {requestId?: Protocol.Network.RequestId, url?: string} = {requestId, url};
// clang-format off
value = html`
<devtools-request-link-icon
.data=${
{
affectedRequest,
requestResolver: input.data.requestResolver || new Logs.RequestResolver.RequestResolver(Logs.NetworkLog.NetworkLog.instance()),
displayURL: true,
urlToDisplay: url,
}
}
>
</devtools-request-link-icon>
`;
// clang-format on
} else {
value = html`
<div class="text-ellipsis" title=${attempt.key.url}>${attempt.key.url}</div>
`;
}
return html`
<devtools-report-key>${i18n.i18n.lockedString('URL')}</devtools-report-key>
<devtools-report-value>
${value}
</devtools-report-value>
`;
};
const action = (isFallbackToPrefetch: boolean): Lit.LitTemplate => {
assertNotNullOrUndefined(input.data);
const attempt = input.data.pipeline.getOriginallyTriggered();
const action = PreloadingString.capitalizedAction(attempt.action);
let maybeFallback: Lit.LitTemplate = Lit.nothing;
if (isFallbackToPrefetch) {
maybeFallback = html`${i18nString(UIStrings.automaticallyFellBackToPrefetch)}`;
}
let maybeInspectButton: Lit.LitTemplate = Lit.nothing;
(() => {
if (!isPrerenderLike(attempt.action)) {
return;
}
const target = SDK.TargetManager.TargetManager.instance().primaryPageTarget();
if (target === null) {
return;
}
const prerenderTarget = SDK.TargetManager.TargetManager.instance().targets().find(
child => child.targetInfo()?.subtype === 'prerender' && child.inspectedURL() === attempt.key.url);
const disabled = (prerenderTarget === undefined);
const inspect = (): void => {
if (prerenderTarget === undefined) {
return;
}
UI.Context.Context.instance().setFlavor(SDK.Target.Target, prerenderTarget);
};
maybeInspectButton = html`
<devtools-button
@click=${inspect}
.title=${i18nString(UIStrings.buttonClickToInspect)}
.size=${Buttons.Button.Size.SMALL}
.variant=${Buttons.Button.Variant.OUTLINED}
.disabled=${disabled}
jslog=${VisualLogging.action('inspect-prerendered-page').track({click: true})}
>
${i18nString(UIStrings.buttonInspect)}
</devtools-button>
`;
})();
return html`
<devtools-report-key>${i18nString(UIStrings.detailsAction)}</devtools-report-key>
<devtools-report-value>
<div class="text-ellipsis" title="">
${action} ${maybeFallback} ${maybeInspectButton}
</div>
</devtools-report-value>
`;
};
const status = (isFallbackToPrefetch: boolean): Lit.LitTemplate => {
assertNotNullOrUndefined(input.data);
const attempt = input.data.pipeline.getOriginallyTriggered();
const detailedStatus = isFallbackToPrefetch ? i18nString(UIStrings.detailedStatusFallbackToPrefetch) :
PreloadingUIUtils.detailedStatus(attempt);
return html`
<devtools-report-key>${i18nString(UIStrings.detailsStatus)}</devtools-report-key>
<devtools-report-value>
${detailedStatus}
</devtools-report-value>
`;
};
const maybePrefetchFailureReason = (): Lit.LitTemplate => {
assertNotNullOrUndefined(input.data);
const attempt = input.data.pipeline.getOriginallyTriggered();
if (attempt.action !== Protocol.Preload.SpeculationAction.Prefetch) {
return Lit.nothing;
}
// Lookup status code for Non2XX failures.
const statusCode = PreloadingHelper.PreloadingForward.preloadStatusCode(attempt);
const failureDescription = prefetchFailureReason(attempt, statusCode);
if (failureDescription === null) {
return Lit.nothing;
}
return html`
<devtools-report-key>${i18nString(UIStrings.detailsFailureReason)}</devtools-report-key>
<devtools-report-value>
${failureDescription}
</devtools-report-value>
`;
};
const targetHint = (): Lit.LitTemplate => {
assertNotNullOrUndefined(input.data);
const attempt = input.data.pipeline.getOriginallyTriggered();
const hasTargetHint = isPrerenderLike(attempt.action) && attempt.key.targetHint !== undefined;
if (!hasTargetHint) {
return Lit.nothing;
}
return html`
<devtools-report-key>${i18nString(UIStrings.detailsTargetHint)}</devtools-report-key>
<devtools-report-value>
${PreloadingUIUtils.detailedTargetHint(attempt.key)}
</devtools-report-value>
`;
};
const formSubmission = (): Lit.LitTemplate => {
assertNotNullOrUndefined(input.data);
const attempt = input.data.pipeline.getOriginallyTriggered();
const hasFormSubmission = attempt.key.formSubmission !== undefined;
if (!hasFormSubmission || !isPrerenderLike(attempt.action)) {
return Lit.nothing;
}
return html`
<devtools-report-key>${i18nString(UIStrings.detailsFormSubmission)}</devtools-report-key>
<devtools-report-value>
${attempt.key.formSubmission ? i18nString(UIStrings.yes) : i18nString(UIStrings.no)}
</devtools-report-value>
`;
};
const maybePrerenderFailureReason = (): Lit.LitTemplate => {
assertNotNullOrUndefined(input.data);
const attempt = input.data.pipeline.getOriginallyTriggered();
if (!isPrerenderLike(attempt.action)) {
return Lit.nothing;
}
// Lookup status code from the network log for NavigationBadHttpStatus.
const statusCode = PreloadingHelper.PreloadingForward.preloadStatusCode(attempt);
const failureReason = prerenderFailureReason(
attempt as SDK.PreloadingModel.PrerenderAttempt | SDK.PreloadingModel.PrerenderUntilScriptAttempt, statusCode);
if (failureReason === null) {
return Lit.nothing;
}
return html`
<devtools-report-key>${i18nString(UIStrings.detailsFailureReason)}</devtools-report-key>
<devtools-report-value>
${failureReason}
</devtools-report-value>
`;
};
const renderRuleSet = (ruleSet: Protocol.Preload.RuleSet,
pageURL: Platform.DevToolsPath.UrlString): Lit.LitTemplate => {
const revealRuleSetView = (): void => {
void Common.Revealer.reveal(new PreloadingHelper.PreloadingForward.RuleSetView(ruleSet.id));
};
const location = ruleSetLocationShort(ruleSet, pageURL);
return html`
<devtools-report-key>${i18nString(UIStrings.detailsRuleSet)}</devtools-report-key>
<devtools-report-value>
<div class="text-ellipsis" title="">
<button class="link" role="link"
@click=${revealRuleSetView}
title=${i18nString(UIStrings.buttonClickToRevealRuleSet)}
style=${Lit.Directives.styleMap({
color: 'var(--sys-color-primary)',
'text-decoration': 'underline',
})}
jslog=${VisualLogging.action('reveal-rule-set').track({click: true})}
>
${location}
</button>
</div>
</devtools-report-value>
`;
};
// clang-format off
Lit.render(html`
<style>${preloadingDetailsReportViewStyles}</style>
<style>${UI.inspectorCommonStyles}</style>
<devtools-report
.data=${{reportTitle: 'Speculative Loading Attempt'}}
jslog=${VisualLogging.section('preloading-details')}>
<devtools-report-section-header>${i18nString(UIStrings.detailsDetailedInformation)}</devtools-report-section-header>
${url()}
${action(isFallbackToPrefetch)}
${status(isFallbackToPrefetch)}
${targetHint()}
${formSubmission()}
${maybePrefetchFailureReason()}
${maybePrerenderFailureReason()}
${input.data.ruleSets.map(ruleSet => renderRuleSet(ruleSet, pageURL))}
</devtools-report>
`, target);
// clang-format on
};
export class PreloadingDetailsReportView extends UI.Widget.VBox {
#data: PreloadingDetailsReportViewData = null;
#view: typeof DEFAULT_VIEW;
constructor(element?: HTMLElement, view = DEFAULT_VIEW) {
super(element);
this.#view = view;
}
set data(data: PreloadingDetailsReportViewData) {
this.#data = data;
this.requestUpdate();
}
override wasShown(): void {
super.wasShown();
this.requestUpdate();
}
override performUpdate(): void {
const viewInput = {
data: this.#data,
};
this.#view(viewInput, undefined, this.contentElement);
}
}