blob: 56baed0b5cac149af9057012e879f633f9107596 [file] [log] [blame]
// 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 BrowserSDK from '../browser_sdk/browser_sdk.js';
import * as Common from '../common/common.js'; // eslint-disable-line no-unused-vars
import * as Components from '../components/components.js';
import * as Elements from '../elements/elements.js';
import * as Host from '../host/host.js';
import * as Network from '../network/network.js';
import * as SDK from '../sdk/sdk.js';
import * as UI from '../ui/ui.js';
import {AggregatedIssue, Events as IssueAggregatorEvents, IssueAggregator} from './IssueAggregator.js'; // eslint-disable-line no-unused-vars
import {IssueSurveyLink} from './IssueSurveyLink.js';
import {createIssueDescriptionFromMarkdown} from './MarkdownIssueDescription.js';
/** @enum {string} */
const AffectedItem = {
Cookie: 'Cookie',
Directive: 'Directive',
Element: 'Element',
LearnMore: 'LearnMore',
Request: 'Request',
Source: 'Source'
};
/**
* @param {string} path
* @return {string}
*/
const extractShortPath = path => {
// 1st regex matches everything after last '/'
// if path ends with '/', 2nd regex returns everything between the last two '/'
return (/[^/]+$/.exec(path) || /[^/]+\/$/.exec(path) || [''])[0];
};
class AffectedResourcesView extends UI.TreeOutline.TreeElement {
/**
* @param {!IssueView} parent
* @param {!{singular:string, plural:string}} resourceName - Singular and plural of the affected resource name.
*/
constructor(parent, resourceName) {
super();
this.toggleOnClick = true;
/** @type {!IssueView} */
this._parent = parent;
this._resourceName = resourceName;
/** @type {!Element} */
this._affectedResourcesCountElement = this.createAffectedResourcesCounter();
/** @type {!Element} */
this._affectedResources = this.createAffectedResources();
this._affectedResourcesCount = 0;
/** @type {?Common.EventTarget.EventDescriptor} */
this._networkListener = null;
/** @type {!Array<!Common.EventTarget.EventDescriptor>} */
this._frameListeners = [];
/** @type {!Set<string>} */
this._unresolvedRequestIds = new Set();
/** @type {!Set<string>} */
this._unresolvedFrameIds = new Set();
}
/**
* @returns {!Element}
*/
createAffectedResourcesCounter() {
const counterLabel = document.createElement('div');
counterLabel.classList.add('affected-resource-label');
this.listItemElement.appendChild(counterLabel);
return counterLabel;
}
/**
* @returns {!Element}
*/
createAffectedResources() {
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;
}
/**
*
* @param {number} count
*/
getResourceName(count) {
if (count === 1) {
return this._resourceName.singular;
}
return this._resourceName.plural;
}
/**
* @param {number} count
*/
updateAffectedResourceCount(count) {
this._affectedResourcesCount = count;
this._affectedResourcesCountElement.textContent = `${count} ${this.getResourceName(count)}`;
this.hidden = this._affectedResourcesCount === 0;
this._parent.updateAffectedResourceVisibility();
}
/**
* @returns {boolean}
*/
isEmpty() {
return this._affectedResourcesCount === 0;
}
clear() {
this._affectedResources.textContent = '';
}
expandIfOneResource() {
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.
* @param {string} requestId
* @return {!Array<!SDK.NetworkRequest.NetworkRequest>}
*/
_resolveRequestId(requestId) {
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;
}
/**
* @param {!Common.EventTarget.EventTargetEvent} event
*/
_onRequestAdded(event) {
const request = /** @type {!SDK.NetworkRequest.NetworkRequest} */ (event.data);
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.
* @param {!Protocol.Page.FrameId} frameId
* @return {?SDK.ResourceTreeModel.ResourceTreeFrame}
*/
_resolveFrameId(frameId) {
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;
}
/**
*
* @param {!Common.EventTarget.EventTargetEvent} event
*/
_onFrameChanged(event) {
const frame = /** @type {!SDK.ResourceTreeModel.ResourceTreeFrame} */ (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.EventTarget.removeEventListeners(this._frameListeners);
this._frameListeners = [];
}
if (frameWasUnresolved) {
this.update();
}
}
/**
* @param {!Protocol.Page.FrameId} frameId
* @param {!SDK.Issue.Issue} issue
* @returns {!HTMLElement}
*/
_createFrameCell(frameId, issue) {
const frame = this._resolveFrameId(frameId);
const url = frame && (frame.unreachableUrl() || frame.url) || ls`unknown`;
const frameCell = /** @type {!HTMLElement} */ (document.createElement('td'));
frameCell.classList.add('affected-resource-cell');
if (frame) {
const icon = new Elements.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 () => {
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, ls`Click to reveal the frame's DOM node in the Elements panel`);
frameCell.appendChild(icon);
}
frameCell.appendChild(document.createTextNode(url));
frameCell.onmouseenter = () => {
const frame = SDK.FrameManager.FrameManager.instance().getFrame(frameId);
if (frame) {
frame.highlight();
}
};
frameCell.onmouseleave = () => SDK.OverlayModel.OverlayModel.hideDOMNodeHighlight();
return frameCell;
}
/**
* @param {!Protocol.Audits.AffectedRequest} request
* @returns {!HTMLElement}
*/
_createRequestCell(request) {
let url = request.url;
let filename = url ? extractShortPath(url) : '';
const requestCell = /** @type {!HTMLElement} */ (document.createElement('td'));
requestCell.classList.add('affected-resource-cell');
const icon = new Elements.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 = () => {
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, ls`Click to show request in the network panel`);
} else {
UI.Tooltip.Tooltip.install(icon, ls`Request unavailable in the network panel, try reloading the inspected page`);
icon.classList.add('unavailable');
}
if (url) {
UI.Tooltip.Tooltip.install(requestCell, url);
}
requestCell.appendChild(document.createTextNode(filename));
return requestCell;
}
/**
* @virtual
* @return {void}
*/
update() {
throw new Error('This should never be called, did you forget to override?');
}
}
class AffectedElementsView extends AffectedResourcesView {
/**
* @param {!IssueView} parent
* @param {!SDK.Issue.Issue} issue
*/
constructor(parent, issue) {
super(parent, {singular: ls`element`, plural: ls`elements`});
/** @type {!SDK.Issue.Issue} */
this._issue = issue;
}
_sendTelemetry() {
Host.userMetrics.issuesPanelResourceOpened(this._issue.getCategory(), AffectedItem.Element);
}
/**
* @param {!Iterable<!SDK.Issue.AffectedElement>} affectedElements
*/
async _appendAffectedElements(affectedElements) {
let count = 0;
for (const element of affectedElements) {
await this._appendAffectedElement(element);
count++;
}
this.updateAffectedResourceCount(count);
}
/**
* @param {!SDK.Issue.AffectedElement} element
*/
async _appendAffectedElement({backendNodeId, nodeName}) {
const mainTarget = /** @type {!SDK.SDKModel.Target} */ (SDK.SDKModel.TargetManager.instance().mainTarget());
const deferredDOMNode = new SDK.DOMModel.DeferredDOMNode(mainTarget, backendNodeId);
const anchorElement = await Common.Linkifier.Linkifier.linkify(deferredDOMNode);
anchorElement.textContent = nodeName;
anchorElement.addEventListener('click', this._sendTelemetry);
anchorElement.addEventListener('keydown', /** @param {!Event} event */ event => {
if (isEnterKey(event)) {
this._sendTelemetry();
}
});
const cellElement = document.createElement('td');
cellElement.classList.add('affected-resource-element', 'devtools-link');
cellElement.appendChild(anchorElement);
const rowElement = document.createElement('tr');
rowElement.appendChild(cellElement);
this._affectedResources.appendChild(rowElement);
}
/**
* @override
*/
update() {
this.clear();
this._appendAffectedElements(this._issue.elements());
}
}
class AffectedDirectivesView extends AffectedResourcesView {
/**
* @param {!IssueView} parent
* @param {!AggregatedIssue} issue
*/
constructor(parent, issue) {
super(parent, {singular: ls`directive`, plural: ls`directives`});
/** @type {!AggregatedIssue} */
this._issue = issue;
}
/**
* @param {!Element} header
*/
_appendDirectiveColumnTitle(header) {
const name = document.createElement('td');
name.classList.add('affected-resource-header');
name.textContent = ls`Directive`;
header.appendChild(name);
}
/**
* @param {!Element} header
*/
_appendURLColumnTitle(header) {
const info = document.createElement('td');
info.classList.add('affected-resource-header');
info.classList.add('affected-resource-directive-info-header');
info.textContent = ls`Resource`;
header.appendChild(info);
}
/**
* @param {!Element} header
*/
_appendElementColumnTitle(header) {
const affectedNode = document.createElement('td');
affectedNode.classList.add('affected-resource-header');
affectedNode.textContent = ls`Element`;
header.appendChild(affectedNode);
}
/**
* @param {!Element} header
*/
_appendSourceCodeColumnTitle(header) {
const sourceCodeLink = document.createElement('td');
sourceCodeLink.classList.add('affected-resource-header');
sourceCodeLink.textContent = ls`Source code`;
header.appendChild(sourceCodeLink);
}
/**
* @param {!Element} header
*/
_appendStatusColumnTitle(header) {
const status = document.createElement('td');
status.classList.add('affected-resource-header');
status.textContent = ls`Status`;
header.appendChild(status);
}
/**
* @param {!Element} element
* @param {boolean} isReportOnly
*/
_appendStatus(element, isReportOnly) {
const status = document.createElement('td');
if (isReportOnly) {
status.classList.add('affected-resource-report-only-status');
status.textContent = ls`report-only`;
} else {
status.classList.add('affected-resource-blocked-status');
status.textContent = ls`blocked`;
}
element.appendChild(status);
}
/**
* @param {!Element} element
* @param {string} directive
*/
_appendViolatedDirective(element, directive) {
const violatedDirective = document.createElement('td');
violatedDirective.textContent = directive;
element.appendChild(violatedDirective);
}
/**
* @param {!Element} element
* @param {string} url
*/
_appendBlockedURL(element, url) {
const info = document.createElement('td');
info.classList.add('affected-resource-directive-info');
info.textContent = url;
element.appendChild(info);
}
/**
* @param {!Element} element
* @param {number | undefined} nodeId
* @param {!SDK.IssuesModel.IssuesModel} model
*/
_appendBlockedElement(element, nodeId, model) {
const elementsPanelLinkComponent = new Elements.ElementsPanelLink.ElementsPanelLink();
if (nodeId) {
const violatingNodeId = nodeId;
UI.Tooltip.Tooltip.install(
elementsPanelLinkComponent, ls`Click to reveal the violating DOM node in the Elements panel`);
/** @type {function(!Event=):void} */
const onElementRevealIconClick = () => {
const target = model.getTargetIfNotDisposed();
if (target) {
Host.userMetrics.issuesPanelResourceOpened(this._issue.getCategory(), AffectedItem.Element);
const deferredDOMNode = new SDK.DOMModel.DeferredDOMNode(target, violatingNodeId);
Common.Revealer.reveal(deferredDOMNode);
}
};
/** @type {function(!Event=):void} */
const onElementRevealIconMouseEnter = () => {
const target = model.getTargetIfNotDisposed();
if (target) {
const deferredDOMNode = new SDK.DOMModel.DeferredDOMNode(target, violatingNodeId);
if (deferredDOMNode) {
deferredDOMNode.highlight();
}
}
};
/** @type {function(!Event=):void} */
const onElementRevealIconMouseLeave = () => {
SDK.OverlayModel.OverlayModel.hideDOMNodeHighlight();
};
elementsPanelLinkComponent
.data = {onElementRevealIconClick, onElementRevealIconMouseEnter, onElementRevealIconMouseLeave};
}
const violatingNode = document.createElement('td');
violatingNode.classList.add('affected-resource-csp-info-node');
violatingNode.appendChild(elementsPanelLinkComponent);
element.appendChild(violatingNode);
}
/**
* @param {!Element} element
* @param {?Protocol.Audits.SourceCodeLocation | undefined} sourceLocation
*/
_appendSourceLocation(element, sourceLocation) {
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,
/* scriptId */ null, sourceLocation.url, sourceLocation.lineNumber);
sourceCodeLocation.appendChild(sourceAnchor);
}
element.appendChild(sourceCodeLocation);
}
/**
* @param {!Iterable<!SDK.ContentSecurityPolicyIssue.ContentSecurityPolicyIssue>} cspIssues
*/
_appendAffectedContentSecurityPolicyDetails(cspIssues) {
const header = document.createElement('tr');
if (this._issue.code() === SDK.ContentSecurityPolicyIssue.inlineViolationCode) {
this._appendDirectiveColumnTitle(header);
this._appendElementColumnTitle(header);
this._appendSourceCodeColumnTitle(header);
this._appendStatusColumnTitle(header);
} else if (this._issue.code() === SDK.ContentSecurityPolicyIssue.urlViolationCode) {
this._appendURLColumnTitle(header);
this._appendStatusColumnTitle(header);
this._appendDirectiveColumnTitle(header);
this._appendSourceCodeColumnTitle(header);
} else if (this._issue.code() === SDK.ContentSecurityPolicyIssue.evalViolationCode) {
this._appendSourceCodeColumnTitle(header);
this._appendDirectiveColumnTitle(header);
this._appendStatusColumnTitle(header);
} else if (this._issue.code() === SDK.ContentSecurityPolicyIssue.trustedTypesSinkViolationCode) {
this._appendSourceCodeColumnTitle(header);
this._appendStatusColumnTitle(header);
} else if (this._issue.code() === SDK.ContentSecurityPolicyIssue.trustedTypesPolicyViolationCode) {
this._appendSourceCodeColumnTitle(header);
this._appendDirectiveColumnTitle(header);
this._appendStatusColumnTitle(header);
} else {
this.updateAffectedResourceCount(0);
return;
}
this._affectedResources.appendChild(header);
let count = 0;
for (const cspIssue of cspIssues) {
count++;
this._appendAffectedContentSecurityPolicyDetail(cspIssue);
}
this.updateAffectedResourceCount(count);
}
/**
* @param {!SDK.ContentSecurityPolicyIssue.ContentSecurityPolicyIssue} cspIssue
*/
_appendAffectedContentSecurityPolicyDetail(cspIssue) {
const element = document.createElement('tr');
element.classList.add('affected-resource-directive');
const cspIssueDetails = cspIssue.details();
if (this._issue.code() === SDK.ContentSecurityPolicyIssue.inlineViolationCode) {
this._appendViolatedDirective(element, cspIssueDetails.violatedDirective);
this._appendBlockedElement(element, cspIssueDetails.violatingNodeId, cspIssue.model());
this._appendSourceLocation(element, cspIssueDetails.sourceCodeLocation);
this._appendStatus(element, cspIssueDetails.isReportOnly);
} else if (this._issue.code() === SDK.ContentSecurityPolicyIssue.urlViolationCode) {
const url = cspIssueDetails.blockedURL ? cspIssueDetails.blockedURL : '';
this._appendBlockedURL(element, url);
this._appendStatus(element, cspIssueDetails.isReportOnly);
this._appendViolatedDirective(element, cspIssueDetails.violatedDirective);
this._appendSourceLocation(element, cspIssueDetails.sourceCodeLocation);
} else if (this._issue.code() === SDK.ContentSecurityPolicyIssue.evalViolationCode) {
this._appendSourceLocation(element, cspIssueDetails.sourceCodeLocation);
this._appendViolatedDirective(element, cspIssueDetails.violatedDirective);
this._appendStatus(element, cspIssueDetails.isReportOnly);
} else if (this._issue.code() === SDK.ContentSecurityPolicyIssue.trustedTypesSinkViolationCode) {
this._appendSourceLocation(element, cspIssueDetails.sourceCodeLocation);
this._appendStatus(element, cspIssueDetails.isReportOnly);
} else if (this._issue.code() === SDK.ContentSecurityPolicyIssue.trustedTypesPolicyViolationCode) {
this._appendSourceLocation(element, cspIssueDetails.sourceCodeLocation);
this._appendViolatedDirective(element, cspIssueDetails.violatedDirective);
this._appendStatus(element, cspIssueDetails.isReportOnly);
} else {
return;
}
this._affectedResources.appendChild(element);
}
/**
* @override
*/
update() {
this.clear();
this._appendAffectedContentSecurityPolicyDetails(this._issue.cspIssues());
}
}
class AffectedCookiesView extends AffectedResourcesView {
/**
* @param {!IssueView} parent
* @param {!AggregatedIssue} issue
*/
constructor(parent, issue) {
super(parent, {singular: ls`cookie`, plural: ls`cookies`});
/** @type {!AggregatedIssue} */
this._issue = issue;
}
/**
* @param {!Iterable<!{cookie: !Protocol.Audits.AffectedCookie, hasRequest: boolean}>} cookies
*/
_appendAffectedCookies(cookies) {
const header = document.createElement('tr');
const name = document.createElement('td');
name.classList.add('affected-resource-header');
name.textContent = 'Name';
header.appendChild(name);
const info = document.createElement('td');
info.classList.add('affected-resource-header');
info.classList.add('affected-resource-cookie-info-header');
info.textContent = ls`Domain` +
' & ' + ls`Path`;
header.appendChild(info);
this._affectedResources.appendChild(header);
let count = 0;
for (const cookie of cookies) {
count++;
this.appendAffectedCookie(cookie.cookie, cookie.hasRequest);
}
this.updateAffectedResourceCount(count);
}
/**
* @param {!Protocol.Audits.AffectedCookie} cookie
* @param {boolean} hasAssociatedRequest
*/
appendAffectedCookie(cookie, hasAssociatedRequest) {
const element = document.createElement('tr');
element.classList.add('affected-resource-cookie');
const name = document.createElement('td');
if (hasAssociatedRequest) {
name.appendChild(UI.UIUtils.createTextButton(cookie.name, () => {
Host.userMetrics.issuesPanelResourceOpened(this._issue.getCategory(), AffectedItem.Cookie);
Network.NetworkPanel.NetworkPanel.revealAndFilter([
{
filterType: 'cookie-domain',
filterValue: cookie.domain,
},
{
filterType: 'cookie-name',
filterValue: cookie.name,
},
{
filterType: 'cookie-path',
filterValue: cookie.path,
}
]);
}, 'link-style devtools-link'));
} else {
name.textContent = cookie.name;
}
const info = document.createElement('td');
info.classList.add('affected-resource-cookie-info');
info.textContent = `${cookie.domain}${cookie.path}`;
element.appendChild(name);
element.appendChild(info);
this._affectedResources.appendChild(element);
}
/**
* @override
*/
update() {
this.clear();
this._appendAffectedCookies(this._issue.cookiesWithRequestIndicator());
}
}
class AffectedRequestsView extends AffectedResourcesView {
/**
* @param {!IssueView} parent
* @param {!SDK.Issue.Issue} issue
*/
constructor(parent, issue) {
super(parent, {singular: ls`request`, plural: ls`requests`});
/** @type {!SDK.Issue.Issue} */
this._issue = issue;
}
/**
* @param {!Iterable<!Protocol.Audits.AffectedRequest>} affectedRequests
*/
_appendAffectedRequests(affectedRequests) {
let count = 0;
for (const affectedRequest of affectedRequests) {
for (const request of this._resolveRequestId(affectedRequest.requestId)) {
count++;
this._appendNetworkRequest(request);
}
}
this.updateAffectedResourceCount(count);
}
/**
*
* @param {!SDK.NetworkRequest.NetworkRequest} request
*/
_appendNetworkRequest(request) {
const nameText = request.name().trimMiddle(100);
const nameElement = document.createElement('td');
const tab = issueTypeToNetworkHeaderMap.get(this._issue.getCategory()) || Network.NetworkItemView.Tabs.Headers;
nameElement.appendChild(UI.UIUtils.createTextButton(nameText, () => {
Host.userMetrics.issuesPanelResourceOpened(this._issue.getCategory(), AffectedItem.Request);
Network.NetworkPanel.NetworkPanel.selectAndShowRequest(request, tab);
}, 'link-style devtools-link'));
const element = document.createElement('tr');
element.classList.add('affected-resource-request');
element.appendChild(nameElement);
this._affectedResources.appendChild(element);
}
/**
* @override
*/
update() {
this.clear();
// eslint-disable-next-line no-unused-vars
for (const _ of this._issue.blockedByResponseDetails()) {
// If the issue has blockedByResponseDetails, the corresponding AffectedBlockedByResponseView
// will take care of displaying the request.
this.updateAffectedResourceCount(0);
return;
}
this._appendAffectedRequests(this._issue.requests());
}
}
class AffectedSourcesView extends AffectedResourcesView {
/**
* @param {!IssueView} parent
* @param {!SDK.Issue.Issue} issue
*/
constructor(parent, issue) {
super(parent, {singular: ls`source`, plural: ls`sources`});
/** @type {!SDK.Issue.Issue} */
this._issue = issue;
}
/**
* @param {!Iterable<!SDK.Issue.AffectedSource>} affectedSources
*/
_appendAffectedSources(affectedSources) {
let count = 0;
for (const source of affectedSources) {
this._appendAffectedSource(source);
count++;
}
this.updateAffectedResourceCount(count);
}
/**
* @param {!SDK.Issue.AffectedSource} source
*/
_appendAffectedSource({url, lineNumber, columnNumber}) {
const cellElement = document.createElement('td');
// TODO(chromium:1072331): Check feasibility of plumping through scriptId for `linkifyScriptLocation`
// to support source maps and formatted scripts.
const linkifierURLOptions =
/** @type {!Components.Linkifier.LinkifyURLOptions} */ ({columnNumber, lineNumber, tabStop: true});
// An element created with linkifyURL can subscribe to the events
// 'click' neither 'keydown' if that key is the 'Enter' key.
// Also, this element has a context menu, so we should be able to
// track when the user use the context menu too.
// TODO(crbug.com/1108503): Add some mechanism to be able to add telemetry to this element.
const anchorElement = Components.Linkifier.Linkifier.linkifyURL(url, linkifierURLOptions);
cellElement.appendChild(anchorElement);
const rowElement = document.createElement('tr');
rowElement.classList.add('affected-resource-source');
rowElement.appendChild(cellElement);
this._affectedResources.appendChild(rowElement);
}
/**
* @override
*/
update() {
this.clear();
this._appendAffectedSources(this._issue.sources());
}
}
/** @type {!Map<!SDK.Issue.IssueCategory, !Network.NetworkItemView.Tabs>} */
const issueTypeToNetworkHeaderMap = new Map([
[SDK.Issue.IssueCategory.SameSiteCookie, Network.NetworkItemView.Tabs.Cookies],
[SDK.Issue.IssueCategory.CrossOriginEmbedderPolicy, Network.NetworkItemView.Tabs.Headers],
[SDK.Issue.IssueCategory.MixedContent, Network.NetworkItemView.Tabs.Headers]
]);
class AffectedMixedContentView extends AffectedResourcesView {
/**
* @param {!IssueView} parent
* @param {!SDK.Issue.Issue} issue
*/
constructor(parent, issue) {
super(parent, {singular: ls`resource`, plural: ls`resources`});
/** @type {!SDK.Issue.Issue} */
this._issue = issue;
}
/**
* @param {!Iterable<!Protocol.Audits.MixedContentIssueDetails>} mixedContents
*/
_appendAffectedMixedContents(mixedContents) {
const header = document.createElement('tr');
const name = document.createElement('td');
name.classList.add('affected-resource-header');
name.textContent = ls`Name`;
header.appendChild(name);
const info = document.createElement('td');
info.classList.add('affected-resource-header');
info.textContent = ls`Restriction Status`;
header.appendChild(info);
this._affectedResources.appendChild(header);
let count = 0;
for (const mixedContent of mixedContents) {
if (mixedContent.request) {
this._resolveRequestId(mixedContent.request.requestId).forEach(networkRequest => {
this.appendAffectedMixedContent(mixedContent, networkRequest);
count++;
});
} else {
this.appendAffectedMixedContent(mixedContent);
count++;
}
}
this.updateAffectedResourceCount(count);
}
/**
* @param {!Protocol.Audits.MixedContentIssueDetails} mixedContent
* @param {?SDK.NetworkRequest.NetworkRequest} maybeRequest
*/
appendAffectedMixedContent(mixedContent, maybeRequest = null) {
const element = document.createElement('tr');
element.classList.add('affected-resource-mixed-content');
const filename = extractShortPath(mixedContent.insecureURL);
const name = document.createElement('td');
if (maybeRequest) {
const request = maybeRequest; // re-assignment to make type checker happy
const tab = issueTypeToNetworkHeaderMap.get(this._issue.getCategory()) || Network.NetworkItemView.Tabs.Headers;
name.appendChild(UI.UIUtils.createTextButton(filename, () => {
Host.userMetrics.issuesPanelResourceOpened(this._issue.getCategory(), AffectedItem.Request);
Network.NetworkPanel.NetworkPanel.selectAndShowRequest(request, tab);
}, 'link-style devtools-link'));
} else {
name.classList.add('affected-resource-mixed-content-info');
name.textContent = filename;
}
UI.Tooltip.Tooltip.install(name, mixedContent.insecureURL);
element.appendChild(name);
const status = document.createElement('td');
status.classList.add('affected-resource-mixed-content-info');
status.textContent = SDK.MixedContentIssue.MixedContentIssue.translateStatus(mixedContent.resolutionStatus);
element.appendChild(status);
this._affectedResources.appendChild(element);
}
/**
* @override
*/
update() {
this.clear();
this._appendAffectedMixedContents(this._issue.mixedContents());
}
}
class AffectedHeavyAdView extends AffectedResourcesView {
/**
* @param {!IssueView} parent
* @param {!SDK.Issue.Issue} issue
*/
constructor(parent, issue) {
super(parent, {singular: ls`resource`, plural: ls`resources`});
/** @type {!SDK.Issue.Issue} */
this._issue = issue;
}
/**
* @param {!Iterable<!Protocol.Audits.HeavyAdIssueDetails>} heavyAds
*/
_appendAffectedHeavyAds(heavyAds) {
const header = document.createElement('tr');
const reason = document.createElement('td');
reason.classList.add('affected-resource-header');
reason.textContent = ls`Limit exceeded`;
header.appendChild(reason);
const resolution = document.createElement('td');
resolution.classList.add('affected-resource-header');
resolution.textContent = ls`Resolution Status`;
header.appendChild(resolution);
const frame = document.createElement('td');
frame.classList.add('affected-resource-header');
frame.textContent = ls`Frame URL`;
header.appendChild(frame);
this._affectedResources.appendChild(header);
let count = 0;
for (const heavyAd of heavyAds) {
this._appendAffectedHeavyAd(heavyAd);
count++;
}
this.updateAffectedResourceCount(count);
}
/**
* @param {!Protocol.Audits.HeavyAdResolutionStatus} status
* @return {string}
*/
_statusToString(status) {
switch (status) {
case Protocol.Audits.HeavyAdResolutionStatus.HeavyAdBlocked:
return ls`Removed`;
case Protocol.Audits.HeavyAdResolutionStatus.HeavyAdWarning:
return ls`Warned`;
}
return '';
}
/**
* @param {!Protocol.Audits.HeavyAdReason} status
* @return {string}
*/
_limitToString(status) {
switch (status) {
case Protocol.Audits.HeavyAdReason.CpuPeakLimit:
return ls`CPU peak limit`;
case Protocol.Audits.HeavyAdReason.CpuTotalLimit:
return ls`CPU total limit`;
case Protocol.Audits.HeavyAdReason.NetworkTotalLimit:
return ls`Network limit`;
}
return '';
}
/**
* @param {!Protocol.Audits.HeavyAdIssueDetails} heavyAd
*/
_appendAffectedHeavyAd(heavyAd) {
const element = document.createElement('tr');
element.classList.add('affected-resource-heavy-ad');
const reason = document.createElement('td');
reason.classList.add('affected-resource-heavy-ad-info');
reason.textContent = this._limitToString(heavyAd.reason);
element.appendChild(reason);
const status = document.createElement('td');
status.classList.add('affected-resource-heavy-ad-info');
status.textContent = this._statusToString(heavyAd.resolution);
element.appendChild(status);
const frameId = heavyAd.frame.frameId;
const frameUrl = this._createFrameCell(frameId, this._issue);
element.appendChild(frameUrl);
this._affectedResources.appendChild(element);
}
/**
* @override
*/
update() {
this.clear();
this._appendAffectedHeavyAds(this._issue.heavyAds());
}
}
class AffectedBlockedByResponseView extends AffectedResourcesView {
/**
* @param {!IssueView} parent
* @param {!SDK.Issue.Issue} issue
*/
constructor(parent, issue) {
super(parent, {singular: ls`request`, plural: ls`requests`});
/** @type {!SDK.Issue.Issue} */
this._issue = issue;
}
/**
* @param {!Iterable<!Protocol.Audits.BlockedByResponseIssueDetails>} details
*/
_appendDetails(details) {
const header = document.createElement('tr');
const request = document.createElement('td');
request.classList.add('affected-resource-header');
request.textContent = ls`Request`;
header.appendChild(request);
const name = document.createElement('td');
name.classList.add('affected-resource-header');
name.textContent = ls`Parent Frame`;
header.appendChild(name);
const frame = document.createElement('td');
frame.classList.add('affected-resource-header');
frame.textContent = ls`Blocked Resource`;
header.appendChild(frame);
this._affectedResources.appendChild(header);
let count = 0;
for (const detail of details) {
this._appendDetail(detail);
count++;
}
this.updateAffectedResourceCount(count);
}
/**
* @param {!Protocol.Audits.BlockedByResponseIssueDetails} details
*/
_appendDetail(details) {
const element = document.createElement('tr');
element.classList.add('affected-resource-row');
const requestCell = this._createRequestCell(details.request);
element.appendChild(requestCell);
if (details.parentFrame) {
const frameUrl = this._createFrameCell(details.parentFrame.frameId, this._issue);
element.appendChild(frameUrl);
} else {
element.appendChild(document.createElement('td'));
}
if (details.blockedFrame) {
const frameUrl = this._createFrameCell(details.blockedFrame.frameId, this._issue);
element.appendChild(frameUrl);
} else {
element.appendChild(document.createElement('td'));
}
this._affectedResources.appendChild(element);
}
/**
* @override
*/
update() {
this.clear();
this._appendDetails(this._issue.blockedByResponseDetails());
}
}
/** @type {!Map<!SDK.Issue.IssueCategory, string>} */
export const IssueCategoryNames = new Map([
[SDK.Issue.IssueCategory.CrossOriginEmbedderPolicy, ls`Cross Origin Embedder Policy`],
[SDK.Issue.IssueCategory.MixedContent, ls`Mixed Content`],
[SDK.Issue.IssueCategory.SameSiteCookie, ls`SameSite Cookie`], [SDK.Issue.IssueCategory.HeavyAd, ls`Heavy Ads`],
[SDK.Issue.IssueCategory.ContentSecurityPolicy, ls`Content Security Policy`],
[SDK.Issue.IssueCategory.Other, ls`Other`]
]);
class IssueCategoryView extends UI.TreeOutline.TreeElement {
/**
* @param {!SDK.Issue.IssueCategory} category
*/
constructor(category) {
super();
this._category = category;
/** @type {!Array<!AggregatedIssue>} */
this._issues = [];
this.toggleOnClick = true;
this.listItemElement.classList.add('issue-category');
}
getCategoryName() {
return IssueCategoryNames.get(this._category) || ls`Other`;
}
/**
* @override
*/
onattach() {
this._appendHeader();
}
_appendHeader() {
const header = document.createElement('div');
header.classList.add('header');
const title = document.createElement('div');
title.classList.add('title');
title.textContent = this.getCategoryName();
header.appendChild(title);
this.listItemElement.appendChild(header);
}
}
// TODO(petermarshall, 1112738): Add survey triggers here.
// These come from chrome/browser/ui/hats/hats_service.cc.
/** @type {!Map<!SDK.Issue.IssueCategory, string|null>} */
const issueSurveyTriggers = new Map([
[SDK.Issue.IssueCategory.CrossOriginEmbedderPolicy, null], [SDK.Issue.IssueCategory.MixedContent, null],
[SDK.Issue.IssueCategory.SameSiteCookie, 'devtools-issues-cookies-samesite'], [SDK.Issue.IssueCategory.HeavyAd, null],
[SDK.Issue.IssueCategory.ContentSecurityPolicy, null], [SDK.Issue.IssueCategory.Other, null]
]);
class IssueView extends UI.TreeOutline.TreeElement {
/**
*
* @param {!IssuesPaneImpl} parent
* @param {!AggregatedIssue} issue
* @param {!SDK.Issue.IssueDescription} description
*/
constructor(parent, issue, description) {
super();
this._parent = parent;
this._issue = issue;
/** @type {!SDK.Issue.IssueDescription} */
this._description = description;
this.toggleOnClick = true;
this.listItemElement.classList.add('issue');
this.childrenListElement.classList.add('body');
this._affectedResources = this._createAffectedResources();
/** @type {!Array<!AffectedResourcesView>} */
this._affectedResourceViews = [
new AffectedCookiesView(this, this._issue), new AffectedElementsView(this, this._issue),
new AffectedRequestsView(this, this._issue), new AffectedMixedContentView(this, this._issue),
new AffectedSourcesView(this, this._issue), new AffectedHeavyAdView(this, this._issue),
new AffectedDirectivesView(this, this._issue), new AffectedBlockedByResponseView(this, this._issue)
];
this._aggregatedIssuesCount = null;
this._hasBeenExpandedBefore = false;
}
/**
* @returns {string}
*/
getIssueTitle() {
return this._description.title;
}
/**
* @override
*/
onattach() {
this._appendHeader();
this._createBody();
this.appendChild(this._affectedResources);
for (const view of this._affectedResourceViews) {
this.appendAffectedResource(view);
view.update();
}
this._createReadMoreLinks();
this.updateAffectedResourceVisibility();
}
/**
* @param {!UI.TreeOutline.TreeElement} resource
*/
appendAffectedResource(resource) {
this._affectedResources.appendChild(resource);
}
_appendHeader() {
const header = document.createElement('div');
header.classList.add('header');
const icon = new Elements.Icon.Icon();
icon.data = {iconName: 'breaking_change_icon', color: '', width: '16px', height: '16px'};
icon.classList.add('breaking-change');
this._aggregatedIssuesCount = /** @type {!HTMLElement} */ (document.createElement('span'));
const countAdorner = Elements.Adorner.Adorner.create(this._aggregatedIssuesCount, 'countWrapper');
countAdorner.classList.add('aggregated-issues-count');
this._aggregatedIssuesCount.textContent = `${this._issue.getAggregatedIssuesCount()}`;
header.appendChild(icon);
header.appendChild(countAdorner);
const title = document.createElement('div');
title.classList.add('title');
title.textContent = this._description.title;
header.appendChild(title);
this.listItemElement.appendChild(header);
}
/**
* @override
*/
onexpand() {
const issueCategory = this._issue.getCategory().description;
Host.userMetrics.issuesPanelIssueExpanded(issueCategory);
if (!this._hasBeenExpandedBefore) {
this._hasBeenExpandedBefore = true;
for (const view of this._affectedResourceViews) {
view.expandIfOneResource();
}
}
}
_updateAggregatedIssuesCount() {
if (this._aggregatedIssuesCount) {
this._aggregatedIssuesCount.textContent = `${this._issue.getAggregatedIssuesCount()}`;
}
}
updateAffectedResourceVisibility() {
const noResources = this._affectedResourceViews.every(view => view.isEmpty());
this._affectedResources.hidden = noResources;
}
/**
*
* @returns {!UI.TreeOutline.TreeElement}
*/
_createAffectedResources() {
const wrapper = new UI.TreeOutline.TreeElement();
wrapper.setCollapsible(false);
wrapper.setExpandable(true);
wrapper.expand();
wrapper.selectable = false;
wrapper.listItemElement.classList.add('affected-resources-label');
wrapper.listItemElement.textContent = ls`Affected Resources`;
wrapper.childrenListElement.classList.add('affected-resources');
return wrapper;
}
_createBody() {
const messageElement = new UI.TreeOutline.TreeElement();
messageElement.setCollapsible(false);
messageElement.selectable = false;
const message = this._description.message();
messageElement.listItemElement.appendChild(message);
this.appendChild(messageElement);
}
_createReadMoreLinks() {
const linkWrapper = new UI.TreeOutline.TreeElement();
linkWrapper.setCollapsible(false);
linkWrapper.listItemElement.classList.add('link-wrapper');
const linkList = linkWrapper.listItemElement.createChild('ul', 'link-list');
for (const description of this._description.links) {
const link = UI.Fragment.html
`<a class="link devtools-link" role="link" tabindex="0" href=${description.link}>${
ls`Learn more: ${description.linkTitle}`}</a>`;
const linkIcon = new Elements.Icon.Icon();
linkIcon.data = {iconName: 'link_icon', color: 'var(--issue-link)', width: '16px', height: '16px'};
linkIcon.classList.add('link-icon');
link.prepend(linkIcon);
self.onInvokeElement(link, event => {
Host.userMetrics.issuesPanelResourceOpened(this._issue.getCategory(), AffectedItem.LearnMore);
const mainTarget = SDK.SDKModel.TargetManager.instance().mainTarget();
if (mainTarget) {
mainTarget.targetAgent().invoke_createTarget({url: description.link});
}
event.consume(true);
});
const linkListItem = linkList.createChild('li');
linkListItem.appendChild(link);
}
this.appendChild(linkWrapper);
const surveyTrigger = issueSurveyTriggers.get(this._issue.getCategory());
if (surveyTrigger) {
// This part of the UI is async so be careful relying on it being available.
const surveyLink = new IssueSurveyLink();
surveyLink.data = {
trigger: surveyTrigger,
canShowSurvey: Host.InspectorFrontendHost.InspectorFrontendHostInstance.canShowSurvey,
showSurvey: Host.InspectorFrontendHost.InspectorFrontendHostInstance.showSurvey
};
linkList.createChild('li').appendChild(surveyLink);
}
}
update() {
this._affectedResourceViews.forEach(view => view.update());
this.updateAffectedResourceVisibility();
this._updateAggregatedIssuesCount();
}
/**
* @param {(boolean|undefined)=} expand - Expands the issue if `true`, collapses if `false`, toggles collapse if undefined
*/
toggle(expand) {
if (expand || (expand === undefined && !this.expanded)) {
this.expand();
} else {
this.collapse();
}
}
}
/** @return {!Common.Settings.Setting<boolean>} */
export function getGroupIssuesByCategorySetting() {
return Common.Settings.Settings.instance().createSetting('groupIssuesByCategory', false);
}
export class IssuesPaneImpl extends UI.Widget.VBox {
constructor() {
super(true);
this.registerRequiredCSS('issues/issuesPane.css', {enableLegacyPatching: true});
this.contentElement.classList.add('issues-pane');
this._categoryViews = new Map();
this._issueViews = new Map();
this._showThirdPartyCheckbox = null;
const {toolbarContainer, updateToolbarIssuesCount} = this._createToolbars();
this._issuesToolbarContainer = toolbarContainer;
this._updateToolbarIssuesCount = updateToolbarIssuesCount;
this._issuesTree = new UI.TreeOutline.TreeOutlineInShadow();
this._issuesTree.registerRequiredCSS('issues/issuesTree.css', {enableLegacyPatching: true});
this._issuesTree.setShowSelectionOnKeyboardFocus(true);
this._issuesTree.contentElement.classList.add('issues');
this.contentElement.appendChild(this._issuesTree.element);
this._noIssuesMessageDiv = document.createElement('div');
this._noIssuesMessageDiv.classList.add('issues-pane-no-issues');
this.contentElement.appendChild(this._noIssuesMessageDiv);
/** @type {!BrowserSDK.IssuesManager.IssuesManager} */
this._issuesManager = BrowserSDK.IssuesManager.IssuesManager.instance();
/** @type {!IssueAggregator} */
this._aggregator = new IssueAggregator(this._issuesManager);
this._aggregator.addEventListener(IssueAggregatorEvents.AggregatedIssueUpdated, this._issueUpdated, this);
this._aggregator.addEventListener(IssueAggregatorEvents.FullUpdateRequired, this._fullUpdate, this);
for (const issue of this._aggregator.aggregatedIssues()) {
this._updateIssueView(issue);
}
this._issuesManager.addEventListener(BrowserSDK.IssuesManager.Events.IssuesCountUpdated, this._updateCounts, this);
this._updateCounts();
}
/**
* @override
* @return {!Array<!Element>}
*/
elementsToRestoreScrollPositionsFor() {
return [this._issuesTree.element];
}
/**
* @returns {!{toolbarContainer: !Element, updateToolbarIssuesCount: function(number):void}}
*/
_createToolbars() {
const toolbarContainer = this.contentElement.createChild('div', 'issues-toolbar-container');
new UI.Toolbar.Toolbar('issues-toolbar-left', toolbarContainer);
const rightToolbar = new UI.Toolbar.Toolbar('issues-toolbar-right', toolbarContainer);
const groupByCategorySetting = getGroupIssuesByCategorySetting();
const groupByCategoryCheckbox = new UI.Toolbar.ToolbarSettingCheckbox(
groupByCategorySetting, ls`Group displayed issues under associated categories`, ls`Group by category`);
// Hide the option to toggle category grouping for now.
groupByCategoryCheckbox.setVisible(false);
rightToolbar.appendToolbarItem(groupByCategoryCheckbox);
groupByCategorySetting.addChangeListener(() => {
this._fullUpdate();
});
const thirdPartySetting = SDK.Issue.getShowThirdPartyIssuesSetting();
this._showThirdPartyCheckbox = new UI.Toolbar.ToolbarSettingCheckbox(
thirdPartySetting, ls`Include cookie Issues caused by third-party sites`,
ls`Include third-party cookie issues`);
rightToolbar.appendToolbarItem(this._showThirdPartyCheckbox);
this.setDefaultFocusedElement(this._showThirdPartyCheckbox.inputElement);
rightToolbar.appendSeparator();
const toolbarWarnings = document.createElement('div');
toolbarWarnings.classList.add('toolbar-warnings');
const breakingChangeIcon = new Elements.Icon.Icon();
breakingChangeIcon.data = {iconName: 'breaking_change_icon', color: '', width: '16px', height: '16px'};
breakingChangeIcon.classList.add('breaking-change');
toolbarWarnings.appendChild(breakingChangeIcon);
const toolbarIssuesCount = toolbarWarnings.createChild('span', 'warnings-count-label');
const toolbarIssuesItem = new UI.Toolbar.ToolbarItem(toolbarWarnings);
rightToolbar.appendToolbarItem(toolbarIssuesItem);
/** @param {number} count */
const updateToolbarIssuesCount = count => {
toolbarIssuesCount.textContent = `${count}`;
if (count === 1) {
toolbarIssuesItem.setTitle(ls`Issues pertaining to ${count} operation detected.`);
} else {
toolbarIssuesItem.setTitle(ls`Issues pertaining to ${count} operations detected.`);
}
};
return {toolbarContainer, updateToolbarIssuesCount};
}
/**
* @param {!Common.EventTarget.EventTargetEvent} event
*/
_issueUpdated(event) {
const issue = /** @type {!AggregatedIssue} */ (event.data);
this._updateIssueView(issue);
}
/**
* @param {!AggregatedIssue} issue
*/
_updateIssueView(issue) {
if (!this._issueViews.has(issue.code())) {
let description = issue.getDescription();
if (!description) {
console.warn('Could not find description for issue code:', issue.code());
return;
}
if ('file' in description) {
// TODO(crbug.com/1011811): Remove casts once closure is gone. TypeScript can infer the type variant.
description =
createIssueDescriptionFromMarkdown(/** @type {!SDK.Issue.MarkdownIssueDescription} */ (description));
}
const view = new IssueView(this, issue, /** @type {!SDK.Issue.IssueDescription} */ (description));
this._issueViews.set(issue.code(), view);
const parent = this._getIssueViewParent(issue);
parent.appendChild(view, (a, b) => {
if (a instanceof IssueView && b instanceof IssueView) {
return a.getIssueTitle().localeCompare(b.getIssueTitle());
}
console.error('The issues tree should only contain IssueView objects as direct children');
return 0;
});
}
this._issueViews.get(issue.code()).update();
this._updateCounts();
}
/**
* @param {!AggregatedIssue} issue
* @returns {!UI.TreeOutline.TreeOutline | !UI.TreeOutline.TreeElement}
*/
_getIssueViewParent(issue) {
if (!getGroupIssuesByCategorySetting().get()) {
return this._issuesTree;
}
const category = issue.getCategory();
const view = this._categoryViews.get(category);
if (view) {
return view;
}
const newView = new IssueCategoryView(category);
this._issuesTree.appendChild(newView, (a, b) => {
if (a instanceof IssueCategoryView && b instanceof IssueCategoryView) {
return a.getCategoryName().localeCompare(b.getCategoryName());
}
return 0;
});
this._categoryViews.set(category, newView);
return newView;
}
/**
* @param {!Map<*, !UI.TreeOutline.TreeElement>} views
*/
_clearViews(views) {
for (const view of views.values()) {
view.parent && view.parent.removeChild(view);
}
views.clear();
}
_fullUpdate() {
this._clearViews(this._categoryViews);
this._clearViews(this._issueViews);
if (this._aggregator) {
for (const issue of this._aggregator.aggregatedIssues()) {
this._updateIssueView(issue);
}
}
this._updateCounts();
}
_updateCounts() {
const count = this._issuesManager.numberOfIssues();
this._updateToolbarIssuesCount(count);
this._showIssuesTreeOrNoIssuesDetectedMessage(count);
}
/**
* @param {number} issuesCount
*/
_showIssuesTreeOrNoIssuesDetectedMessage(issuesCount) {
if (issuesCount > 0) {
this._issuesTree.element.hidden = false;
this._noIssuesMessageDiv.style.display = 'none';
const firstChild = this._issuesTree.firstChild();
if (firstChild) {
firstChild.select(/** omitFocus= */ true);
this.setDefaultFocusedElement(firstChild.listItemElement);
}
} else {
this._issuesTree.element.hidden = true;
if (this._showThirdPartyCheckbox) {
this.setDefaultFocusedElement(this._showThirdPartyCheckbox.inputElement);
}
// We alreay know that issesCount is zero here.
const hasOnlyThirdPartyIssues = this._issuesManager.numberOfAllStoredIssues() > 0;
this._noIssuesMessageDiv.textContent =
hasOnlyThirdPartyIssues ? ls`Only third-party cookie issues detected so far` : ls`No issues detected so far`;
this._noIssuesMessageDiv.style.display = 'flex';
}
}
/**
* @param {string} code
*/
revealByCode(code) {
const issueView = this._issueViews.get(code);
if (issueView) {
issueView.expand();
issueView.reveal();
}
}
}