chrome-devtools-frontend
Version:
Chrome DevTools UI
308 lines (278 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.
import * as Common from '../common/common.js';
import * as Components from '../components/components.js';
import * as Host from '../host/host.js';
import * as i18n from '../i18n/i18n.js';
import * as Network from '../network/network.js';
import * as SDK from '../sdk/sdk.js';
import * as WebComponents from '../ui/components/components.js';
import * as UI from '../ui/ui.js';
import {IssueView} from './IssueView.js';
export 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 Title for a link to a request in the network panel
*/
clickToShowRequestInTheNetwork: 'Click to show request in the network panel',
/**
*@description Title for an unavailable link a request in the network panel
*/
requestUnavailableInTheNetwork: 'Request unavailable in the network panel, try reloading the inspected page',
};
const str_ = i18n.i18n.registerUIStrings('issues/AffectedResourcesView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export const enum AffectedItem {
Cookie = 'Cookie',
Directive = 'Directive',
Element = 'Element',
LearnMore = 'LearnMore',
Request = 'Request',
Source = 'Source',
}
export const extractShortPath = (path: string): 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];
};
/**
* 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 class AffectedResourcesView extends UI.TreeOutline.TreeElement {
private readonly parentView: IssueView;
protected readonly resourceName: {singular: string, plural: string};
private affectedResourcesCountElement: HTMLElement;
protected affectedResources: HTMLElement;
private affectedResourcesCount: number;
private networkListener: Common.EventTarget.EventDescriptor|null;
private frameListeners: Common.EventTarget.EventDescriptor[];
private unresolvedRequestIds: Set<string>;
private unresolvedFrameIds: Set<string>;
/**
* @param resourceName - Singular and plural of the affected resource name.
*/
constructor(parent: IssueView, resourceName: {singular: string, plural: string}) {
super();
this.toggleOnClick = true;
this.parentView = parent;
this.resourceName = resourceName;
this.affectedResourcesCountElement = this.createAffectedResourcesCounter();
this.affectedResources = this.createAffectedResources();
this.affectedResourcesCount = 0;
this.networkListener = null;
this.frameListeners = [];
this.unresolvedRequestIds = new Set();
this.unresolvedFrameIds = new Set();
}
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;
}
private getResourceName(count: number): string {
if (count === 1) {
return this.resourceName.singular;
}
return this.resourceName.plural;
}
protected updateAffectedResourceCount(count: number): void {
this.affectedResourcesCount = count;
this.affectedResourcesCountElement.textContent = `${count} ${this.getResourceName(count)}`;
this.hidden = this.affectedResourcesCount === 0;
this.parentView.updateAffectedResourceVisibility();
}
isEmpty(): boolean {
return this.affectedResourcesCount === 0;
}
clear(): void {
this.affectedResources.textContent = '';
}
expandIfOneResource(): void {
if (this.affectedResourcesCount === 1) {
this.expand();
}
}
/**
* This function resolves a requestId to network requests. If the requestId does not resolve, a listener is installed
* that takes care of updating the view if the network request is added. This is useful if the issue is added before
* the network request gets reported.
*/
protected resolveRequestId(requestId: string): SDK.NetworkRequest.NetworkRequest[] {
const requests = SDK.NetworkLog.NetworkLog.instance().requestsForId(requestId);
if (!requests.length) {
this.unresolvedRequestIds.add(requestId);
if (!this.networkListener) {
this.networkListener = SDK.NetworkLog.NetworkLog.instance().addEventListener(
SDK.NetworkLog.Events.RequestAdded, this.onRequestAdded, this);
}
}
return requests;
}
private onRequestAdded(event: Common.EventTarget.EventTargetEvent): void {
const request = event.data as SDK.NetworkRequest.NetworkRequest;
const requestWasUnresolved = this.unresolvedRequestIds.delete(request.requestId());
if (this.unresolvedRequestIds.size === 0 && this.networkListener) {
// Stop listening once all requests are resolved.
Common.EventTarget.EventTarget.removeEventListeners([this.networkListener]);
this.networkListener = null;
}
if (requestWasUnresolved) {
this.update();
}
}
/**
* 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.
*/
private 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.FrameAddedToTarget, this.onFrameChanged, this);
const navigateListener = SDK.FrameManager.FrameManager.instance().addEventListener(
SDK.FrameManager.Events.FrameNavigated, this.onFrameChanged, this);
this.frameListeners = [addListener, navigateListener];
}
}
return frame;
}
private onFrameChanged(event: Common.EventTarget.EventTargetEvent): void {
const frame = event.data.frame as SDK.ResourceTreeModel.ResourceTreeFrame;
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.EventTarget.removeEventListeners(this.frameListeners);
this.frameListeners = [];
}
if (frameWasUnresolved) {
this.update();
}
}
protected createFrameCell(frameId: Protocol.Page.FrameId, issue: SDK.Issue.Issue): 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 WebComponents.Icon.Icon();
icon.data = {iconName: 'elements_panel_icon', color: 'var(--issue-link)', width: '16px', height: '16px'};
icon.classList.add('link', 'elements-panel');
icon.onclick = async (): Promise<void> => {
Host.userMetrics.issuesPanelResourceOpened(issue.getCategory(), AffectedItem.Element);
const frame = SDK.FrameManager.FrameManager.instance().getFrame(frameId);
if (frame) {
const ownerNode = await frame.getOwnerDOMNodeOrDocument();
if (ownerNode) {
Common.Revealer.reveal(ownerNode);
}
}
};
UI.Tooltip.Tooltip.install(icon, i18nString(UIStrings.clickToRevealTheFramesDomNodeIn));
frameCell.appendChild(icon);
}
frameCell.appendChild(document.createTextNode(url));
frameCell.onmouseenter = (): void => {
const frame = SDK.FrameManager.FrameManager.instance().getFrame(frameId);
if (frame) {
frame.highlight();
}
};
frameCell.onmouseleave = (): void => SDK.OverlayModel.OverlayModel.hideDOMNodeHighlight();
return frameCell;
}
protected createRequestCell(request: Protocol.Audits.AffectedRequest): HTMLElement {
let url = request.url;
let filename = url ? extractShortPath(url) : '';
const requestCell = document.createElement('td');
requestCell.classList.add('affected-resource-cell');
const icon = new WebComponents.Icon.Icon();
icon.data = {iconName: 'network_panel_icon', color: 'var(--issue-link)', width: '16px', height: '16px'};
icon.classList.add('network-panel');
requestCell.appendChild(icon);
const requests = this.resolveRequestId(request.requestId);
if (requests.length) {
const request = requests[0];
requestCell.onclick = (): void => {
Network.NetworkPanel.NetworkPanel.selectAndShowRequest(request, Network.NetworkItemView.Tabs.Headers);
};
requestCell.classList.add('link');
icon.classList.add('link');
url = request.url();
filename = extractShortPath(url);
UI.Tooltip.Tooltip.install(icon, i18nString(UIStrings.clickToShowRequestInTheNetwork));
} else {
UI.Tooltip.Tooltip.install(icon, i18nString(UIStrings.requestUnavailableInTheNetwork));
icon.classList.add('unavailable');
}
if (url) {
UI.Tooltip.Tooltip.install(requestCell, url);
}
requestCell.appendChild(document.createTextNode(filename));
return requestCell;
}
protected appendSourceLocation(
element: HTMLElement, sourceLocation: Protocol.Audits.SourceCodeLocation|undefined,
target: SDK.SDKModel.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, sourceLocation.lineNumber);
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, additionalClass: string|null = null): HTMLTableDataCellElement {
const cell = document.createElement('td');
cell.textContent = textContent;
if (additionalClass) {
cell.classList.add(additionalClass);
}
return cell;
}
protected appendIssueDetailCell(element: HTMLElement, textContent: string, additionalClass: string|null = null):
HTMLTableDataCellElement {
const cell = this.createIssueDetailCell(textContent, additionalClass);
element.appendChild(cell);
return cell;
}
update(): void {
throw new Error('This should never be called, did you forget to override?');
}
}