chrome-devtools-frontend
Version:
Chrome DevTools UI
473 lines (424 loc) • 19.1 kB
text/typescript
// Copyright 2021 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 '../../core/common/common.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as Root from '../../core/root/root.js';
import * as IssuesManager from '../../models/issues_manager/issues_manager.js';
import * as IssueCounter from '../../ui/components/issue_counter/issue_counter.js';
import * as UI from '../../ui/legacy/legacy.js';
import {HiddenIssuesRow} from './HiddenIssuesRow.js';
import issuesPaneStyles from './issuesPane.css.js';
import issuesTreeStyles from './issuesTree.css.js';
import type {AggregatedIssue, AggregationKey} from './IssueAggregator.js';
import {Events as IssueAggregatorEvents, IssueAggregator} from './IssueAggregator.js';
import {IssueView} from './IssueView.js';
import {IssueKindView, getGroupIssuesByKindSetting, issueKindViewSortPriority} from './IssueKindView.js';
const UIStrings = {
/**
* @description Category title for a group of cross origin embedder policy (COEP) issues
*/
crossOriginEmbedderPolicy: 'Cross Origin Embedder Policy',
/**
* @description Category title for a group of mixed content issues
*/
mixedContent: 'Mixed Content',
/**
* @description Category title for a group of SameSite cookie issues
*/
samesiteCookie: 'SameSite Cookie',
/**
* @description Category title for a group of heavy ads issues
*/
heavyAds: 'Heavy Ads',
/**
* @description Category title for a group of content security policy (CSP) issues
*/
contentSecurityPolicy: 'Content Security Policy',
/**
* @description Category title for a group of trusted web activity issues
*/
trustedWebActivity: 'Trusted Web Activity',
/**
* @description Text for other types of items
*/
other: 'Other',
/**
* @description Category title for the different 'low text contrast' issues. Low text contrast refers
* to the difference between the color of a text and the background color where that text
* appears.
*/
lowTextContrast: 'Low Text Contrast',
/**
* @description Category title for the different 'Cross-Origin Resource Sharing' (CORS) issues. CORS
* refers to one origin (e.g 'a.com') loading resources from another origin (e.g. 'b.com').
*/
cors: 'Cross Origin Resource Sharing',
/**
* @description Title for a checkbox which toggles grouping by category in the issues tab
*/
groupDisplayedIssuesUnder: 'Group displayed issues under associated categories',
/**
* @description Label for a checkbox which toggles grouping by category in the issues tab
*/
groupByCategory: 'Group by category',
/**
* @description Title for a checkbox which toggles grouping by kind in the issues tab
*/
groupDisplayedIssuesUnderKind: 'Group displayed issues as Page errors, Breaking changes and Improvements',
/**
* @description Label for a checkbox which toggles grouping by kind in the issues tab
*/
groupByKind: 'Group by kind',
/**
* @description Title for a checkbox. Whether the issues tab should include third-party issues or not.
*/
includeCookieIssuesCausedBy: 'Include cookie Issues caused by third-party sites',
/**
* @description Label for a checkbox. Whether the issues tab should include third-party issues or not.
*/
includeThirdpartyCookieIssues: 'Include third-party cookie issues',
/**
* @description Label on the issues tab
*/
onlyThirdpartyCookieIssues: 'Only third-party cookie issues detected so far',
/**
* @description Label in the issues panel
*/
noIssuesDetectedSoFar: 'No issues detected so far',
/**
* @description Category title for the different 'Attribution Reporting API' issues. The
* Attribution Reporting API is a newly proposed web API (see https://github.com/WICG/conversion-measurement-api).
*/
attributionReporting: 'Attribution Reporting `API`',
/**
* @description Category title for the different 'Quirks Mode' issues. Quirks Mode refers
* to the legacy browser modes that displays web content according to outdated
* browser behaviors.
*/
quirksMode: 'Quirks Mode',
/**
* @description Category title for the different 'Generic' issues.
*/
generic: 'Generic',
};
const str_ = i18n.i18n.registerUIStrings('panels/issues/IssuesPane.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
class IssueCategoryView extends UI.TreeOutline.TreeElement {
private category: IssuesManager.Issue.IssueCategory;
private issues: AggregatedIssue[];
constructor(category: IssuesManager.Issue.IssueCategory) {
super();
this.category = category;
this.issues = [];
this.toggleOnClick = true;
this.listItemElement.classList.add('issue-category');
this.childrenListElement.classList.add('issue-category-body');
}
getCategoryName(): string {
switch (this.category) {
case IssuesManager.Issue.IssueCategory.CrossOriginEmbedderPolicy:
return i18nString(UIStrings.crossOriginEmbedderPolicy);
case IssuesManager.Issue.IssueCategory.MixedContent:
return i18nString(UIStrings.mixedContent);
case IssuesManager.Issue.IssueCategory.SameSiteCookie:
return i18nString(UIStrings.samesiteCookie);
case IssuesManager.Issue.IssueCategory.HeavyAd:
return i18nString(UIStrings.heavyAds);
case IssuesManager.Issue.IssueCategory.ContentSecurityPolicy:
return i18nString(UIStrings.contentSecurityPolicy);
case IssuesManager.Issue.IssueCategory.TrustedWebActivity:
return i18nString(UIStrings.trustedWebActivity);
case IssuesManager.Issue.IssueCategory.LowTextContrast:
return i18nString(UIStrings.lowTextContrast);
case IssuesManager.Issue.IssueCategory.Cors:
return i18nString(UIStrings.cors);
case IssuesManager.Issue.IssueCategory.AttributionReporting:
return i18nString(UIStrings.attributionReporting);
case IssuesManager.Issue.IssueCategory.QuirksMode:
return i18nString(UIStrings.quirksMode);
case IssuesManager.Issue.IssueCategory.Generic:
return i18nString(UIStrings.generic);
case IssuesManager.Issue.IssueCategory.Other:
return i18nString(UIStrings.other);
}
}
onattach(): void {
this.appendHeader();
}
private appendHeader(): void {
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);
}
}
export function getGroupIssuesByCategorySetting(): Common.Settings.Setting<boolean> {
return Common.Settings.Settings.instance().createSetting('groupIssuesByCategory', false);
}
let issuesPaneInstance: IssuesPane;
export class IssuesPane extends UI.Widget.VBox {
private categoryViews: Map<IssuesManager.Issue.IssueCategory, IssueCategoryView>;
private issueViews: Map<AggregationKey, IssueView>;
private kindViews: Map<IssuesManager.Issue.IssueKind, IssueKindView>;
private showThirdPartyCheckbox: UI.Toolbar.ToolbarSettingCheckbox|null;
private issuesTree: UI.TreeOutline.TreeOutlineInShadow;
private hiddenIssuesRow: HiddenIssuesRow;
private noIssuesMessageDiv: HTMLDivElement;
private issuesManager: IssuesManager.IssuesManager.IssuesManager;
private aggregator: IssueAggregator;
private issueViewUpdatePromise: Promise<void> = Promise.resolve();
private constructor() {
super(true);
this.contentElement.classList.add('issues-pane');
this.categoryViews = new Map();
this.kindViews = new Map();
this.issueViews = new Map();
this.showThirdPartyCheckbox = null;
this.createToolbars();
this.issuesTree = new UI.TreeOutline.TreeOutlineInShadow();
this.issuesTree.setShowSelectionOnKeyboardFocus(true);
this.issuesTree.contentElement.classList.add('issues');
this.contentElement.appendChild(this.issuesTree.element);
this.hiddenIssuesRow = new HiddenIssuesRow();
this.issuesTree.appendChild(this.hiddenIssuesRow);
this.noIssuesMessageDiv = document.createElement('div');
this.noIssuesMessageDiv.classList.add('issues-pane-no-issues');
this.contentElement.appendChild(this.noIssuesMessageDiv);
this.issuesManager = IssuesManager.IssuesManager.IssuesManager.instance();
this.aggregator = new IssueAggregator(this.issuesManager);
this.aggregator.addEventListener(IssueAggregatorEvents.AggregatedIssueUpdated, this.issueUpdated, this);
this.aggregator.addEventListener(IssueAggregatorEvents.FullUpdateRequired, this.onFullUpdate, this);
this.hiddenIssuesRow.hidden = this.issuesManager.numberOfHiddenIssues() === 0;
this.onFullUpdate();
this.issuesManager.addEventListener(IssuesManager.IssuesManager.Events.IssuesCountUpdated, this.updateCounts, this);
}
static instance(opts: {forceNew: boolean|null} = {forceNew: null}): IssuesPane {
const {forceNew} = opts;
if (!issuesPaneInstance || forceNew) {
issuesPaneInstance = new IssuesPane();
}
return issuesPaneInstance;
}
elementsToRestoreScrollPositionsFor(): Element[] {
return [this.issuesTree.element];
}
private createToolbars(): {toolbarContainer: Element} {
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, i18nString(UIStrings.groupDisplayedIssuesUnder), i18nString(UIStrings.groupByCategory));
// Hide the option to toggle category grouping for now.
groupByCategoryCheckbox.setVisible(false);
rightToolbar.appendToolbarItem(groupByCategoryCheckbox);
groupByCategorySetting.addChangeListener(() => {
this.fullUpdate(true);
});
const groupByKindSetting = getGroupIssuesByKindSetting();
const groupByKindSettingCheckbox = new UI.Toolbar.ToolbarSettingCheckbox(
groupByKindSetting, i18nString(UIStrings.groupDisplayedIssuesUnderKind), i18nString(UIStrings.groupByKind));
rightToolbar.appendToolbarItem(groupByKindSettingCheckbox);
groupByKindSetting.addChangeListener(() => {
this.fullUpdate(true);
});
groupByKindSettingCheckbox.setVisible(Root.Runtime.experiments.isEnabled('groupAndHideIssuesByKind'));
const thirdPartySetting = IssuesManager.Issue.getShowThirdPartyIssuesSetting();
this.showThirdPartyCheckbox = new UI.Toolbar.ToolbarSettingCheckbox(
thirdPartySetting, i18nString(UIStrings.includeCookieIssuesCausedBy),
i18nString(UIStrings.includeThirdpartyCookieIssues));
rightToolbar.appendToolbarItem(this.showThirdPartyCheckbox);
this.setDefaultFocusedElement(this.showThirdPartyCheckbox.inputElement);
rightToolbar.appendSeparator();
const issueCounter = new IssueCounter.IssueCounter.IssueCounter();
issueCounter.data = {
tooltipCallback: (): void => {
const issueEnumeration = IssueCounter.IssueCounter.getIssueCountsEnumeration(
IssuesManager.IssuesManager.IssuesManager.instance(), false);
issueCounter.title = issueEnumeration;
},
displayMode: IssueCounter.IssueCounter.DisplayMode.ShowAlways,
issuesManager: IssuesManager.IssuesManager.IssuesManager.instance(),
};
issueCounter.id = 'console-issues-counter';
const issuesToolbarItem = new UI.Toolbar.ToolbarItem(issueCounter);
rightToolbar.appendToolbarItem(issuesToolbarItem);
return {toolbarContainer};
}
private issueUpdated(event: Common.EventTarget.EventTargetEvent<AggregatedIssue>): void {
this.scheduleIssueViewUpdate(event.data);
}
private scheduleIssueViewUpdate(issue: AggregatedIssue): void {
this.issueViewUpdatePromise = this.issueViewUpdatePromise.then(() => this.updateIssueView(issue));
}
/** Don't call directly. Use `scheduleIssueViewUpdate` instead. */
private async updateIssueView(issue: AggregatedIssue): Promise<void> {
let issueView = this.issueViews.get(issue.aggregationKey());
if (!issueView) {
const description = issue.getDescription();
if (!description) {
console.warn('Could not find description for issue code:', issue.code());
return;
}
const markdownDescription =
await IssuesManager.MarkdownIssueDescription.createIssueDescriptionFromMarkdown(description);
issueView = new IssueView(issue, markdownDescription);
this.issueViews.set(issue.aggregationKey(), issueView);
const parent = this.getIssueViewParent(issue);
this.appendIssueViewToParent(issueView, parent);
} else {
issueView.setIssue(issue);
const newParent = this.getIssueViewParent(issue);
if (issueView.parent !== newParent &&
!(newParent instanceof UI.TreeOutline.TreeOutline && issueView.parent === newParent.rootElement())) {
issueView.parent?.removeChild(issueView);
this.appendIssueViewToParent(issueView, newParent);
}
}
issueView.update();
this.updateCounts();
}
appendIssueViewToParent(issueView: IssueView, parent: UI.TreeOutline.TreeOutline|UI.TreeOutline.TreeElement): void {
parent.appendChild(issueView, (a, b) => {
if (a instanceof HiddenIssuesRow) {
return 1;
}
if (b instanceof HiddenIssuesRow) {
return -1;
}
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;
});
}
private getIssueViewParent(issue: AggregatedIssue): UI.TreeOutline.TreeOutline|UI.TreeOutline.TreeElement {
const groupByKind = Root.Runtime.experiments.isEnabled('groupAndHideIssuesByKind');
if (issue.isHidden()) {
return this.hiddenIssuesRow;
}
if (groupByKind && getGroupIssuesByKindSetting().get()) {
const kind = issue.getKind();
const view = this.kindViews.get(kind);
if (view) {
return view;
}
const newView = new IssueKindView(kind);
this.issuesTree.appendChild(newView, (a, b) => {
if (a instanceof IssueKindView && b instanceof IssueKindView) {
return issueKindViewSortPriority(a, b);
}
return 0;
});
this.kindViews.set(kind, newView);
return newView;
}
if (getGroupIssuesByCategorySetting().get()) {
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;
}
return this.issuesTree;
}
private clearViews<T>(views: Map<T, UI.TreeOutline.TreeElement>, preservedSet?: Set<T>): void {
for (const [key, view] of Array.from(views.entries())) {
if (preservedSet?.has(key)) {
continue;
}
view.parent && view.parent.removeChild(view);
views.delete(key);
}
}
private onFullUpdate(): void {
this.fullUpdate(false);
}
private fullUpdate(force: boolean): void {
this.clearViews(this.categoryViews, force ? undefined : this.aggregator.aggregatedIssueCategories());
this.clearViews(this.kindViews, force ? undefined : this.aggregator.aggregatedIssueKinds());
this.clearViews(this.issueViews, force ? undefined : this.aggregator.aggregatedIssueCodes());
if (this.aggregator) {
for (const issue of this.aggregator.aggregatedIssues()) {
this.scheduleIssueViewUpdate(issue);
}
}
this.updateCounts();
}
private updateIssueKindViewsCount(): void {
for (const view of this.kindViews.values()) {
const count = this.issuesManager.numberOfIssues(view.getKind());
view.update(count);
}
}
private updateCounts(): void {
const groupByKind = Root.Runtime.experiments.isEnabled('groupAndHideIssuesByKind');
this.showIssuesTreeOrNoIssuesDetectedMessage(
this.issuesManager.numberOfIssues(), this.issuesManager.numberOfHiddenIssues());
if (groupByKind && getGroupIssuesByKindSetting().get()) {
this.updateIssueKindViewsCount();
}
}
private showIssuesTreeOrNoIssuesDetectedMessage(issuesCount: number, hiddenIssueCount: number): void {
if (issuesCount > 0 || hiddenIssueCount > 0) {
this.hiddenIssuesRow.hidden = hiddenIssueCount === 0;
this.hiddenIssuesRow.update(hiddenIssueCount);
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 ? i18nString(UIStrings.onlyThirdpartyCookieIssues) :
i18nString(UIStrings.noIssuesDetectedSoFar);
this.noIssuesMessageDiv.style.display = 'flex';
}
}
async reveal(issue: IssuesManager.Issue.Issue): Promise<void> {
await this.issueViewUpdatePromise;
const key = this.aggregator.keyForIssue(issue);
const issueView = this.issueViews.get(key);
const groupByKind = Root.Runtime.experiments.isEnabled('groupAndHideIssuesByKind');
if (issueView) {
if (issueView.isForHiddenIssue()) {
this.hiddenIssuesRow.expand();
this.hiddenIssuesRow.reveal();
}
if (groupByKind && getGroupIssuesByKindSetting().get() && !issueView.isForHiddenIssue()) {
const kindView = this.kindViews.get(issueView.getIssueKind());
kindView?.expand();
kindView?.reveal();
}
issueView.expand();
issueView.reveal();
issueView.select(false, true);
}
}
wasShown(): void {
super.wasShown();
this.issuesTree.registerCSSFiles([issuesTreeStyles]);
this.registerCSSFiles([issuesPaneStyles]);
}
}