UNPKG

chrome-devtools-frontend

Version:
308 lines (278 loc) • 12.2 kB
// 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?'); } }