blob: 2d254d104808b5db07c50e6759f6e690dcb89f52 [file] [log] [blame]
// Copyright 2016 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-imperative-dom-api */
import '../../ui/legacy/legacy.js';
import * as Common from '../../core/common/common.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as UI from '../../ui/legacy/legacy.js';
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
import {
type AuditProgressChangedEvent,
Events,
LighthouseController,
type PageAuditabilityChangedEvent,
type PageWarningsChangedEvent,
} from './LighthouseController.js';
import lighthousePanelStyles from './lighthousePanel.css.js';
import {ProtocolService} from './LighthouseProtocolService.js';
import type {ReportJSON, RunnerResultArtifacts} from './LighthouseReporterTypes.js';
import {LighthouseReportRenderer} from './LighthouseReportRenderer.js';
import {Item, ReportSelector} from './LighthouseReportSelector.js';
import {StartView} from './LighthouseStartView.js';
import {StatusView} from './LighthouseStatusView.js';
import {TimespanView} from './LighthouseTimespanView.js';
const UIStrings = {
/**
*@description Text that appears when user drag and drop something (for example, a file) in Lighthouse Panel
*/
dropLighthouseJsonHere: 'Drop `Lighthouse` JSON here',
/**
*@description Tooltip text that appears when hovering over the largeicon add button in the Lighthouse Panel
*/
performAnAudit: 'Perform an audit…',
/**
*@description Text to clear everything
*/
clearAll: 'Clear all',
/**
*@description Tooltip text that appears when hovering over the largeicon settings gear in show settings pane setting in start view of the audits panel
*/
lighthouseSettings: '`Lighthouse` settings',
/**
*@description Status header in the Lighthouse panel
*/
printing: 'Printing',
/**
*@description Status text in the Lighthouse panel
*/
thePrintPopupWindowIsOpenPlease: 'The print popup window is open. Please close it to continue.',
/**
*@description Text in Lighthouse Panel
*/
cancelling: 'Cancelling',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/lighthouse/LighthousePanel.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
let lighthousePanelInstace: LighthousePanel;
type Nullable<T> = T|null;
export class LighthousePanel extends UI.Panel.Panel {
private readonly controller: LighthouseController;
private readonly startView: StartView;
private readonly statusView: StatusView;
private readonly timespanView: TimespanView;
private warningText: Nullable<string>;
private unauditableExplanation: Nullable<string>;
private readonly cachedRenderedReports: Map<ReportJSON, HTMLElement>;
// @ts-expect-error keep local reference
private readonly dropTarget: UI.DropTarget.DropTarget;
private readonly auditResultsElement: HTMLElement;
private clearButton!: UI.Toolbar.ToolbarButton;
private newButton!: UI.Toolbar.ToolbarButton;
private reportSelector!: ReportSelector;
private settingsPane!: UI.Widget.Widget;
private rightToolbar!: UI.Toolbar.Toolbar;
private showSettingsPaneSetting!: Common.Settings.Setting<boolean>;
private constructor(
controller: LighthouseController,
) {
super('lighthouse');
this.registerRequiredCSS(lighthousePanelStyles);
this.controller = controller;
this.startView = new StartView(this.controller, this);
this.timespanView = new TimespanView(this);
this.statusView = new StatusView(this);
this.warningText = null;
this.unauditableExplanation = null;
this.cachedRenderedReports = new Map();
this.dropTarget = new UI.DropTarget.DropTarget(
this.contentElement, [UI.DropTarget.Type.File], i18nString(UIStrings.dropLighthouseJsonHere),
this.handleDrop.bind(this));
this.controller.addEventListener(Events.PageAuditabilityChanged, this.refreshStartAuditUI.bind(this));
this.controller.addEventListener(Events.PageWarningsChanged, this.refreshWarningsUI.bind(this));
this.controller.addEventListener(Events.AuditProgressChanged, this.refreshStatusUI.bind(this));
this.renderToolbar();
this.auditResultsElement = this.contentElement.createChild('div', 'lighthouse-results-container');
this.renderStartView();
this.controller.recomputePageAuditability();
}
static instance(opts?: {forceNew: boolean, protocolService: ProtocolService, controller: LighthouseController}):
LighthousePanel {
if (!lighthousePanelInstace || opts?.forceNew) {
const protocolService = opts?.protocolService ?? new ProtocolService();
const controller = opts?.controller ?? new LighthouseController(protocolService);
lighthousePanelInstace = new LighthousePanel(controller);
}
return lighthousePanelInstace;
}
static getEvents(): typeof Events {
return Events;
}
async handleTimespanStart(): Promise<void> {
try {
this.timespanView.show(this.contentElement);
await this.controller.startLighthouse();
this.timespanView.ready();
} catch (err) {
this.handleError(err);
}
}
async handleTimespanEnd(): Promise<void> {
try {
this.timespanView.hide();
this.renderStatusView();
const {lhr, artifacts} = await this.controller.collectLighthouseResults();
this.buildReportUI(lhr, artifacts);
} catch (err) {
this.handleError(err);
}
}
async handleCompleteRun(): Promise<void> {
try {
await this.controller.startLighthouse();
this.renderStatusView();
const {lhr, artifacts} = await this.controller.collectLighthouseResults();
this.buildReportUI(lhr, artifacts);
} catch (err) {
this.handleError(err);
}
}
async handleRunCancel(): Promise<void> {
this.statusView.updateStatus(i18nString(UIStrings.cancelling));
this.timespanView.hide();
await this.controller.cancelLighthouse();
this.renderStartView();
}
private handleError(err: unknown): void {
if (err instanceof Error) {
this.statusView.renderBugReport(err);
}
}
private refreshWarningsUI(evt: Common.EventTarget.EventTargetEvent<PageWarningsChangedEvent>): void {
// PageWarningsChanged fires multiple times during an audit, which we want to ignore.
if (this.controller.getCurrentRun()) {
return;
}
this.warningText = evt.data.warning;
this.startView.setWarningText(evt.data.warning);
}
private refreshStartAuditUI(evt: Common.EventTarget.EventTargetEvent<PageAuditabilityChangedEvent>): void {
// PageAuditabilityChanged fires multiple times during an audit, which we want to ignore.
if (this.controller.getCurrentRun()) {
return;
}
this.startView.refresh();
this.unauditableExplanation = evt.data.helpText;
this.startView.setUnauditableExplanation(evt.data.helpText);
this.startView.setStartButtonEnabled(!evt.data.helpText);
}
private refreshStatusUI(evt: Common.EventTarget.EventTargetEvent<AuditProgressChangedEvent>): void {
this.statusView.updateStatus(evt.data.message);
}
private refreshToolbarUI(): void {
this.clearButton.setEnabled(this.reportSelector.hasItems());
}
private clearAll(): void {
this.reportSelector.clearAll();
this.renderStartView();
this.refreshToolbarUI();
}
private renderToolbar(): void {
const lighthouseToolbarContainer = this.element.createChild('div', 'lighthouse-toolbar-container');
lighthouseToolbarContainer.setAttribute('jslog', `${VisualLogging.toolbar()}`);
lighthouseToolbarContainer.role = 'toolbar';
const toolbar = lighthouseToolbarContainer.createChild('devtools-toolbar');
toolbar.role = 'presentation';
this.newButton = new UI.Toolbar.ToolbarButton(i18nString(UIStrings.performAnAudit), 'plus');
toolbar.appendToolbarItem(this.newButton);
this.newButton.addEventListener(UI.Toolbar.ToolbarButton.Events.CLICK, this.renderStartView.bind(this));
toolbar.appendSeparator();
this.reportSelector = new ReportSelector(() => this.renderStartView());
toolbar.appendToolbarItem(this.reportSelector.comboBox());
this.clearButton = new UI.Toolbar.ToolbarButton(i18nString(UIStrings.clearAll), 'clear');
toolbar.appendToolbarItem(this.clearButton);
this.clearButton.addEventListener(UI.Toolbar.ToolbarButton.Events.CLICK, this.clearAll.bind(this));
this.settingsPane = new UI.Widget.HBox();
this.settingsPane.show(this.contentElement);
this.settingsPane.element.classList.add('lighthouse-settings-pane');
this.settingsPane.element.appendChild(this.startView.settingsToolbar());
this.showSettingsPaneSetting = Common.Settings.Settings.instance().createSetting(
'lighthouse-show-settings-toolbar', false, Common.Settings.SettingStorageType.SYNCED);
this.rightToolbar = lighthouseToolbarContainer.createChild('devtools-toolbar');
this.rightToolbar.role = 'presentation';
this.rightToolbar.appendSeparator();
this.rightToolbar.appendToolbarItem(new UI.Toolbar.ToolbarSettingToggle(
this.showSettingsPaneSetting, 'gear', i18nString(UIStrings.lighthouseSettings), 'gear-filled'));
this.showSettingsPaneSetting.addChangeListener(this.updateSettingsPaneVisibility.bind(this));
this.updateSettingsPaneVisibility();
this.refreshToolbarUI();
}
private updateSettingsPaneVisibility(): void {
this.settingsPane.element.classList.toggle('hidden', !this.showSettingsPaneSetting.get());
}
private toggleSettingsDisplay(show: boolean): void {
this.rightToolbar.classList.toggle('hidden', !show);
this.settingsPane.element.classList.toggle('hidden', !show);
this.updateSettingsPaneVisibility();
}
private renderStartView(): void {
this.auditResultsElement.removeChildren();
this.statusView.hide();
this.reportSelector.selectNewReport();
this.contentElement.classList.toggle('in-progress', false);
this.startView.show(this.contentElement);
this.toggleSettingsDisplay(true);
this.startView.setUnauditableExplanation(this.unauditableExplanation);
this.startView.setStartButtonEnabled(!this.unauditableExplanation);
if (!this.unauditableExplanation) {
this.startView.focusStartButton();
}
this.startView.setWarningText(this.warningText);
this.newButton.setEnabled(false);
this.refreshToolbarUI();
this.setDefaultFocusedChild(this.startView);
}
private renderStatusView(): void {
const inspectedURL = this.controller.getCurrentRun()?.inspectedURL;
this.contentElement.classList.toggle('in-progress', true);
this.statusView.setInspectedURL(inspectedURL);
this.statusView.show(this.contentElement);
}
private beforePrint(): void {
this.statusView.show(this.contentElement);
this.statusView.toggleCancelButton(false);
this.statusView.renderText(i18nString(UIStrings.printing), i18nString(UIStrings.thePrintPopupWindowIsOpenPlease));
}
private afterPrint(): void {
this.statusView.hide();
this.statusView.toggleCancelButton(true);
}
private renderReport(lighthouseResult: ReportJSON, artifacts?: RunnerResultArtifacts): void {
this.toggleSettingsDisplay(false);
this.contentElement.classList.toggle('in-progress', false);
this.startView.hideWidget();
this.statusView.hide();
this.auditResultsElement.removeChildren();
this.newButton.setEnabled(true);
this.refreshToolbarUI();
const cachedRenderedReport = this.cachedRenderedReports.get(lighthouseResult);
if (cachedRenderedReport) {
this.auditResultsElement.appendChild(cachedRenderedReport);
return;
}
const reportContainer = LighthouseReportRenderer.renderLighthouseReport(lighthouseResult, artifacts, {
beforePrint: this.beforePrint.bind(this),
afterPrint: this.afterPrint.bind(this),
});
this.cachedRenderedReports.set(lighthouseResult, reportContainer);
}
private buildReportUI(lighthouseResult: ReportJSON, artifacts?: RunnerResultArtifacts): void {
if (lighthouseResult === null) {
return;
}
const optionElement = new Item(
lighthouseResult, () => this.renderReport(lighthouseResult, artifacts), this.renderStartView.bind(this));
this.reportSelector.prepend(optionElement);
this.refreshToolbarUI();
this.renderReport(lighthouseResult);
this.newButton.element.focus();
}
private handleDrop(dataTransfer: DataTransfer): void {
const items = dataTransfer.items;
if (!items.length) {
return;
}
const item = items[0];
if (item.kind === 'file') {
const file = items[0].getAsFile();
if (!file) {
return;
}
const reader = new FileReader();
reader.onload = () => this.loadedFromFile(reader.result as string);
reader.readAsText(file);
}
}
private loadedFromFile(report: string): void {
const data = JSON.parse(report);
if (!data['lighthouseVersion']) {
return;
}
this.buildReportUI(data as ReportJSON);
}
override elementsToRestoreScrollPositionsFor(): Element[] {
const els = super.elementsToRestoreScrollPositionsFor();
const lhContainerEl = this.auditResultsElement.querySelector('.lh-container');
if (lhContainerEl) {
els.push(lhContainerEl);
}
return els;
}
}