From 552fa785640a779abf570daf5b881cc9ba6f5e01 Mon Sep 17 00:00:00 2001 From: Adela Almasan <88068998+adela-almasan@users.noreply.github.com> Date: Wed, 3 Jan 2024 06:27:17 -0600 Subject: [PATCH] StateTimeline: Add tooltip multi mode (#79944) Co-authored-by: Leon Sorokin --- .../state-timeline/StateTimelinePanel.tsx | 3 + .../state-timeline/StateTimelineTooltip2.tsx | 132 +++++++++++++----- .../plugins/panel/state-timeline/module.tsx | 5 +- 3 files changed, 100 insertions(+), 40 deletions(-) diff --git a/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx b/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx index f12f6af50a8..f9928ec7a9e 100644 --- a/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx +++ b/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx @@ -210,7 +210,10 @@ export const StateTimelinePanel = ({ alignedData={alignedFrame} seriesIdx={seriesIdx} timeZone={timeZone} + mode={options.tooltip.mode} + sortOrder={options.tooltip.sort} isPinned={isPinned} + timeRange={timeRange} /> ); }} diff --git a/public/app/plugins/panel/state-timeline/StateTimelineTooltip2.tsx b/public/app/plugins/panel/state-timeline/StateTimelineTooltip2.tsx index a24fd1bf2a0..48c7ca5604b 100644 --- a/public/app/plugins/panel/state-timeline/StateTimelineTooltip2.tsx +++ b/public/app/plugins/panel/state-timeline/StateTimelineTooltip2.tsx @@ -2,6 +2,7 @@ import { css } from '@emotion/css'; import React from 'react'; import { + arrayUtils, DataFrame, Field, FieldType, @@ -9,9 +10,11 @@ import { getFieldDisplayName, GrafanaTheme2, LinkModel, + TimeRange, TimeZone, } from '@grafana/data'; -import { useStyles2, useTheme2 } from '@grafana/ui'; +import { SortOrder } from '@grafana/schema/dist/esm/common/common.gen'; +import { TooltipDisplayMode, useStyles2, useTheme2 } from '@grafana/ui'; import { VizTooltipContent } from '@grafana/ui/src/components/VizTooltip/VizTooltipContent'; import { VizTooltipFooter } from '@grafana/ui/src/components/VizTooltip/VizTooltipFooter'; import { VizTooltipHeader } from '@grafana/ui/src/components/VizTooltip/VizTooltipHeader'; @@ -28,6 +31,9 @@ interface StateTimelineTooltip2Props { seriesIdx: number | null | undefined; isPinned: boolean; timeZone?: TimeZone; + timeRange: TimeRange; + mode?: TooltipDisplayMode; + sortOrder?: SortOrder; } export const StateTimelineTooltip2 = ({ @@ -36,6 +42,9 @@ export const StateTimelineTooltip2 = ({ dataIdxs, seriesIdx, timeZone, + timeRange, + mode = TooltipDisplayMode.Single, + sortOrder = SortOrder.None, isPinned, }: StateTimelineTooltip2Props) => { const styles = useStyles2(getStyles); @@ -65,54 +74,45 @@ export const StateTimelineTooltip2 = ({ return null; } - const field = alignedData.fields[seriesIdx!]; - - const links: Array> = getDataLinks(field, datapointIdx); + let contentLabelValue: LabelValue[] = []; const xField = alignedData.fields[0]; const xFieldFmt = xField.display || getDisplayProcessor({ field: xField, timeZone, theme }); - const dataFrameFieldIndex = field.state?.origin; - const fieldFmt = field.display || getDisplayProcessor({ field, timeZone, theme }); - const value = field.values[datapointIdx!]; - const display = fieldFmt(value); - const fieldDisplayName = dataFrameFieldIndex - ? getFieldDisplayName( - data[dataFrameFieldIndex.frameIndex].fields[dataFrameFieldIndex.fieldIndex], - data[dataFrameFieldIndex.frameIndex], - data - ) - : null; - - const nextStateIdx = findNextStateIndex(field, datapointIdx!); - let nextStateTs; - if (nextStateIdx) { - nextStateTs = xField.values[nextStateIdx!]; - } - - const stateTs = xField.values[datapointIdx!]; - let duration = nextStateTs && fmtDuration(nextStateTs - stateTs); - - if (nextStateTs) { - duration = nextStateTs && fmtDuration(nextStateTs - stateTs); - } + let links: Array> = []; const from = xFieldFmt(xField.values[datapointIdx!]).text; - const to = xFieldFmt(xField.values[nextStateIdx!]).text; - const getHeaderLabel = (): LabelValue => { - return { - label: '', - value: Boolean(to) ? to : from, - }; - }; + // Single mode + if (mode === TooltipDisplayMode.Single || isPinned) { + const field = alignedData.fields[seriesIdx!]; + links = getDataLinks(field, datapointIdx); + + const fieldFmt = field.display || getDisplayProcessor({ field, timeZone, theme }); + const value = field.values[datapointIdx!]; + const display = fieldFmt(value); + + const nextStateIdx = findNextStateIndex(field, datapointIdx!); + let nextStateTs; + if (nextStateIdx) { + nextStateTs = xField.values[nextStateIdx!]; + } + + const stateTs = xField.values[datapointIdx!]; + let duration: string; + + if (nextStateTs) { + duration = nextStateTs && fmtDuration(nextStateTs - stateTs); + } else { + const to = timeRange.to.valueOf(); + duration = fmtDuration(to - stateTs); + } - const getContentLabelValue = (): LabelValue[] => { const durationEntry: LabelValue[] = duration ? [{ label: 'Duration', value: duration }] : []; - return [ + contentLabelValue = [ { - label: fieldDisplayName ?? '', + label: getFieldDisplayName(field), value: display.text, color: display.color, colorIndicator: ColorIndicator.value, @@ -120,6 +120,62 @@ export const StateTimelineTooltip2 = ({ }, ...durationEntry, ]; + } + + if (mode === TooltipDisplayMode.Multi && !isPinned) { + const fields = alignedData.fields; + const sortIdx: unknown[] = []; + + for (let i = 0; i < fields.length; i++) { + const field = fields[i]; + if ( + !field || + field === xField || + field.type === FieldType.time || + field.config.custom?.hideFrom?.tooltip || + field.config.custom?.hideFrom?.viz + ) { + continue; + } + + const fieldFmt = field.display || getDisplayProcessor({ field, timeZone, theme }); + const v = field.values[dataIdxs[i]!]; + const display = fieldFmt(v); + + sortIdx.push(v); + contentLabelValue.push({ + label: getFieldDisplayName(field), + value: display.text, + color: display.color, + colorIndicator: ColorIndicator.value, + colorPlacement: ColorPlacement.trailing, + isActive: seriesIdx === i, + }); + } + + if (sortOrder !== SortOrder.None) { + // create sort reference series array, as Array.sort() mutates the original array + const sortRef = [...contentLabelValue]; + const sortFn = arrayUtils.sortValues(sortOrder); + + contentLabelValue.sort((a, b) => { + // get compared values indices to retrieve raw values from sortIdx + const aIdx = sortRef.indexOf(a); + const bIdx = sortRef.indexOf(b); + return sortFn(sortIdx[aIdx], sortIdx[bIdx]); + }); + } + } + + const getHeaderLabel = (): LabelValue => { + return { + label: '', + value: from, + }; + }; + + const getContentLabelValue = (): LabelValue[] => { + return contentLabelValue; }; return ( diff --git a/public/app/plugins/panel/state-timeline/module.tsx b/public/app/plugins/panel/state-timeline/module.tsx index 8dfab7fcdce..e18f695f7c3 100644 --- a/public/app/plugins/panel/state-timeline/module.tsx +++ b/public/app/plugins/panel/state-timeline/module.tsx @@ -5,6 +5,7 @@ import { identityOverrideProcessor, PanelPlugin, } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { VisibilityMode } from '@grafana/schema'; import { commonOptionsBuilder } from '@grafana/ui'; @@ -14,7 +15,7 @@ import { NullEditorSettings } from '../timeseries/config'; import { StateTimelinePanel } from './StateTimelinePanel'; import { timelinePanelChangedHandler } from './migrations'; -import { Options, FieldConfig, defaultOptions, defaultFieldConfig } from './panelcfg.gen'; +import { defaultFieldConfig, defaultOptions, FieldConfig, Options } from './panelcfg.gen'; import { StatTimelineSuggestionsSupplier } from './suggestions'; export const plugin = new PanelPlugin(StateTimelinePanel) @@ -121,7 +122,7 @@ export const plugin = new PanelPlugin(StateTimelinePanel) }); commonOptionsBuilder.addLegendOptions(builder, false); - commonOptionsBuilder.addTooltipOptions(builder, true); + commonOptionsBuilder.addTooltipOptions(builder, !config.featureToggles.newVizTooltips); }) .setSuggestionsSupplier(new StatTimelineSuggestionsSupplier()) .setDataSupport({ annotations: true });