UNPKG

chrome-devtools-frontend

Version:
277 lines (236 loc) • 10.1 kB
// Copyright 2024 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-lit-render-outside-of-view */ import './SidebarSingleInsightSet.js'; import * as Host from '../../../core/host/host.js'; import * as i18n from '../../../core/i18n/i18n.js'; import type * as Platform from '../../../core/platform/platform.js'; import * as Trace from '../../../models/trace/trace.js'; import * as Buttons from '../../../ui/components/buttons/buttons.js'; import * as ComponentHelpers from '../../../ui/components/helpers/helpers.js'; import * as Lit from '../../../ui/lit/lit.js'; import * as Utils from '../utils/utils.js'; import * as Insights from './insights/insights.js'; import type {ActiveInsight} from './Sidebar.js'; import sidebarInsightsTabStyles from './sidebarInsightsTab.css.js'; import type {SidebarSingleInsightSet, SidebarSingleInsightSetData} from './SidebarSingleInsightSet.js'; const {html} = Lit; const FEEDBACK_URL = 'https://crbug.com/371170842' as Platform.DevToolsPath.UrlString; const UIStrings = { /** *@description text show in feedback button */ feedbackButton: 'Feedback', /** *@description text show in feedback tooltip */ feedbackTooltip: 'Insights is an experimental feature. Your feedback will help us improve it.', } as const; const str_ = i18n.i18n.registerUIStrings('panels/timeline/components/SidebarInsightsTab.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); export class SidebarInsightsTab extends HTMLElement { readonly #shadow = this.attachShadow({mode: 'open'}); #parsedTrace: Trace.Handlers.Types.ParsedTrace|null = null; #traceMetadata: Trace.Types.File.MetaData|null = null; #insights: Trace.Insights.Types.TraceInsightSets|null = null; #activeInsight: ActiveInsight|null = null; #selectedCategory = Trace.Insights.Types.InsightCategory.ALL; /** * When a trace has sets of insights, we show an accordion with each * set within. A set can be specific to a single navigation, or include the * beginning of the trace up to the first navigation. * You can only have one of these open at any time, and we track it via this ID. */ #insightSetKey: string|null = null; // TODO(paulirish): add back a disconnectedCallback() to avoid memory leaks that doesn't cause b/372943062 set parsedTrace(data: Trace.Handlers.Types.ParsedTrace|null) { if (data === this.#parsedTrace) { return; } this.#parsedTrace = data; this.#insightSetKey = null; void ComponentHelpers.ScheduledRender.scheduleRender(this, this.#render); } set traceMetadata(data: Trace.Types.File.MetaData|null) { if (data === this.#traceMetadata) { return; } this.#traceMetadata = data; this.#insightSetKey = null; void ComponentHelpers.ScheduledRender.scheduleRender(this, this.#render); } set insights(data: Trace.Insights.Types.TraceInsightSets|null) { if (data === this.#insights) { return; } this.#insights = data; this.#insightSetKey = null; if (!this.#insights || !this.#parsedTrace) { return; } // Select by default the first non-trivial insight set: // - greater than 5s in duration // - or, has a navigation // In practice this means selecting either the first or the second insight set. const trivialThreshold = Trace.Helpers.Timing.milliToMicro(Trace.Types.Timing.Milli(5000)); const insightSets = [...this.#insights.values()]; this.#insightSetKey = insightSets.find(insightSet => insightSet.navigation || insightSet.bounds.range > trivialThreshold)?.id // If everything is "trivial", just select the first one. ?? insightSets[0]?.id ?? null; void ComponentHelpers.ScheduledRender.scheduleRender(this, this.#render); } set activeInsight(active: ActiveInsight|null) { if (active === this.#activeInsight) { return; } this.#activeInsight = active; // Only update the insightSetKey if there is an active insight. Otherwise, closing an insight // would also collapse the insight set. Usually the proper insight set is already set because // the user has it open already in order for this setter to be called, but insights can also // be activated by clicking on a insight chip in the Summary panel, which may require opening // a different insight set. if (this.#activeInsight) { this.#insightSetKey = this.#activeInsight.insightSetKey; } void ComponentHelpers.ScheduledRender.scheduleRender(this, this.#render); } #insightSetToggled(id: string): void { this.#insightSetKey = this.#insightSetKey === id ? null : id; // Update the active insight set. if (this.#insightSetKey !== this.#activeInsight?.insightSetKey) { this.dispatchEvent(new Insights.SidebarInsight.InsightDeactivated()); } void ComponentHelpers.ScheduledRender.scheduleRender(this, this.#render); } #insightSetHovered(id: string): void { const data = this.#insights?.get(id); data && this.dispatchEvent(new Insights.SidebarInsight.InsightSetHovered(data.bounds)); } #insightSetUnhovered(): void { this.dispatchEvent(new Insights.SidebarInsight.InsightSetHovered()); } #onFeedbackClick(): void { Host.InspectorFrontendHost.InspectorFrontendHostInstance.openInNewTab(FEEDBACK_URL); } #onZoomClick(event: Event, id: string): void { event.stopPropagation(); const data = this.#insights?.get(id); if (!data) { return; } this.dispatchEvent(new Insights.SidebarInsight.InsightSetZoom(data.bounds)); } #renderZoomButton(insightSetToggled: boolean): Lit.TemplateResult { const classes = Lit.Directives.classMap({ 'zoom-icon': true, active: insightSetToggled, }); // clang-format off return html` <div class=${classes}> <devtools-button .data=${{ variant: Buttons.Button.Variant.ICON, iconName: 'center-focus-weak', size: Buttons.Button.Size.SMALL, } as Buttons.Button.ButtonData} ></devtools-button></div>`; // clang-format on } #renderDropdownIcon(insightSetToggled: boolean): Lit.TemplateResult { const containerClasses = Lit.Directives.classMap({ 'dropdown-icon': true, active: insightSetToggled, }); // clang-format off return html` <div class=${containerClasses}> <devtools-button .data=${{ variant: Buttons.Button.Variant.ICON, iconName: 'chevron-right', size: Buttons.Button.Size.SMALL, } as Buttons.Button.ButtonData} ></devtools-button></div> `; // clang-format on } highlightActiveInsight(): void { if (!this.#activeInsight) { return; } // Find the right set for this insight via the set key. const set = this.#shadow?.querySelector<SidebarSingleInsightSet>( `devtools-performance-sidebar-single-navigation[data-insight-set-key="${this.#activeInsight.insightSetKey}"]`); if (!set) { return; } set.highlightActiveInsight(); } #render(): void { if (!this.#parsedTrace || !this.#insights) { Lit.render(Lit.nothing, this.#shadow, {host: this}); return; } const hasMultipleInsightSets = this.#insights.size > 1; const labels = Utils.Helpers.createUrlLabels([...this.#insights.values()].map(({url}) => url)); const contents = // clang-format off html` <style>${sidebarInsightsTabStyles}</style> <div class="insight-sets-wrapper"> ${[...this.#insights.values()].map(({id, url}, index) => { const data: SidebarSingleInsightSetData = { insights: this.#insights, insightSetKey: id, activeCategory: this.#selectedCategory, activeInsight: this.#activeInsight, parsedTrace: this.#parsedTrace, traceMetadata: this.#traceMetadata, }; const contents = html` <devtools-performance-sidebar-single-navigation data-insight-set-key=${id} .data=${data}> </devtools-performance-sidebar-single-navigation> `; if (hasMultipleInsightSets) { return html`<details ?open=${id === this.#insightSetKey} > <summary @click=${() => this.#insightSetToggled(id)} @mouseenter=${() => this.#insightSetHovered(id)} @mouseleave=${() => this.#insightSetUnhovered()} title=${url.href}> ${this.#renderDropdownIcon(id === this.#insightSetKey)} <span>${labels[index]}</span> <span class='zoom-button' @click=${(event: Event) => this.#onZoomClick(event, id)}>${this.#renderZoomButton(id === this.#insightSetKey)}</span> </summary> ${contents} </details>`; } return contents; })} </div> <div class="feedback-wrapper"> <devtools-button .variant=${Buttons.Button.Variant.OUTLINED} .iconName=${'experiment'} @click=${this.#onFeedbackClick}> ${i18nString(UIStrings.feedbackButton)} </devtools-button> <p class="tooltip">${i18nString(UIStrings.feedbackTooltip)}</p> </div> `; // clang-format on // Insight components contain state, so to prevent insights from previous trace loads breaking things we use the parsedTrace // as a render key. // Note: newer Lit has `keyed`, but we don't have that, so we do it manually. https://lit.dev/docs/templates/directives/#keyed const result = Lit.Directives.repeat([contents], () => this.#parsedTrace, template => template); Lit.render(result, this.#shadow, {host: this}); } } customElements.define('devtools-performance-sidebar-insights', SidebarInsightsTab); declare global { interface HTMLElementTagNameMap { 'devtools-performance-sidebar-insights': SidebarInsightsTab; } }