[go: nahoru, domu]

blob: 868cc2a9211d707d4c7213b717026eccb21c31e3 [file] [log] [blame]
// 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 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 {
Events as IssueAggregatorEvents,
IssueAggregator,
type AggregatedIssue,
type AggregationKey,
} 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 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 {
#category: IssuesManager.Issue.IssueCategory;
constructor(category: IssuesManager.Issue.IssueCategory) {
super();
this.#category = category;
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.Cookie:
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.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);
}
}
override onattach(): void {
this.#appendHeader();
}
#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 {
#categoryViews: Map<IssuesManager.Issue.IssueCategory, IssueCategoryView>;
#issueViews: Map<AggregationKey, IssueView>;
#kindViews: Map<IssuesManager.Issue.IssueKind, IssueKindView>;
#showThirdPartyCheckbox: UI.Toolbar.ToolbarSettingCheckbox|null;
#issuesTree: UI.TreeOutline.TreeOutlineInShadow;
#hiddenIssuesRow: HiddenIssuesRow;
#noIssuesMessageDiv: HTMLDivElement;
#issuesManager: IssuesManager.IssuesManager.IssuesManager;
#aggregator: IssueAggregator;
#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;
}
override elementsToRestoreScrollPositionsFor(): Element[] {
return [this.#issuesTree.element];
}
#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(true);
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};
}
#issueUpdated(event: Common.EventTarget.EventTargetEvent<AggregatedIssue>): void {
this.#scheduleIssueViewUpdate(event.data);
}
#scheduleIssueViewUpdate(issue: AggregatedIssue): void {
this.#issueViewUpdatePromise = this.#issueViewUpdatePromise.then(() => this.#updateIssueView(issue));
}
/** Don't call directly. Use `scheduleIssueViewUpdate` instead. */
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;
});
}
#getIssueViewParent(issue: AggregatedIssue): UI.TreeOutline.TreeOutline|UI.TreeOutline.TreeElement {
if (issue.isHidden()) {
return this.#hiddenIssuesRow;
}
if (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;
}
#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);
}
}
#onFullUpdate(): void {
this.#fullUpdate(false);
}
#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();
}
#updateIssueKindViewsCount(): void {
for (const view of this.#kindViews.values()) {
const count = this.#issuesManager.numberOfIssues(view.getKind());
view.update(count);
}
}
#updateCounts(): void {
this.#showIssuesTreeOrNoIssuesDetectedMessage(
this.#issuesManager.numberOfIssues(), this.#issuesManager.numberOfHiddenIssues());
if (getGroupIssuesByKindSetting().get()) {
this.#updateIssueKindViewsCount();
}
}
#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);
if (issueView) {
if (issueView.isForHiddenIssue()) {
this.#hiddenIssuesRow.expand();
this.#hiddenIssuesRow.reveal();
}
if (getGroupIssuesByKindSetting().get() && !issueView.isForHiddenIssue()) {
const kindView = this.#kindViews.get(issueView.getIssueKind());
kindView?.expand();
kindView?.reveal();
}
issueView.expand();
issueView.reveal();
issueView.select(false, true);
}
}
override wasShown(): void {
super.wasShown();
this.#issuesTree.registerCSSFiles([issuesTreeStyles]);
this.registerCSSFiles([issuesPaneStyles]);
}
}