chrome-devtools-frontend
Version:
Chrome DevTools UI
789 lines (704 loc) • 31.6 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_underscored_properties */
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 i18n from '../i18n/i18n.js';
import * as Network from '../network/network.js';
import * as Platform from '../platform/platform.js';
import * as SDK from '../sdk/sdk.js';
import * as WebComponents from '../ui/components/components.js';
import * as UI from '../ui/ui.js';
import {AffectedCookiesView} from './AffectedCookiesView.js';
import {AffectedElementsView} from './AffectedElementsView.js';
import {AffectedElementsWithLowContrastView} from './AffectedElementsWithLowContrastView.js';
import {AffectedItem, AffectedResourcesView, extractShortPath} from './AffectedResourcesView.js';
import {AffectedSharedArrayBufferIssueDetailsView} from './AffectedSharedArrayBufferIssueDetailsView.js';
import {AffectedTrustedWebActivityIssueDetailsView} from './AffectedTrustedWebActivityIssueDetailsView.js';
import {CorsIssueDetailsView} from './CorsIssueDetailsView.js';
import {AggregatedIssue} from './IssueAggregator.js'; // eslint-disable-line no-unused-vars
import {IssueDescription} from './MarkdownIssueDescription.js'; // eslint-disable-line no-unused-vars
export const UIStrings = {
/**
*@description Noun, singular. Label for a column or field containing the name of an entity.
*/
name: 'Name',
/**
*@description Singular label for number of affected directive resource indication in issue view
*/
directive: 'directive',
/**
*@description Plural label for number of affected directive resource indication in issue view
*/
directives: 'directives',
/**
*@description Indicates that a CSP error should be treated as a warning
*/
reportonly: 'report-only',
/**
*@description The kind of resolution for a mixed content issue
*/
blocked: 'blocked',
/**
*@description Tooltip for button linking to the Elements panel
*/
clickToRevealTheViolatingDomNode: 'Click to reveal the violating DOM node in the Elements panel',
/**
*@description Header for the section listing affected directives
*/
directiveC: 'Directive',
/**
*@description Label for the column in the element list in the CSS Overview report
*/
element: 'Element',
/**
*@description Header for the source location column
*/
sourceLocation: 'Source Location',
/**
*@description Text for the status of something
*/
status: 'Status',
/**
*@description Text that refers to the resources of the web page
*/
resourceC: 'Resource',
/**
*@description A tag of Enable Local Overrides setting that can be searched in the command menu
*/
request: 'request',
/**
*@description Label for number of affected resources indication in issue view
*/
requests: 'requests',
/**
*@description Singular label for number of affected source resource indication in issue view
*/
source: 'source',
/**
*@description Plural label for number of affected source resource indication in issue view
*/
sources: 'sources',
/**
*@description Label for number of affected resources indication in issue view
*/
resource: 'resource',
/**
*@description Label for number of affected resources indication in issue view
*/
resources: 'resources',
/**
*@description Label for mixed content issue's restriction status
*/
restrictionStatus: 'Restriction Status',
/**
*@description Title for a column in an Heavy Ads issue view
*/
limitExceeded: 'Limit exceeded',
/**
*@description Title for a column in an Heavy Ads issue view
*/
resolutionStatus: 'Resolution Status',
/**
*@description Title for a column in an Heavy Ads issue view
*/
frameUrl: 'Frame URL',
/**
* @description When there is a Heavy Ad, the browser can choose to deal with it in different ways.
* This string indicates that the ad was bad enough that it was removed.
*/
removed: 'Removed',
/**
* @description When there is a Heavy Ad, the browser can choose to deal with it in different ways.
* This string indicates that the ad was only warned, and not removed.
*/
warned: 'Warned',
/**
*@description Reason for a Heavy Ad being flagged in issue view. The Ad has been flagged as a
*Heavy Ad because it exceeded the set limit for peak CPU usage, e.g. it blocked the main thread
*for more than 15 seconds in any 30-second window.
*/
cpuPeakLimit: 'CPU peak limit',
/**
*@description Reason for a Heavy Ad being flagged in issue view
*/
cpuTotalLimit: 'CPU total limit',
/**
*@description Reason for a Heavy Ad being flagged in issue view
*/
networkLimit: 'Network limit',
/**
*@description Label for the category of affected resources in issue view
*/
requestC: 'Request',
/**
*@description Title for a column in an issue view
*/
parentFrame: 'Parent Frame',
/**
*@description Title for a column in an issue view
*/
blockedResource: 'Blocked Resource',
/**
*@description Header for the section listing affected resources
*/
affectedResources: 'Affected Resources',
/**
*@description Title for a link to further information in issue view
*@example {SameSite Cookies Explained} PH1
*/
learnMoreS: 'Learn more: {PH1}',
/**
*@description Link text for opening a survey in the expended view of an Issue in the issue view
*/
isThisIssueMessageHelpfulToYou: 'Is this issue message helpful to you?',
};
const str_ = i18n.i18n.registerUIStrings('issues/IssueView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
class AffectedDirectivesView extends AffectedResourcesView {
_issue: AggregatedIssue;
constructor(parent: IssueView, issue: AggregatedIssue) {
super(parent, {singular: i18nString(UIStrings.directive), plural: i18nString(UIStrings.directives)});
this._issue = issue;
}
_appendStatus(element: Element, isReportOnly: boolean): void {
const status = document.createElement('td');
if (isReportOnly) {
status.classList.add('affected-resource-report-only-status');
status.textContent = i18nString(UIStrings.reportonly);
} else {
status.classList.add('affected-resource-blocked-status');
status.textContent = i18nString(UIStrings.blocked);
}
element.appendChild(status);
}
_appendViolatedDirective(element: Element, directive: string): void {
const violatedDirective = document.createElement('td');
violatedDirective.textContent = directive;
element.appendChild(violatedDirective);
}
_appendBlockedURL(element: Element, url: string): void {
const info = document.createElement('td');
info.classList.add('affected-resource-directive-info');
info.textContent = url;
element.appendChild(info);
}
_appendBlockedElement(element: Element, nodeId: number|undefined, model: SDK.IssuesModel.IssuesModel): void {
const elementsPanelLinkComponent = new Elements.ElementsPanelLink.ElementsPanelLink();
if (nodeId) {
const violatingNodeId = nodeId;
UI.Tooltip.Tooltip.install(elementsPanelLinkComponent, i18nString(UIStrings.clickToRevealTheViolatingDomNode));
const onElementRevealIconClick: (arg0?: Event|undefined) => void = (): void => {
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);
}
};
const onElementRevealIconMouseEnter: (arg0?: Event|undefined) => void = (): void => {
const target = model.getTargetIfNotDisposed();
if (target) {
const deferredDOMNode = new SDK.DOMModel.DeferredDOMNode(target, violatingNodeId);
if (deferredDOMNode) {
deferredDOMNode.highlight();
}
}
};
const onElementRevealIconMouseLeave: (arg0?: Event|undefined) => void = (): void => {
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);
}
_appendAffectedContentSecurityPolicyDetails(
cspIssues: Iterable<SDK.ContentSecurityPolicyIssue.ContentSecurityPolicyIssue>): void {
const header = document.createElement('tr');
if (this._issue.code() === SDK.ContentSecurityPolicyIssue.inlineViolationCode) {
this.appendColumnTitle(header, i18nString(UIStrings.directiveC));
this.appendColumnTitle(header, i18nString(UIStrings.element));
this.appendColumnTitle(header, i18nString(UIStrings.sourceLocation));
this.appendColumnTitle(header, i18nString(UIStrings.status));
} else if (this._issue.code() === SDK.ContentSecurityPolicyIssue.urlViolationCode) {
this.appendColumnTitle(header, i18nString(UIStrings.resourceC), 'affected-resource-directive-info-header');
this.appendColumnTitle(header, i18nString(UIStrings.status));
this.appendColumnTitle(header, i18nString(UIStrings.directiveC));
this.appendColumnTitle(header, i18nString(UIStrings.sourceLocation));
} else if (this._issue.code() === SDK.ContentSecurityPolicyIssue.evalViolationCode) {
this.appendColumnTitle(header, i18nString(UIStrings.sourceLocation));
this.appendColumnTitle(header, i18nString(UIStrings.directiveC));
this.appendColumnTitle(header, i18nString(UIStrings.status));
} else if (this._issue.code() === SDK.ContentSecurityPolicyIssue.trustedTypesSinkViolationCode) {
this.appendColumnTitle(header, i18nString(UIStrings.sourceLocation));
this.appendColumnTitle(header, i18nString(UIStrings.status));
} else if (this._issue.code() === SDK.ContentSecurityPolicyIssue.trustedTypesPolicyViolationCode) {
this.appendColumnTitle(header, i18nString(UIStrings.sourceLocation));
this.appendColumnTitle(header, i18nString(UIStrings.directiveC));
this.appendColumnTitle(header, i18nString(UIStrings.status));
} else {
this.updateAffectedResourceCount(0);
return;
}
this.affectedResources.appendChild(header);
let count = 0;
for (const cspIssue of cspIssues) {
count++;
this._appendAffectedContentSecurityPolicyDetail(cspIssue);
}
this.updateAffectedResourceCount(count);
}
_appendAffectedContentSecurityPolicyDetail(cspIssue: SDK.ContentSecurityPolicyIssue.ContentSecurityPolicyIssue):
void {
const element = document.createElement('tr');
element.classList.add('affected-resource-directive');
const cspIssueDetails = cspIssue.details();
const model = cspIssue.model();
const maybeTarget = cspIssue.model()?.getTargetIfNotDisposed();
if (this._issue.code() === SDK.ContentSecurityPolicyIssue.inlineViolationCode && model) {
this._appendViolatedDirective(element, cspIssueDetails.violatedDirective);
this._appendBlockedElement(element, cspIssueDetails.violatingNodeId, model);
this.appendSourceLocation(element, cspIssueDetails.sourceCodeLocation, maybeTarget);
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, maybeTarget);
} else if (this._issue.code() === SDK.ContentSecurityPolicyIssue.evalViolationCode) {
this.appendSourceLocation(element, cspIssueDetails.sourceCodeLocation, maybeTarget);
this._appendViolatedDirective(element, cspIssueDetails.violatedDirective);
this._appendStatus(element, cspIssueDetails.isReportOnly);
} else if (this._issue.code() === SDK.ContentSecurityPolicyIssue.trustedTypesSinkViolationCode) {
this.appendSourceLocation(element, cspIssueDetails.sourceCodeLocation, maybeTarget);
this._appendStatus(element, cspIssueDetails.isReportOnly);
} else if (this._issue.code() === SDK.ContentSecurityPolicyIssue.trustedTypesPolicyViolationCode) {
this.appendSourceLocation(element, cspIssueDetails.sourceCodeLocation, maybeTarget);
this._appendViolatedDirective(element, cspIssueDetails.violatedDirective);
this._appendStatus(element, cspIssueDetails.isReportOnly);
} else {
return;
}
this.affectedResources.appendChild(element);
}
update(): void {
this.clear();
this._appendAffectedContentSecurityPolicyDetails(this._issue.getCspIssues());
}
}
class AffectedRequestsView extends AffectedResourcesView {
_issue: SDK.Issue.Issue;
constructor(parent: IssueView, issue: SDK.Issue.Issue) {
super(parent, {singular: i18nString(UIStrings.request), plural: i18nString(UIStrings.requests)});
this._issue = issue;
}
_appendAffectedRequests(affectedRequests: Iterable<Protocol.Audits.AffectedRequest>): void {
let count = 0;
for (const affectedRequest of affectedRequests) {
for (const request of this.resolveRequestId(affectedRequest.requestId)) {
count++;
this._appendNetworkRequest(request);
}
}
this.updateAffectedResourceCount(count);
}
_appendNetworkRequest(request: SDK.NetworkRequest.NetworkRequest): void {
const nameText = Platform.StringUtilities.trimMiddle(request.name(), 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);
}
update(): void {
this.clear();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for (const unused of this._issue.getBlockedByResponseDetails()) {
// 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 {
_issue: SDK.Issue.Issue;
constructor(parent: IssueView, issue: SDK.Issue.Issue) {
super(parent, {singular: i18nString(UIStrings.source), plural: i18nString(UIStrings.sources)});
this._issue = issue;
}
_appendAffectedSources(affectedSources: Iterable<SDK.Issue.AffectedSource>): void {
let count = 0;
for (const source of affectedSources) {
this._appendAffectedSource(source);
count++;
}
this.updateAffectedResourceCount(count);
}
_appendAffectedSource({url, lineNumber, columnNumber}: SDK.Issue.AffectedSource): void {
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 = ({columnNumber, lineNumber, tabStop: true} as Components.Linkifier.LinkifyURLOptions);
// 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);
}
update(): void {
this.clear();
this._appendAffectedSources(this._issue.sources());
}
}
const issueTypeToNetworkHeaderMap = new Map<symbol, string>([
[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 {
_issue: AggregatedIssue;
constructor(parent: IssueView, issue: AggregatedIssue) {
super(parent, {singular: i18nString(UIStrings.resource), plural: i18nString(UIStrings.resources)});
this._issue = issue;
}
_appendAffectedMixedContentDetails(mixedContentIssues: Iterable<SDK.MixedContentIssue.MixedContentIssue>): void {
const header = document.createElement('tr');
this.appendColumnTitle(header, i18nString(UIStrings.name));
this.appendColumnTitle(header, i18nString(UIStrings.restrictionStatus));
this.affectedResources.appendChild(header);
let count = 0;
for (const issue of mixedContentIssues) {
const details = issue.getDetails();
if (details.request) {
this.resolveRequestId(details.request.requestId).forEach(networkRequest => {
this.appendAffectedMixedContent(details, networkRequest);
count++;
});
} else {
this.appendAffectedMixedContent(details);
count++;
}
}
this.updateAffectedResourceCount(count);
}
appendAffectedMixedContent(
mixedContent: Protocol.Audits.MixedContentIssueDetails,
maybeRequest: SDK.NetworkRequest.NetworkRequest|null = null): void {
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);
}
update(): void {
this.clear();
this._appendAffectedMixedContentDetails(this._issue.getMixedContentIssues());
}
}
class AffectedHeavyAdView extends AffectedResourcesView {
_issue: SDK.Issue.Issue;
constructor(parent: IssueView, issue: SDK.Issue.Issue) {
super(parent, {singular: i18nString(UIStrings.resource), plural: i18nString(UIStrings.resources)});
this._issue = issue;
}
_appendAffectedHeavyAds(heavyAds: Iterable<Protocol.Audits.HeavyAdIssueDetails>): void {
const header = document.createElement('tr');
this.appendColumnTitle(header, i18nString(UIStrings.limitExceeded));
this.appendColumnTitle(header, i18nString(UIStrings.resolutionStatus));
this.appendColumnTitle(header, i18nString(UIStrings.frameUrl));
this.affectedResources.appendChild(header);
let count = 0;
for (const heavyAd of heavyAds) {
this._appendAffectedHeavyAd(heavyAd);
count++;
}
this.updateAffectedResourceCount(count);
}
_statusToString(status: Protocol.Audits.HeavyAdResolutionStatus): string {
switch (status) {
case Protocol.Audits.HeavyAdResolutionStatus.HeavyAdBlocked:
return i18nString(UIStrings.removed);
case Protocol.Audits.HeavyAdResolutionStatus.HeavyAdWarning:
return i18nString(UIStrings.warned);
}
return '';
}
_limitToString(status: Protocol.Audits.HeavyAdReason): string {
switch (status) {
case Protocol.Audits.HeavyAdReason.CpuPeakLimit:
return i18nString(UIStrings.cpuPeakLimit);
case Protocol.Audits.HeavyAdReason.CpuTotalLimit:
return i18nString(UIStrings.cpuTotalLimit);
case Protocol.Audits.HeavyAdReason.NetworkTotalLimit:
return i18nString(UIStrings.networkLimit);
}
return '';
}
_appendAffectedHeavyAd(heavyAd: Protocol.Audits.HeavyAdIssueDetails): void {
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);
}
update(): void {
this.clear();
this._appendAffectedHeavyAds(this._issue.heavyAds());
}
}
class AffectedBlockedByResponseView extends AffectedResourcesView {
_issue: SDK.Issue.Issue;
constructor(parent: IssueView, issue: SDK.Issue.Issue) {
super(parent, {singular: i18nString(UIStrings.request), plural: i18nString(UIStrings.requests)});
this._issue = issue;
}
_appendDetails(details: Iterable<Protocol.Audits.BlockedByResponseIssueDetails>): void {
const header = document.createElement('tr');
this.appendColumnTitle(header, i18nString(UIStrings.requestC));
this.appendColumnTitle(header, i18nString(UIStrings.parentFrame));
this.appendColumnTitle(header, i18nString(UIStrings.blockedResource));
this.affectedResources.appendChild(header);
let count = 0;
for (const detail of details) {
this._appendDetail(detail);
count++;
}
this.updateAffectedResourceCount(count);
}
_appendDetail(details: Protocol.Audits.BlockedByResponseIssueDetails): void {
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);
}
update(): void {
this.clear();
this._appendDetails(this._issue.getBlockedByResponseDetails());
}
}
// These come from chrome/browser/ui/hats/hats_service.cc.
const issueSurveyTriggers = new Map<symbol, string|null>([
[SDK.Issue.IssueCategory.CrossOriginEmbedderPolicy, 'devtools-issues-coep'],
[SDK.Issue.IssueCategory.MixedContent, 'devtools-issues-mixed-content'],
[SDK.Issue.IssueCategory.SameSiteCookie, 'devtools-issues-cookies-samesite'],
[SDK.Issue.IssueCategory.HeavyAd, 'devtools-issues-heavy-ad'],
[SDK.Issue.IssueCategory.ContentSecurityPolicy, 'devtools-issues-csp'],
[SDK.Issue.IssueCategory.Other, null],
]);
export class IssueView extends UI.TreeOutline.TreeElement {
_parent: UI.Widget.VBox;
_issue: AggregatedIssue;
_description: IssueDescription;
toggleOnClick: boolean;
affectedResources: UI.TreeOutline.TreeElement;
_affectedResourceViews: AffectedResourcesView[];
_aggregatedIssuesCount: HTMLElement|null;
_hasBeenExpandedBefore: boolean;
constructor(parent: UI.Widget.VBox, issue: AggregatedIssue, description: IssueDescription) {
super();
this._parent = parent;
this._issue = issue;
this._description = description;
this.toggleOnClick = true;
this.listItemElement.classList.add('issue');
this.childrenListElement.classList.add('body');
this.affectedResources = this._createAffectedResources();
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),
new AffectedSharedArrayBufferIssueDetailsView(this, this._issue),
new AffectedElementsWithLowContrastView(this, this._issue),
new AffectedTrustedWebActivityIssueDetailsView(this, this._issue),
new CorsIssueDetailsView(this, this._issue),
];
this._aggregatedIssuesCount = null;
this._hasBeenExpandedBefore = false;
}
getIssueTitle(): string {
return this._description.title;
}
onattach(): void {
this._appendHeader();
this._createBody();
this.appendChild(this.affectedResources);
for (const view of this._affectedResourceViews) {
this.appendAffectedResource(view);
view.update();
}
this._createReadMoreLinks();
this.updateAffectedResourceVisibility();
}
appendAffectedResource(resource: UI.TreeOutline.TreeElement): void {
this.affectedResources.appendChild(resource);
}
_appendHeader(): void {
const header = document.createElement('div');
header.classList.add('header');
const icon = new WebComponents.Icon.Icon();
icon.data = {iconName: 'breaking_change_icon', color: '', width: '16px', height: '16px'};
icon.classList.add('breaking-change');
this._aggregatedIssuesCount = (document.createElement('span') as HTMLElement);
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);
}
onexpand(): void {
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(): void {
if (this._aggregatedIssuesCount) {
this._aggregatedIssuesCount.textContent = `${this._issue.getAggregatedIssuesCount()}`;
}
}
updateAffectedResourceVisibility(): void {
const noResources = this._affectedResourceViews.every(view => view.isEmpty());
this.affectedResources.hidden = noResources;
}
_createAffectedResources(): UI.TreeOutline.TreeElement {
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 = i18nString(UIStrings.affectedResources);
wrapper.childrenListElement.classList.add('affected-resources');
return wrapper;
}
_createBody(): void {
const messageElement = new UI.TreeOutline.TreeElement();
messageElement.setCollapsible(false);
messageElement.selectable = false;
messageElement.listItemElement.appendChild(this._description.view);
this.appendChild(messageElement);
}
_createReadMoreLinks(): void {
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}>${
i18nString(UIStrings.learnMoreS, {PH1: description.linkTitle})}</a>`;
const linkIcon = new WebComponents.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 WebComponents.SurveyLink.SurveyLink();
surveyLink.data = {
trigger: surveyTrigger,
promptText: i18nString(UIStrings.isThisIssueMessageHelpfulToYou),
canShowSurvey: Host.InspectorFrontendHost.InspectorFrontendHostInstance.canShowSurvey,
showSurvey: Host.InspectorFrontendHost.InspectorFrontendHostInstance.showSurvey,
};
linkList.createChild('li').appendChild(surveyLink);
}
}
update(): void {
this._affectedResourceViews.forEach(view => view.update());
this.updateAffectedResourceVisibility();
this._updateAggregatedIssuesCount();
}
toggle(expand?: boolean): void {
if (expand || (expand === undefined && !this.expanded)) {
this.expand();
} else {
this.collapse();
}
}
}