chrome-devtools-frontend
Version:
Chrome DevTools UI
137 lines (119 loc) • 4.56 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.
import * as i18n from '../../../core/i18n/i18n.js';
import type * as Trace from '../../../models/trace/trace.js';
import * as UI from '../../../ui/legacy/legacy.js';
import * as Lit from '../../../ui/lit/lit.js';
import relatedInsightsStyles from './relatedInsightChips.css.js';
const {html, render} = Lit;
const UIStrings = {
/**
* @description prefix shown next to related insight chips
*/
insightKeyword: 'Insight',
/**
* @description Prefix shown next to related insight chips and containing the insight name.
* @example {Improve image delivery} PH1
*/
insightWithName: 'Insight: {PH1}',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/timeline/components/RelatedInsightChips.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export interface RelatedInsight {
insightLabel: string;
messages: string[];
activateInsight: () => void;
}
export type EventToRelatedInsightsMap = Map<Trace.Types.Events.Event, RelatedInsight[]>;
export interface ViewInput {
activeEvent: Trace.Types.Events.Event|null;
eventToInsightsMap: EventToRelatedInsightsMap;
onInsightClick: (insight: RelatedInsight) => void;
}
export interface Data {
eventToRelatedInsightsMap: EventToRelatedInsightsMap|null;
activeEvent: Trace.Types.Events.Event|null;
}
export class RelatedInsightChips extends UI.Widget.Widget {
#view: typeof DEFAULT_VIEW;
#activeEvent: Trace.Types.Events.Event|null = null;
#eventToInsightsMap: EventToRelatedInsightsMap = new Map();
constructor(element?: HTMLElement, view = DEFAULT_VIEW) {
super(element);
this.#view = view;
}
set activeEvent(event: Trace.Types.Events.Event|null) {
if (event === this.#activeEvent) {
return;
}
this.#activeEvent = event;
this.requestUpdate();
}
set eventToInsightsMap(map: EventToRelatedInsightsMap|null) {
// Purposefully don't check object equality here; the contents of the map
// could have changed, so play it safe and always trigger a re-render.
this.#eventToInsightsMap = map ?? new Map();
this.requestUpdate();
}
override performUpdate(): Promise<void>|void {
const input: ViewInput = {
activeEvent: this.#activeEvent,
eventToInsightsMap: this.#eventToInsightsMap,
onInsightClick(insight) {
insight.activateInsight();
},
};
this.#view(input, {}, this.contentElement);
}
}
export const DEFAULT_VIEW: (input: ViewInput, output: object, target: HTMLElement) => void =
(input, _output, target) => {
const {activeEvent, eventToInsightsMap} = input;
const relatedInsights = activeEvent ? eventToInsightsMap.get(activeEvent) ?? [] : [];
if (!activeEvent || eventToInsightsMap.size === 0 || relatedInsights.length === 0) {
render(Lit.nothing, target);
return;
}
// TODO: Render insight messages in a separate UX
// Right before insight chips is acceptable for now
const insightMessages = relatedInsights.flatMap(insight => {
// TODO: support markdown (`md`).
// clang-format off
return insight.messages.map(message => html`
<li class="insight-message-box">
<button type="button" @click=${(event: Event) => {
event.preventDefault();
input.onInsightClick(insight);
}}>
<div class="insight-label">${i18nString(UIStrings.insightWithName, {
PH1: insight.insightLabel,
})}</div>
<div class="insight-message">${message}</div>
</button>
</li>
`);
// clang-format on
});
const insightChips = relatedInsights.flatMap(insight => {
// clang-format off
return [html`
<li class="insight-chip">
<button type="button" @click=${(event: Event) => {
event.preventDefault();
input.onInsightClick(insight);
}}>
<span class="keyword">${i18nString(UIStrings.insightKeyword)}</span>
<span class="insight-label">${insight.insightLabel}</span>
</button>
</li>
`];
// clang-format on
});
// clang-format off
render(html`<style>${relatedInsightsStyles}</style>
<ul>${insightMessages}</ul>
<ul>${insightChips}</ul>`,
target);
// clang-format on
};