chrome-devtools-frontend
Version:
Chrome DevTools UI
304 lines (270 loc) • 12.2 kB
text/typescript
// Copyright 2020 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.
/* eslint-disable rulesdir/no-imperative-dom-api */
import * as Common from '../../core/common/common.js';
import * as Host from '../../core/host/host.js';
import * as i18n from '../../core/i18n/i18n.js';
import type * as Platform from '../../core/platform/platform.js';
import * as SDK from '../../core/sdk/sdk.js';
import type * as Protocol from '../../generated/protocol.js';
import type * as IssuesManager from '../../models/issues_manager/issues_manager.js';
import * as Logs from '../../models/logs/logs.js';
import type * as NetworkForward from '../../panels/network/forward/forward.js';
import * as IconButton from '../../ui/components/icon_button/icon_button.js';
import * as RequestLinkIcon from '../../ui/components/request_link_icon/request_link_icon.js';
import * as Components from '../../ui/legacy/components/utils/utils.js';
import * as UI from '../../ui/legacy/legacy.js';
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
import type {AggregatedIssue} from './IssueAggregator.js';
import type {IssueView} from './IssueView.js';
const UIStrings = {
/**
*@description Text in Object Properties Section
*/
unknown: 'unknown',
/**
*@description Tooltip for button linking to the Elements panel
*/
clickToRevealTheFramesDomNodeIn: 'Click to reveal the frame\'s DOM node in the Elements panel',
/**
*@description Replacement text for a link to an HTML element which is not available (anymore).
*/
unavailable: 'unavailable',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/issues/AffectedResourcesView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export const enum AffectedItem {
COOKIE = 'Cookie',
DIRECTIVE = 'Directive',
ELEMENT = 'Element',
REQUEST = 'Request',
SOURCE = 'Source',
}
export const extractShortPath = (path: Platform.DevToolsPath.UrlString): string => {
// 1st regex matches everything after last '/'
// if path ends with '/', 2nd regex returns everything between the last two '/'
return (/[^/]+$/.exec(path) || /[^/]+\/$/.exec(path) || [''])[0];
};
export interface CreateRequestCellOptions {
linkToPreflight?: boolean;
highlightHeader?: {section: NetworkForward.UIRequestLocation.UIHeaderSection, name: string};
networkTab?: NetworkForward.UIRequestLocation.UIRequestTabs;
additionalOnClickAction?: () => void;
}
/**
* The base class for all affected resource views. It provides basic scaffolding
* as well as machinery for resolving request and frame ids to SDK objects.
*/
export abstract class AffectedResourcesView extends UI.TreeOutline.TreeElement {
readonly #parentView: IssueView;
protected issue: AggregatedIssue;
protected affectedResourcesCountElement: HTMLElement;
protected affectedResources: HTMLElement;
#affectedResourcesCount: number;
#frameListeners: Common.EventTarget.EventDescriptor[];
#unresolvedFrameIds: Set<string>;
protected requestResolver: Logs.RequestResolver.RequestResolver;
constructor(parent: IssueView, issue: AggregatedIssue, jslogContext: string) {
super(/* title */ undefined, /* expandable */ undefined, jslogContext);
this.#parentView = parent;
this.issue = issue;
this.toggleOnClick = true;
this.affectedResourcesCountElement = this.createAffectedResourcesCounter();
this.affectedResources = this.createAffectedResources();
this.#affectedResourcesCount = 0;
this.requestResolver = new Logs.RequestResolver.RequestResolver();
this.#frameListeners = [];
this.#unresolvedFrameIds = new Set();
}
/**
* Sets the issue to take the resources from. Does not
* trigger an update, the caller needs to do that explicitly.
*/
setIssue(issue: AggregatedIssue): void {
this.issue = issue;
}
createAffectedResourcesCounter(): HTMLElement {
const counterLabel = document.createElement('div');
counterLabel.classList.add('affected-resource-label');
this.listItemElement.appendChild(counterLabel);
return counterLabel;
}
createAffectedResources(): HTMLElement {
const body = new UI.TreeOutline.TreeElement();
const affectedResources = document.createElement('table');
affectedResources.classList.add('affected-resource-list');
body.listItemElement.appendChild(affectedResources);
this.appendChild(body);
return affectedResources;
}
protected abstract getResourceNameWithCount(count: number): string;
protected updateAffectedResourceCount(count: number): void {
this.#affectedResourcesCount = count;
this.affectedResourcesCountElement.textContent = this.getResourceNameWithCount(count);
this.hidden = this.#affectedResourcesCount === 0;
this.#parentView.updateAffectedResourceVisibility();
}
isEmpty(): boolean {
return this.#affectedResourcesCount === 0;
}
clear(): void {
this.affectedResources.textContent = '';
this.requestResolver.clear();
}
expandIfOneResource(): void {
if (this.#affectedResourcesCount === 1) {
this.expand();
}
}
/**
* This function resolves a frameId to a ResourceTreeFrame. If the frameId does not resolve, or hasn't navigated yet,
* a listener is installed that takes care of updating the view if the frame is added. This is useful if the issue is
* added before the frame gets reported.
*/
#resolveFrameId(frameId: Protocol.Page.FrameId): SDK.ResourceTreeModel.ResourceTreeFrame|null {
const frame = SDK.FrameManager.FrameManager.instance().getFrame(frameId);
if (!frame || !frame.url) {
this.#unresolvedFrameIds.add(frameId);
if (!this.#frameListeners.length) {
const addListener = SDK.FrameManager.FrameManager.instance().addEventListener(
SDK.FrameManager.Events.FRAME_ADDED_TO_TARGET, this.#onFrameChanged, this);
const navigateListener = SDK.FrameManager.FrameManager.instance().addEventListener(
SDK.FrameManager.Events.FRAME_NAVIGATED, this.#onFrameChanged, this);
this.#frameListeners = [addListener, navigateListener];
}
}
return frame;
}
#onFrameChanged(event: Common.EventTarget.EventTargetEvent<{frame: SDK.ResourceTreeModel.ResourceTreeFrame}>): void {
const frame = event.data.frame;
if (!frame.url) {
return;
}
const frameWasUnresolved = this.#unresolvedFrameIds.delete(frame.id);
if (this.#unresolvedFrameIds.size === 0 && this.#frameListeners.length) {
// Stop listening once all requests are resolved.
Common.EventTarget.removeEventListeners(this.#frameListeners);
this.#frameListeners = [];
}
if (frameWasUnresolved) {
this.update();
}
}
protected createFrameCell(frameId: Protocol.Page.FrameId, issueCategory: IssuesManager.Issue.IssueCategory):
HTMLElement {
const frame = this.#resolveFrameId(frameId);
const url = frame && (frame.unreachableUrl() || frame.url) || i18nString(UIStrings.unknown);
const frameCell = document.createElement('td');
frameCell.classList.add('affected-resource-cell');
if (frame) {
const icon = new IconButton.Icon.Icon();
icon.data = {iconName: 'code-circle', color: 'var(--icon-link)', width: '16px', height: '16px'};
icon.classList.add('link', 'elements-panel');
icon.onclick = async () => {
Host.userMetrics.issuesPanelResourceOpened(issueCategory, AffectedItem.ELEMENT);
const frame = SDK.FrameManager.FrameManager.instance().getFrame(frameId);
if (frame) {
const ownerNode = await frame.getOwnerDOMNodeOrDocument();
if (ownerNode) {
void Common.Revealer.reveal(ownerNode);
}
}
};
icon.title = i18nString(UIStrings.clickToRevealTheFramesDomNodeIn);
frameCell.appendChild(icon);
}
frameCell.appendChild(document.createTextNode(url));
frameCell.onmouseenter = () => {
const frame = SDK.FrameManager.FrameManager.instance().getFrame(frameId);
if (frame) {
void frame.highlight();
}
};
frameCell.onmouseleave = () => SDK.OverlayModel.OverlayModel.hideDOMNodeHighlight();
return frameCell;
}
protected createRequestCell(affectedRequest: Protocol.Audits.AffectedRequest, options: CreateRequestCellOptions = {}):
HTMLElement {
const requestCell = document.createElement('td');
requestCell.classList.add('affected-resource-cell');
const requestLinkIcon = new RequestLinkIcon.RequestLinkIcon.RequestLinkIcon();
requestLinkIcon.data = {...options, affectedRequest, requestResolver: this.requestResolver, displayURL: true};
requestCell.appendChild(requestLinkIcon);
return requestCell;
}
protected async createElementCell(
{backendNodeId, nodeName, target}: IssuesManager.Issue.AffectedElement,
issueCategory: IssuesManager.Issue.IssueCategory): Promise<Element> {
if (!target) {
const cellElement = document.createElement('td');
cellElement.textContent = nodeName || i18nString(UIStrings.unavailable);
return cellElement;
}
function sendTelemetry(): void {
Host.userMetrics.issuesPanelResourceOpened(issueCategory, AffectedItem.ELEMENT);
}
const deferredDOMNode = new SDK.DOMModel.DeferredDOMNode(target, backendNodeId);
const anchorElement = (await Common.Linkifier.Linkifier.linkify(deferredDOMNode)) as HTMLElement;
anchorElement.textContent = nodeName;
anchorElement.addEventListener('click', () => sendTelemetry());
anchorElement.addEventListener('keydown', (event: Event) => {
if ((event as KeyboardEvent).key === 'Enter') {
sendTelemetry();
}
});
const cellElement = document.createElement('td');
cellElement.classList.add('affected-resource-element', 'devtools-link');
cellElement.appendChild(anchorElement);
return cellElement;
}
protected appendSourceLocation(
element: HTMLElement,
sourceLocation: {url: string, lineNumber: number, scriptId?: Protocol.Runtime.ScriptId, columnNumber?: number}|
undefined,
target: SDK.Target.Target|null|undefined): void {
const sourceCodeLocation = document.createElement('td');
sourceCodeLocation.classList.add('affected-source-location');
if (sourceLocation) {
const maxLengthForDisplayedURLs = 40; // Same as console messages.
// TODO(crbug.com/1108503): Add some mechanism to be able to add telemetry to this element.
const linkifier = new Components.Linkifier.Linkifier(maxLengthForDisplayedURLs);
const sourceAnchor = linkifier.linkifyScriptLocation(
target || null, sourceLocation.scriptId || null, sourceLocation.url as Platform.DevToolsPath.UrlString,
sourceLocation.lineNumber, {columnNumber: sourceLocation.columnNumber, inlineFrameIndex: 0});
sourceAnchor.setAttribute('jslog', `${VisualLogging.link('source-location').track({click: true})}`);
sourceCodeLocation.appendChild(sourceAnchor);
}
element.appendChild(sourceCodeLocation);
}
protected appendColumnTitle(header: HTMLElement, title: string, additionalClass: string|null = null): void {
const info = document.createElement('td');
info.classList.add('affected-resource-header');
if (additionalClass) {
info.classList.add(additionalClass);
}
info.textContent = title;
header.appendChild(info);
}
protected createIssueDetailCell(textContent: string|HTMLElement, additionalClass: string|null = null):
HTMLTableDataCellElement {
const cell = document.createElement('td');
if (typeof textContent === 'string') {
cell.textContent = textContent;
} else {
cell.appendChild(textContent);
}
if (additionalClass) {
cell.classList.add(additionalClass);
}
return cell;
}
protected appendIssueDetailCell(
element: HTMLElement, textContent: string|HTMLElement,
additionalClass: string|null = null): HTMLTableDataCellElement {
const cell = this.createIssueDetailCell(textContent, additionalClass);
element.appendChild(cell);
return cell;
}
abstract update(): void;
}