UNPKG

chrome-devtools-frontend

Version:
473 lines (424 loc) • 19.1 kB
// 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]); } }