mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-30 18:03:48 +08:00
Adds ellipsis for long URLs Screenshots: https://imgur.com/a/wOaXZ5u Fixed: 1402167 Change-Id: I629fcabf55b134b2cd08942ae0a34c08d13d7bc1 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/4132884 Reviewed-by: Kateryna Prokopenko <kprokopenko@chromium.org> Auto-Submit: Wolfgang Beyer <wolfi@chromium.org> Commit-Queue: Kateryna Prokopenko <kprokopenko@chromium.org> Commit-Queue: Wolfgang Beyer <wolfi@chromium.org>
651 lines
27 KiB
TypeScript
651 lines
27 KiB
TypeScript
// Copyright (c) 2021 The Chromium Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
import type * as Platform from '../../../core/platform/platform.js';
|
|
import * as i18n from '../../../core/i18n/i18n.js';
|
|
import * as Buttons from '../../../ui/components/buttons/buttons.js';
|
|
import * as SDK from '../../../core/sdk/sdk.js';
|
|
import * as LitHtml from '../../../ui/lit-html/lit-html.js';
|
|
import * as Root from '../../../core/root/root.js';
|
|
import * as ReportView from '../../../ui/components/report_view/report_view.js';
|
|
import * as UI from '../../../ui/legacy/legacy.js';
|
|
import * as Protocol from '../../../generated/protocol.js';
|
|
import * as IconButton from '../../../ui/components/icon_button/icon_button.js';
|
|
import * as ComponentHelpers from '../../../ui/components/helpers/helpers.js';
|
|
import * as Coordinator from '../../../ui/components/render_coordinator/render_coordinator.js';
|
|
import * as ChromeLink from '../../../ui/components/chrome_link/chrome_link.js';
|
|
import * as ExpandableList from '../../../ui/components/expandable_list/expandable_list.js';
|
|
import * as TreeOutline from '../../../ui/components/tree_outline/tree_outline.js';
|
|
|
|
import {NotRestoredReasonDescription} from './BackForwardCacheStrings.js';
|
|
import backForwardCacheViewStyles from './backForwardCacheView.css.js';
|
|
|
|
const UIStrings = {
|
|
/**
|
|
* @description Title text in back/forward cache view of the Application panel
|
|
*/
|
|
mainFrame: 'Main Frame',
|
|
/**
|
|
* @description Title text in back/forward cache view of the Application panel
|
|
*/
|
|
backForwardCacheTitle: 'Back/forward cache',
|
|
/**
|
|
* @description Status text for the status of the main frame
|
|
*/
|
|
unavailable: 'unavailable',
|
|
/**
|
|
* @description Entry name text in the back/forward cache view of the Application panel
|
|
*/
|
|
url: 'URL:',
|
|
/**
|
|
* @description Status text for the status of the back/forward cache status
|
|
*/
|
|
unknown: 'Unknown Status',
|
|
/**
|
|
* @description Status text for the status of the back/forward cache status indicating that
|
|
* the back/forward cache was not used and a normal navigation occured instead.
|
|
*/
|
|
normalNavigation:
|
|
'Not served from back/forward cache: to trigger back/forward cache, use Chrome\'s back/forward buttons, or use the test button below to automatically navigate away and back.',
|
|
/**
|
|
* @description Status text for the status of the back/forward cache status indicating that
|
|
* the back/forward cache was used to restore the page instead of reloading it.
|
|
*/
|
|
restoredFromBFCache: 'Successfully served from back/forward cache.',
|
|
/**
|
|
* @description Label for a list of reasons which prevent the page from being eligible for
|
|
* back/forward cache. These reasons are actionable i.e. they can be cleaned up to make the
|
|
* page eligible for back/forward cache.
|
|
*/
|
|
pageSupportNeeded: 'Actionable',
|
|
/**
|
|
* @description Explanation for actionable items which prevent the page from being eligible
|
|
* for back/forward cache.
|
|
*/
|
|
pageSupportNeededExplanation:
|
|
'These reasons are actionable i.e. they can be cleaned up to make the page eligible for back/forward cache.',
|
|
/**
|
|
* @description Label for a list of reasons which prevent the page from being eligible for
|
|
* back/forward cache. These reasons are circumstantial / not actionable i.e. they cannot be
|
|
* cleaned up by developers to make the page eligible for back/forward cache.
|
|
*/
|
|
circumstantial: 'Not Actionable',
|
|
/**
|
|
* @description Explanation for circumstantial/non-actionable items which prevent the page from being eligible
|
|
* for back/forward cache.
|
|
*/
|
|
circumstantialExplanation:
|
|
'These reasons are not actionable i.e. caching was prevented by something outside of the direct control of the page.',
|
|
/**
|
|
* @description Label for a list of reasons which prevent the page from being eligible for
|
|
* back/forward cache. These reasons are pending support by chrome i.e. in a future version
|
|
* of chrome they will not prevent back/forward cache usage anymore.
|
|
*/
|
|
supportPending: 'Pending Support',
|
|
/**
|
|
* @description Label for the button to test whether BFCache is available for the page
|
|
*/
|
|
runTest: 'Test back/forward cache',
|
|
/**
|
|
* @description Label for the disabled button while the test is running
|
|
*/
|
|
runningTest: 'Running test',
|
|
/**
|
|
* @description Link Text about explanation of back/forward cache
|
|
*/
|
|
learnMore: 'Learn more: back/forward cache eligibility',
|
|
/**
|
|
* @description Link Text about unload handler
|
|
*/
|
|
neverUseUnload: 'Learn more: Never use unload handler',
|
|
/**
|
|
* @description Explanation for 'pending support' items which prevent the page from being eligible
|
|
* for back/forward cache.
|
|
*/
|
|
supportPendingExplanation:
|
|
'Chrome support for these reasons is pending i.e. they will not prevent the page from being eligible for back/forward cache in a future version of Chrome.',
|
|
/**
|
|
* @description Text that precedes displaying a link to the extension which blocked the page from being eligible for back/forward cache.
|
|
*/
|
|
blockingExtensionId: 'Extension id: ',
|
|
/**
|
|
* @description Label for the 'Frames' section of the back/forward cache view, which shows a frame tree of the
|
|
* page with reasons why the frames can't be cached.
|
|
*/
|
|
framesTitle: 'Frames',
|
|
/**
|
|
* @description Top level summary of the total number of issues found in a single frame.
|
|
*/
|
|
issuesInSingleFrame: '{n, plural, =1 {# issue found in 1 frame.} other {# issues found in 1 frame.}}',
|
|
/**
|
|
* @description Top level summary of the total number of issues found and the number of frames they were found in.
|
|
* 'm' is never less than 2.
|
|
* @example {3} m
|
|
*/
|
|
issuesInMultipleFrames: '{n, plural, =1 {# issue found in {m} frames.} other {# issues found in {m} frames.}}',
|
|
/**
|
|
* @description Shows the number of frames with a particular issue.
|
|
*/
|
|
framesPerIssue: '{n, plural, =1 {# frame} other {# frames}}',
|
|
/**
|
|
*@description Title for a frame in the frame tree that doesn't have a URL. Placeholder indicates which number frame with a blank URL it is.
|
|
*@example {3} PH1
|
|
*/
|
|
blankURLTitle: 'Blank URL [{PH1}]',
|
|
};
|
|
|
|
const str_ = i18n.i18n.registerUIStrings('panels/application/components/BackForwardCacheView.ts', UIStrings);
|
|
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
|
|
|
|
const enum ScreenStatusType {
|
|
Running = 'Running',
|
|
Result = 'Result',
|
|
}
|
|
|
|
export class BackForwardCacheViewWrapper extends UI.ThrottledWidget.ThrottledWidget {
|
|
readonly #bfcacheView: BackForwardCacheView;
|
|
|
|
constructor(bfcacheView: BackForwardCacheView) {
|
|
super(true, 1000);
|
|
this.#bfcacheView = bfcacheView;
|
|
this.#getMainResourceTreeModel()?.addEventListener(
|
|
SDK.ResourceTreeModel.Events.MainFrameNavigated, this.update, this);
|
|
this.#getMainResourceTreeModel()?.addEventListener(
|
|
SDK.ResourceTreeModel.Events.BackForwardCacheDetailsUpdated, this.update, this);
|
|
this.contentElement.classList.add('overflow-auto');
|
|
this.contentElement.appendChild(this.#bfcacheView);
|
|
this.update();
|
|
}
|
|
|
|
async doUpdate(): Promise<void> {
|
|
this.#bfcacheView.data = {frame: this.#getMainFrame()};
|
|
}
|
|
|
|
#getMainResourceTreeModel(): SDK.ResourceTreeModel.ResourceTreeModel|null {
|
|
const mainTarget = SDK.TargetManager.TargetManager.instance().mainFrameTarget();
|
|
return mainTarget?.model(SDK.ResourceTreeModel.ResourceTreeModel) || null;
|
|
}
|
|
|
|
#getMainFrame(): SDK.ResourceTreeModel.ResourceTreeFrame|null {
|
|
return this.#getMainResourceTreeModel()?.mainFrame || null;
|
|
}
|
|
}
|
|
|
|
const coordinator = Coordinator.RenderCoordinator.RenderCoordinator.instance();
|
|
|
|
export interface BackForwardCacheViewData {
|
|
frame: SDK.ResourceTreeModel.ResourceTreeFrame|null;
|
|
}
|
|
|
|
export class BackForwardCacheView extends HTMLElement {
|
|
static readonly litTagName = LitHtml.literal`devtools-resources-back-forward-cache-view`;
|
|
readonly #shadow = this.attachShadow({mode: 'open'});
|
|
#frame: SDK.ResourceTreeModel.ResourceTreeFrame|null = null;
|
|
#screenStatus = ScreenStatusType.Result;
|
|
#nextNodeId = 0;
|
|
#historyIndex = 0;
|
|
|
|
connectedCallback(): void {
|
|
this.#shadow.adoptedStyleSheets = [backForwardCacheViewStyles];
|
|
}
|
|
|
|
set data(data: BackForwardCacheViewData) {
|
|
this.#frame = data.frame;
|
|
void this.#render();
|
|
}
|
|
|
|
async #render(): Promise<void> {
|
|
await coordinator.write('BackForwardCacheView render', () => {
|
|
// Disabled until https://crbug.com/1079231 is fixed.
|
|
// clang-format off
|
|
LitHtml.render(LitHtml.html`
|
|
<${ReportView.ReportView.Report.litTagName} .data=${
|
|
{reportTitle: i18nString(UIStrings.backForwardCacheTitle)} as ReportView.ReportView.ReportData
|
|
}>
|
|
${this.#renderMainFrameInformation()}
|
|
</${ReportView.ReportView.Report.litTagName}>
|
|
`, this.#shadow, {host: this});
|
|
// clang-format on
|
|
});
|
|
}
|
|
|
|
#renderBackForwardCacheTestResult(): void {
|
|
SDK.TargetManager.TargetManager.instance().removeModelListener(
|
|
SDK.ResourceTreeModel.ResourceTreeModel, SDK.ResourceTreeModel.Events.FrameNavigated,
|
|
this.#renderBackForwardCacheTestResult, this);
|
|
this.#screenStatus = ScreenStatusType.Result;
|
|
void this.#render();
|
|
}
|
|
|
|
async #onNavigatedAway(): Promise<void> {
|
|
SDK.TargetManager.TargetManager.instance().removeModelListener(
|
|
SDK.ResourceTreeModel.ResourceTreeModel, SDK.ResourceTreeModel.Events.FrameNavigated, this.#onNavigatedAway,
|
|
this);
|
|
await this.#waitAndGoBackInHistory(50);
|
|
}
|
|
|
|
async #waitAndGoBackInHistory(delay: number): Promise<void> {
|
|
const mainTarget = SDK.TargetManager.TargetManager.instance().mainFrameTarget();
|
|
const resourceTreeModel = mainTarget?.model(SDK.ResourceTreeModel.ResourceTreeModel);
|
|
const historyResults = await resourceTreeModel?.navigationHistory();
|
|
if (!resourceTreeModel || !historyResults) {
|
|
return;
|
|
}
|
|
// The navigation history can be delayed. If this is the case we wait and
|
|
// check again later. Otherwise it would be possible to press the 'Test
|
|
// BFCache' button again too soon, leading to the browser stepping back in
|
|
// history without returning to the correct page.
|
|
if (historyResults.currentIndex === this.#historyIndex) {
|
|
window.setTimeout(this.#waitAndGoBackInHistory.bind(this, delay * 2), delay);
|
|
} else {
|
|
SDK.TargetManager.TargetManager.instance().addModelListener(
|
|
SDK.ResourceTreeModel.ResourceTreeModel, SDK.ResourceTreeModel.Events.FrameNavigated,
|
|
this.#renderBackForwardCacheTestResult, this);
|
|
resourceTreeModel.navigateToHistoryEntry(historyResults.entries[historyResults.currentIndex - 1]);
|
|
}
|
|
}
|
|
|
|
async #navigateAwayAndBack(): Promise<void> {
|
|
// Checking BFCache Compatibility
|
|
|
|
const mainTarget = SDK.TargetManager.TargetManager.instance().mainFrameTarget();
|
|
const resourceTreeModel = mainTarget?.model(SDK.ResourceTreeModel.ResourceTreeModel);
|
|
const historyResults = await resourceTreeModel?.navigationHistory();
|
|
if (!resourceTreeModel || !historyResults) {
|
|
return;
|
|
}
|
|
this.#historyIndex = historyResults.currentIndex;
|
|
this.#screenStatus = ScreenStatusType.Running;
|
|
void this.#render();
|
|
|
|
// This event listener is removed inside of onNavigatedAway().
|
|
SDK.TargetManager.TargetManager.instance().addModelListener(
|
|
SDK.ResourceTreeModel.ResourceTreeModel, SDK.ResourceTreeModel.Events.FrameNavigated, this.#onNavigatedAway,
|
|
this);
|
|
|
|
// We can know whether the current page can use BFCache
|
|
// as the browser navigates to another unrelated page and goes back to the current page.
|
|
// We chose "chrome://terms" because it must be cross-site.
|
|
// Ideally, We want to have our own testing page like "chrome: //bfcache-test".
|
|
void resourceTreeModel.navigate('chrome://terms' as Platform.DevToolsPath.UrlString);
|
|
}
|
|
|
|
#renderMainFrameInformation(): LitHtml.TemplateResult {
|
|
if (!this.#frame) {
|
|
// clang-format off
|
|
return LitHtml.html`
|
|
<${ReportView.ReportView.ReportKey.litTagName}>
|
|
${i18nString(UIStrings.mainFrame)}
|
|
</${ReportView.ReportView.ReportKey.litTagName}>
|
|
<${ReportView.ReportView.ReportValue.litTagName}>
|
|
${i18nString(UIStrings.unavailable)}
|
|
</${ReportView.ReportView.ReportValue.litTagName}>
|
|
`;
|
|
// clang-format on
|
|
}
|
|
const isTestRunning = (this.#screenStatus === ScreenStatusType.Running);
|
|
// Prevent running BFCache test on the DevTools window itself via DevTools on DevTools
|
|
const isTestingForbidden = this.#frame.url.startsWith('devtools://');
|
|
// clang-format off
|
|
return LitHtml.html`
|
|
${this.#renderBackForwardCacheStatus(this.#frame.backForwardCacheDetails.restoredFromCache)}
|
|
<div class="report-line">
|
|
<div class="report-key">
|
|
${i18nString(UIStrings.url)}
|
|
</div>
|
|
<div class="report-value" title=${this.#frame.url}>
|
|
${this.#frame.url}
|
|
</div>
|
|
</div>
|
|
${this.#maybeRenderFrameTree(this.#frame.backForwardCacheDetails.explanationsTree)}
|
|
<${ReportView.ReportView.ReportSection.litTagName}>
|
|
<${Buttons.Button.Button.litTagName}
|
|
aria-label=${i18nString(UIStrings.runTest)}
|
|
.disabled=${isTestRunning || isTestingForbidden}
|
|
.spinner=${isTestRunning}
|
|
.variant=${Buttons.Button.Variant.PRIMARY}
|
|
@click=${this.#navigateAwayAndBack}>
|
|
${isTestRunning ? LitHtml.html`
|
|
${i18nString(UIStrings.runningTest)}`:`
|
|
${i18nString(UIStrings.runTest)}
|
|
`}
|
|
</${Buttons.Button.Button.litTagName}>
|
|
</${ReportView.ReportView.ReportSection.litTagName}>
|
|
<${ReportView.ReportView.ReportSectionDivider.litTagName}>
|
|
</${ReportView.ReportView.ReportSectionDivider.litTagName}>
|
|
${this.#maybeRenderExplanations(this.#frame.backForwardCacheDetails.explanations,
|
|
this.#frame.backForwardCacheDetails.explanationsTree)}
|
|
<${ReportView.ReportView.ReportSection.litTagName}>
|
|
<x-link href="https://web.dev/bfcache/" class="link">
|
|
${i18nString(UIStrings.learnMore)}
|
|
</x-link>
|
|
</${ReportView.ReportView.ReportSection.litTagName}>
|
|
`;
|
|
// clang-format on
|
|
}
|
|
|
|
#maybeRenderFrameTree(explanationTree: Protocol.Page.BackForwardCacheNotRestoredExplanationTree|
|
|
undefined): LitHtml.LitTemplate {
|
|
if (!explanationTree || (explanationTree.explanations.length === 0 && explanationTree.children.length === 0) ||
|
|
!Root.Runtime.experiments.isEnabled('bfcacheDisplayTree')) {
|
|
return LitHtml.nothing;
|
|
}
|
|
|
|
function treeNodeRenderer(node: TreeOutline.TreeOutlineUtils.TreeNode<FrameTreeNodeData>): LitHtml.TemplateResult {
|
|
// clang-format off
|
|
return LitHtml.html`
|
|
<div class="text-ellipsis">
|
|
${node.treeNodeData.iconName ? LitHtml.html`
|
|
<${IconButton.Icon.Icon.litTagName} class="inline-icon" .data=${{
|
|
iconName: node.treeNodeData.iconName,
|
|
color: 'var(--color-text-secondary)',
|
|
width: '16px',
|
|
height: '16px',
|
|
} as IconButton.Icon.IconData}>
|
|
</${IconButton.Icon.Icon.litTagName}>
|
|
` : LitHtml.nothing}
|
|
${node.treeNodeData.text}
|
|
</div>
|
|
`;
|
|
// clang-format on
|
|
}
|
|
|
|
const frameTreeData = this.#buildFrameTreeDataRecursive(explanationTree, {blankCount: 1});
|
|
// Override the icon for the top frame.
|
|
frameTreeData.node.treeNodeData.iconName = 'frame-icon';
|
|
let title = '';
|
|
// The translation pipeline does not support nested plurals. We avoid this
|
|
// here by pulling out the logic for one of the plurals into code instead.
|
|
if (frameTreeData.frameCount === 1) {
|
|
title = i18nString(UIStrings.issuesInSingleFrame, {n: frameTreeData.issueCount});
|
|
} else {
|
|
title = i18nString(UIStrings.issuesInMultipleFrames, {n: frameTreeData.issueCount, m: frameTreeData.frameCount});
|
|
}
|
|
const root: TreeOutline.TreeOutlineUtils.TreeNode<FrameTreeNodeData> = {
|
|
treeNodeData: {
|
|
text: title,
|
|
},
|
|
id: 'root',
|
|
children: () => Promise.resolve([frameTreeData.node]),
|
|
};
|
|
|
|
// clang-format off
|
|
return LitHtml.html`
|
|
<div class="report-line">
|
|
<div class="report-key">
|
|
${i18nString(UIStrings.framesTitle)}
|
|
</div>
|
|
<div class="report-value">
|
|
<${TreeOutline.TreeOutline.TreeOutline.litTagName} .data=${{
|
|
tree: [root],
|
|
defaultRenderer: treeNodeRenderer,
|
|
compact: true,
|
|
} as TreeOutline.TreeOutline.TreeOutlineData<FrameTreeNodeData>}>
|
|
</${TreeOutline.TreeOutline.TreeOutline.litTagName}>
|
|
</div>
|
|
</div>
|
|
`;
|
|
// clang-format on
|
|
}
|
|
|
|
// Builds a subtree of the frame tree, conaining only frames with BFCache issues and their ancestors.
|
|
// Returns the root node, the number of frames in the subtree, and the number of issues in the subtree.
|
|
#buildFrameTreeDataRecursive(
|
|
explanationTree: Protocol.Page.BackForwardCacheNotRestoredExplanationTree,
|
|
nextBlankURLCount: {blankCount: number}):
|
|
{node: TreeOutline.TreeOutlineUtils.TreeNode<FrameTreeNodeData>, frameCount: number, issueCount: number} {
|
|
let frameCount = 1;
|
|
let issueCount = 0;
|
|
const children: TreeOutline.TreeOutlineUtils.TreeNode<FrameTreeNodeData>[] = [];
|
|
|
|
let nodeUrlText = '';
|
|
if (explanationTree.url.length) {
|
|
nodeUrlText = explanationTree.url;
|
|
} else {
|
|
nodeUrlText = i18nString(UIStrings.blankURLTitle, {PH1: nextBlankURLCount.blankCount});
|
|
nextBlankURLCount.blankCount += 1;
|
|
}
|
|
|
|
for (const explanation of explanationTree.explanations) {
|
|
const child = {treeNodeData: {text: explanation.reason}, id: String(this.#nextNodeId++)};
|
|
issueCount += 1;
|
|
children.push(child);
|
|
}
|
|
for (const child of explanationTree.children) {
|
|
const frameTreeData = this.#buildFrameTreeDataRecursive(child, nextBlankURLCount);
|
|
if (frameTreeData.issueCount > 0) {
|
|
children.push(frameTreeData.node);
|
|
issueCount += frameTreeData.issueCount;
|
|
frameCount += frameTreeData.frameCount;
|
|
}
|
|
}
|
|
|
|
let node: TreeOutline.TreeOutlineUtils.TreeNode<FrameTreeNodeData> = {
|
|
treeNodeData: {
|
|
text: `(${issueCount}) ${nodeUrlText}`,
|
|
},
|
|
id: String(this.#nextNodeId++),
|
|
};
|
|
if (children.length) {
|
|
node = {
|
|
...node,
|
|
children: () => Promise.resolve(children),
|
|
};
|
|
node.treeNodeData.iconName = 'frame-embedded-icon';
|
|
} else if (!explanationTree.url.length) {
|
|
// If the current node increased the blank count, but it has no children and
|
|
// is therefore not shown, decrement the blank count again.
|
|
nextBlankURLCount.blankCount -= 1;
|
|
}
|
|
return {node, frameCount, issueCount};
|
|
}
|
|
|
|
#renderBackForwardCacheStatus(status: boolean|undefined): LitHtml.TemplateResult {
|
|
switch (status) {
|
|
case true:
|
|
// clang-format off
|
|
return LitHtml.html`
|
|
<${ReportView.ReportView.ReportSection.litTagName}>
|
|
<div class="status">
|
|
<${IconButton.Icon.Icon.litTagName} class="inline-icon" .data=${{
|
|
iconName: 'ic_checkmark_16x16',
|
|
color: 'green',
|
|
width: '16px',
|
|
height: '16px',
|
|
} as IconButton.Icon.IconData}>
|
|
</${IconButton.Icon.Icon.litTagName}>
|
|
</div>
|
|
${i18nString(UIStrings.restoredFromBFCache)}
|
|
</${ReportView.ReportView.ReportSection.litTagName}>
|
|
`;
|
|
// clang-format on
|
|
case false:
|
|
// clang-format off
|
|
return LitHtml.html`
|
|
<${ReportView.ReportView.ReportSection.litTagName}>
|
|
<div class="status">
|
|
<${IconButton.Icon.Icon.litTagName} class="inline-icon" .data=${{
|
|
iconName: 'circled_backslash_icon',
|
|
color: 'var(--color-text-secondary)',
|
|
width: '16px',
|
|
height: '16px',
|
|
} as IconButton.Icon.IconData}>
|
|
</${IconButton.Icon.Icon.litTagName}>
|
|
</div>
|
|
${i18nString(UIStrings.normalNavigation)}
|
|
</${ReportView.ReportView.ReportSection.litTagName}>
|
|
`;
|
|
// clang-format on
|
|
}
|
|
// clang-format off
|
|
return LitHtml.html`
|
|
<${ReportView.ReportView.ReportSection.litTagName}>
|
|
${i18nString(UIStrings.unknown)}
|
|
</${ReportView.ReportView.ReportSection.litTagName}>
|
|
`;
|
|
// clang-format on
|
|
}
|
|
|
|
#buildReasonToFramesMap(
|
|
explanationTree: Protocol.Page.BackForwardCacheNotRestoredExplanationTree,
|
|
nextBlankURLCount: {blankCount: number},
|
|
outputMap: Map<Protocol.Page.BackForwardCacheNotRestoredReason, string[]>): void {
|
|
let url = explanationTree.url;
|
|
if (url.length === 0) {
|
|
url = i18nString(UIStrings.blankURLTitle, {PH1: nextBlankURLCount.blankCount});
|
|
nextBlankURLCount.blankCount += 1;
|
|
}
|
|
explanationTree.explanations.forEach(explanation => {
|
|
let frames: string[]|undefined = outputMap.get(explanation.reason);
|
|
if (frames === undefined) {
|
|
frames = [url];
|
|
outputMap.set(explanation.reason, frames);
|
|
} else {
|
|
frames.push(url);
|
|
}
|
|
});
|
|
explanationTree.children.map(child => {
|
|
this.#buildReasonToFramesMap(child, nextBlankURLCount, outputMap);
|
|
});
|
|
}
|
|
|
|
#maybeRenderExplanations(
|
|
explanations: Protocol.Page.BackForwardCacheNotRestoredExplanation[],
|
|
explanationTree: Protocol.Page.BackForwardCacheNotRestoredExplanationTree|undefined): LitHtml.LitTemplate {
|
|
if (explanations.length === 0) {
|
|
return LitHtml.nothing;
|
|
}
|
|
|
|
const pageSupportNeeded = explanations.filter(
|
|
explanation => explanation.type === Protocol.Page.BackForwardCacheNotRestoredReasonType.PageSupportNeeded);
|
|
const supportPending = explanations.filter(
|
|
explanation => explanation.type === Protocol.Page.BackForwardCacheNotRestoredReasonType.SupportPending);
|
|
const circumstantial = explanations.filter(
|
|
explanation => explanation.type === Protocol.Page.BackForwardCacheNotRestoredReasonType.Circumstantial);
|
|
|
|
const reasonToFramesMap: Map<Protocol.Page.BackForwardCacheNotRestoredReason, string[]> = new Map();
|
|
if (explanationTree) {
|
|
this.#buildReasonToFramesMap(explanationTree, {blankCount: 1}, reasonToFramesMap);
|
|
}
|
|
// Disabled until https://crbug.com/1079231 is fixed.
|
|
// clang-format off
|
|
return LitHtml.html`
|
|
${this.#renderExplanations(i18nString(UIStrings.pageSupportNeeded), i18nString(UIStrings.pageSupportNeededExplanation), pageSupportNeeded, reasonToFramesMap)}
|
|
${this.#renderExplanations(i18nString(UIStrings.supportPending), i18nString(UIStrings.supportPendingExplanation), supportPending, reasonToFramesMap)}
|
|
${this.#renderExplanations(i18nString(UIStrings.circumstantial), i18nString(UIStrings.circumstantialExplanation), circumstantial, reasonToFramesMap)}
|
|
`;
|
|
// clang-format on
|
|
}
|
|
|
|
#renderExplanations(
|
|
category: Platform.UIString.LocalizedString, explainerText: Platform.UIString.LocalizedString,
|
|
explanations: Protocol.Page.BackForwardCacheNotRestoredExplanation[],
|
|
reasonToFramesMap: Map<Protocol.Page.BackForwardCacheNotRestoredReason, string[]>): LitHtml.TemplateResult {
|
|
// Disabled until https://crbug.com/1079231 is fixed.
|
|
// clang-format off
|
|
return LitHtml.html`
|
|
${explanations.length > 0 ? LitHtml.html`
|
|
<${ReportView.ReportView.ReportSectionHeader.litTagName}>
|
|
${category}
|
|
<div class="help-outline-icon">
|
|
<${IconButton.Icon.Icon.litTagName} class="inline-icon" .data=${{
|
|
iconName: 'help_outline',
|
|
color: 'var(--color-text-secondary)',
|
|
width: '16px',
|
|
height: '16px',
|
|
} as IconButton.Icon.IconData} title=${explainerText}>
|
|
</${IconButton.Icon.Icon.litTagName}>
|
|
</div>
|
|
</${ReportView.ReportView.ReportSectionHeader.litTagName}>
|
|
${explanations.map(explanation => this.#renderReason(explanation, reasonToFramesMap.get(explanation.reason)))}
|
|
` : LitHtml.nothing}
|
|
`;
|
|
// clang-format on
|
|
}
|
|
|
|
#maybeRenderReasonContext(explanation: Protocol.Page.BackForwardCacheNotRestoredExplanation): LitHtml.LitTemplate {
|
|
if (explanation.reason ===
|
|
Protocol.Page.BackForwardCacheNotRestoredReason.EmbedderExtensionSentMessageToCachedFrame &&
|
|
explanation.context) {
|
|
const link = 'chrome://extensions/?id=' + explanation.context;
|
|
// clang-format off
|
|
return LitHtml.html`${i18nString(UIStrings.blockingExtensionId)}
|
|
<${ChromeLink.ChromeLink.ChromeLink.litTagName} .href=${link}>${explanation.context}</${ChromeLink.ChromeLink.ChromeLink.litTagName}>`;
|
|
// clang-format on
|
|
}
|
|
return LitHtml.nothing;
|
|
}
|
|
|
|
#renderFramesPerReason(frames: string[]|undefined): LitHtml.LitTemplate {
|
|
if (frames === undefined || frames.length === 0 || !Root.Runtime.experiments.isEnabled('bfcacheDisplayTree')) {
|
|
return LitHtml.nothing;
|
|
}
|
|
const rows = [LitHtml.html`<div>${i18nString(UIStrings.framesPerIssue, {n: frames.length})}</div>`];
|
|
rows.push(...frames.map(url => LitHtml.html`<div class="text-ellipsis" title=${url}>${url}</div>`));
|
|
return LitHtml.html`
|
|
<div class="explanation-frames">
|
|
<${ExpandableList.ExpandableList.ExpandableList.litTagName} .data=${
|
|
{rows} as
|
|
ExpandableList.ExpandableList.ExpandableListData}></${ExpandableList.ExpandableList.ExpandableList.litTagName}>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
#maybeRenderDeepLinkToUnload(explanation: Protocol.Page.BackForwardCacheNotRestoredExplanation): LitHtml.LitTemplate {
|
|
if (explanation.reason === Protocol.Page.BackForwardCacheNotRestoredReason.UnloadHandlerExistsInMainFrame ||
|
|
explanation.reason === Protocol.Page.BackForwardCacheNotRestoredReason.UnloadHandlerExistsInSubFrame) {
|
|
return LitHtml.html`
|
|
<x-link href="https://web.dev/bfcache/#never-use-the-unload-event" class="link">
|
|
${i18nString(UIStrings.neverUseUnload)}
|
|
</x-link>`;
|
|
}
|
|
return LitHtml.nothing;
|
|
}
|
|
|
|
#renderReason(explanation: Protocol.Page.BackForwardCacheNotRestoredExplanation, frames: string[]|undefined):
|
|
LitHtml.TemplateResult {
|
|
// clang-format off
|
|
return LitHtml.html`
|
|
<${ReportView.ReportView.ReportSection.litTagName}>
|
|
${(explanation.reason in NotRestoredReasonDescription) ?
|
|
LitHtml.html`
|
|
<div class="circled-exclamation-icon">
|
|
<${IconButton.Icon.Icon.litTagName} class="inline-icon" .data=${{
|
|
iconName: 'circled_exclamation_icon',
|
|
color: 'orange',
|
|
width: '16px',
|
|
height: '16px',
|
|
} as IconButton.Icon.IconData}>
|
|
</${IconButton.Icon.Icon.litTagName}>
|
|
</div>
|
|
<div>
|
|
${NotRestoredReasonDescription[explanation.reason].name()}
|
|
${this.#maybeRenderDeepLinkToUnload(explanation)}
|
|
${this.#maybeRenderReasonContext(explanation)}
|
|
</div>` :
|
|
LitHtml.nothing}
|
|
</${ReportView.ReportView.ReportSection.litTagName}>
|
|
<div class="gray-text">
|
|
${explanation.reason}
|
|
</div>
|
|
${this.#renderFramesPerReason(frames)}
|
|
`;
|
|
// clang-format on
|
|
}
|
|
}
|
|
|
|
interface FrameTreeNodeData {
|
|
text: string;
|
|
iconName?: string;
|
|
}
|
|
|
|
ComponentHelpers.CustomElements.defineComponent('devtools-resources-back-forward-cache-view', BackForwardCacheView);
|
|
|
|
declare global {
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
interface HTMLElementTagNameMap {
|
|
'devtools-resources-back-forward-cache-view': BackForwardCacheView;
|
|
}
|
|
}
|