chrome-devtools-frontend
Version:
Chrome DevTools UI
252 lines (209 loc) • 8.52 kB
text/typescript
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/* eslint-disable @devtools/no-imperative-dom-api */
import * as Common from '../../../core/common/common.js';
import type * as Trace from '../../../models/trace/trace.js';
import * as UI from '../../../ui/legacy/legacy.js';
import {InsightActivated, InsightDeactivated} from './insights/SidebarInsight.js';
import {SidebarAnnotationsTab} from './SidebarAnnotationsTab.js';
import {SidebarInsightsTab} from './SidebarInsightsTab.js';
export interface ActiveInsight {
model: Trace.Insights.Types.InsightModel;
insightSetKey: string;
}
export class RemoveAnnotation extends Event {
static readonly eventName = 'removeannotation';
constructor(public removedAnnotation: Trace.Types.File.Annotation) {
super(RemoveAnnotation.eventName, {bubbles: true, composed: true});
}
}
export class RevealAnnotation extends Event {
static readonly eventName = 'revealannotation';
constructor(public annotation: Trace.Types.File.Annotation) {
super(RevealAnnotation.eventName, {bubbles: true, composed: true});
}
}
export class HoverAnnotation extends Event {
static readonly eventName = 'hoverannotation';
constructor(public annotation: Trace.Types.File.Annotation) {
super(HoverAnnotation.eventName, {bubbles: true, composed: true});
}
}
export class AnnotationHoverOut extends Event {
static readonly eventName = 'annotationhoverout';
constructor() {
super(AnnotationHoverOut.eventName, {bubbles: true, composed: true});
}
}
declare global {
interface GlobalEventHandlersEventMap {
[RevealAnnotation.eventName]: RevealAnnotation;
[HoverAnnotation.eventName]: HoverAnnotation;
[AnnotationHoverOut.eventName]: AnnotationHoverOut;
}
}
export const enum SidebarTabs {
INSIGHTS = 'insights',
ANNOTATIONS = 'annotations',
}
export const DEFAULT_SIDEBAR_TAB = SidebarTabs.INSIGHTS;
export const DEFAULT_SIDEBAR_WIDTH_PX = 240;
const MIN_SIDEBAR_WIDTH_PX = 170;
export class SidebarWidget extends UI.Widget.VBox {
#tabbedPane = new UI.TabbedPane.TabbedPane();
#insightsView = new InsightsView();
#annotationsView = new AnnotationsView();
/**
* If the user has an Insight open and then they collapse the sidebar, we
* deactivate that Insight to avoid it showing overlays etc - as the user has
* hidden the Sidebar & Insight from view. But we store it because when the
* user pops the sidebar open, we want to re-activate it.
*/
#insightToRestoreOnOpen: ActiveInsight|null = null;
/**
* We track if the user has opened the sidebar once. This is used to
* automatically show the sidebar for new users when they first record or
* import a trace, but then persist its state (so if they close it, it stays
* closed).
*/
#hasOpenedOnce =
Common.Settings.Settings.instance().createSetting<boolean>('timeline-sidebar-opened-at-least-once', false);
constructor() {
super();
this.setMinimumSize(MIN_SIDEBAR_WIDTH_PX, 0);
this.#tabbedPane.appendTab(
SidebarTabs.INSIGHTS, 'Insights', this.#insightsView, undefined, undefined, false, false, 0,
'timeline.insights-tab');
this.#tabbedPane.appendTab(
SidebarTabs.ANNOTATIONS, 'Annotations', this.#annotationsView, undefined, undefined, false, false, 1,
'timeline.annotations-tab');
// Default the selected tab to Insights. In wasShown() we will change this
// if this is a trace that has no insights.
this.#tabbedPane.selectTab(SidebarTabs.INSIGHTS);
}
override wasShown(): void {
super.wasShown();
this.#hasOpenedOnce.set(true);
this.#tabbedPane.show(this.element);
this.#updateAnnotationsCountBadge();
if (this.#insightToRestoreOnOpen) {
this.element.dispatchEvent(new InsightActivated(
this.#insightToRestoreOnOpen.model,
this.#insightToRestoreOnOpen.insightSetKey,
));
this.#insightToRestoreOnOpen = null;
}
// Swap to the Annotations tab if:
// 1. Insights is currently selected.
// 2. The Insights tab is disabled (which means we have no insights for this trace)
if (this.#tabbedPane.selectedTabId === SidebarTabs.INSIGHTS &&
this.#tabbedPane.tabIsDisabled(SidebarTabs.INSIGHTS)) {
this.#tabbedPane.selectTab(SidebarTabs.ANNOTATIONS);
}
}
override willHide(): void {
super.willHide();
const currentlyActiveInsight = this.#insightsView.getActiveInsight();
this.#insightToRestoreOnOpen = currentlyActiveInsight;
if (currentlyActiveInsight) {
this.element.dispatchEvent(new InsightDeactivated());
}
}
setAnnotations(
updatedAnnotations: Trace.Types.File.Annotation[],
annotationEntryToColorMap: Map<Trace.Types.Events.Event, string>): void {
this.#annotationsView.setAnnotations(updatedAnnotations, annotationEntryToColorMap);
this.#updateAnnotationsCountBadge();
}
#updateAnnotationsCountBadge(): void {
const annotations = this.#annotationsView.deduplicatedAnnotations();
this.#tabbedPane.setBadge('annotations', annotations.length > 0 ? annotations.length.toString() : null);
}
setParsedTrace(parsedTrace: Trace.TraceModel.ParsedTrace|null): void {
this.#insightsView.setParsedTrace(parsedTrace);
this.#tabbedPane.setTabEnabled(
SidebarTabs.INSIGHTS,
Boolean(parsedTrace?.insights && parsedTrace.insights.size > 0),
);
}
setActiveInsight(activeInsight: ActiveInsight|null, opts: {
highlight: boolean,
}): void {
this.#insightsView.setActiveInsight(activeInsight, opts);
if (activeInsight) {
this.#tabbedPane.selectTab(SidebarTabs.INSIGHTS);
}
}
openInsightsTab(): void {
this.#tabbedPane.selectTab(SidebarTabs.INSIGHTS);
}
setActiveInsightSet(insightSetKey: string): void {
this.#insightsView.setActiveInsightSet(insightSetKey);
}
/**
* True if the sidebar has been visible at least one time. This is persisted
* to the user settings so it persists across sessions. This is used because
* we do not force the RPP sidebar open by default; if a user has seen it &
* then closed it, we will not re-open it automatically. But if a user
* has never seen it, we want them to see it once to know it exists.
*/
sidebarHasBeenOpened(): boolean {
return this.#hasOpenedOnce.get();
}
}
class InsightsView extends UI.Widget.VBox {
#component = SidebarInsightsTab.createWidgetElement();
constructor() {
super();
this.element.classList.add('sidebar-insights');
this.#getWidget().show(this.element);
}
#getWidget(): SidebarInsightsTab {
return UI.Widget.Widget.get(this.#component) as SidebarInsightsTab;
}
setParsedTrace(parsedTrace: Trace.TraceModel.ParsedTrace|null): void {
const widget = this.#getWidget();
widget.parsedTrace = parsedTrace;
}
getActiveInsight(): ActiveInsight|null {
return this.#getWidget().activeInsight;
}
setActiveInsight(active: ActiveInsight|null, opts: {highlight: boolean}): void {
const widget = this.#getWidget();
widget.activeInsight = active;
if (opts.highlight && active) {
// Wait for the rendering of the component to be done, otherwise we
// might highlight the wrong insight. The UI needs to be fully
// re-rendered before we can highlight the newly-expanded insight.
void widget.updateComplete.then(() => {
void widget.highlightActiveInsight();
});
}
}
setActiveInsightSet(insightSetKey: string): void {
this.#getWidget().setActiveInsightSet(insightSetKey);
}
}
class AnnotationsView extends UI.Widget.VBox {
#component = new SidebarAnnotationsTab();
constructor() {
super();
this.element.classList.add('sidebar-annotations');
this.#component.show(this.element);
}
setAnnotations(
annotations: Trace.Types.File.Annotation[],
annotationEntryToColorMap: Map<Trace.Types.Events.Event, string>): void {
this.#component.setData({annotations, annotationEntryToColorMap});
}
/**
* The component "de-duplicates" annotations to ensure implementation details
* about how we create pending annotations don't leak into the UI. We expose
* these here because we use this count to show the number of annotations in
* the small adorner in the sidebar tab.
*/
deduplicatedAnnotations(): readonly Trace.Types.File.Annotation[] {
return this.#component.deduplicatedAnnotations();
}
}