Status History: Improve tooltip (#76647)

* feat(state-timeline-tooltip): improve status history tooltip

Co-authored-by: Adela Almasan <adela.almasan@grafana.com>
Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
This commit is contained in:
Ihor Yeromin 2023-11-22 10:55:29 +02:00 committed by GitHub
parent b7a332276a
commit bb316a7c24
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 148 additions and 12 deletions

View File

@ -357,7 +357,6 @@ export function getConfig(opts: TimelineCoreOptions) {
}; };
const init = (u: uPlot) => { const init = (u: uPlot) => {
let over = u.over;
let chars = ''; let chars = '';
for (let i = 32; i <= 126; i++) { for (let i = 32; i <= 126; i++) {
chars += String.fromCharCode(i); chars += String.fromCharCode(i);
@ -367,7 +366,6 @@ export function getConfig(opts: TimelineCoreOptions) {
// be a bit more conservtive to prevent overlap // be a bit more conservtive to prevent overlap
pxPerChar += 2.5; pxPerChar += 2.5;
over.style.overflow = 'hidden';
u.root.querySelectorAll<HTMLDivElement>('.u-cursor-pt').forEach((el) => { u.root.querySelectorAll<HTMLDivElement>('.u-cursor-pt').forEach((el) => {
el.style.borderRadius = '0'; el.style.borderRadius = '0';
}); });

View File

@ -1,9 +1,11 @@
import React, { useCallback, useMemo, useRef, useState } from 'react'; import React, { useCallback, useMemo, useRef, useState } from 'react';
import { CartesianCoords2D, DashboardCursorSync, DataFrame, FieldType, PanelProps } from '@grafana/data'; import { CartesianCoords2D, DashboardCursorSync, DataFrame, FieldType, PanelProps } from '@grafana/data';
import { config } from '@grafana/runtime';
import { import {
Portal, Portal,
TooltipDisplayMode, TooltipDisplayMode,
TooltipPlugin2,
UPlotConfigBuilder, UPlotConfigBuilder,
usePanelContext, usePanelContext,
useTheme2, useTheme2,
@ -11,6 +13,7 @@ import {
ZoomPlugin, ZoomPlugin,
} from '@grafana/ui'; } from '@grafana/ui';
import { HoverEvent, addTooltipSupport } from '@grafana/ui/src/components/uPlot/config/addTooltipSupport'; import { HoverEvent, addTooltipSupport } from '@grafana/ui/src/components/uPlot/config/addTooltipSupport';
import { TooltipHoverMode } from '@grafana/ui/src/components/uPlot/plugins/TooltipPlugin2';
import { CloseButton } from 'app/core/components/CloseButton/CloseButton'; import { CloseButton } from 'app/core/components/CloseButton/CloseButton';
import { TimelineChart } from 'app/core/components/TimelineChart/TimelineChart'; import { TimelineChart } from 'app/core/components/TimelineChart/TimelineChart';
import { import {
@ -23,6 +26,7 @@ import { OutsideRangePlugin } from '../timeseries/plugins/OutsideRangePlugin';
import { getTimezones } from '../timeseries/utils'; import { getTimezones } from '../timeseries/utils';
import { StatusHistoryTooltip } from './StatusHistoryTooltip'; import { StatusHistoryTooltip } from './StatusHistoryTooltip';
import { StatusHistoryTooltip2 } from './StatusHistoryTooltip2';
import { Options } from './panelcfg.gen'; import { Options } from './panelcfg.gen';
const TOOLTIP_OFFSET = 10; const TOOLTIP_OFFSET = 10;
@ -198,10 +202,10 @@ export const StatusHistoryPanel = ({
{...options} {...options}
mode={TimelineMode.Samples} mode={TimelineMode.Samples}
> >
{(config, alignedFrame) => { {(builder, alignedFrame) => {
if (oldConfig.current !== config) { if (oldConfig.current !== builder) {
oldConfig.current = addTooltipSupport({ oldConfig.current = addTooltipSupport({
config, config: builder,
onUPlotClick, onUPlotClick,
setFocusedSeriesIdx, setFocusedSeriesIdx,
setFocusedPointIdx, setFocusedPointIdx,
@ -213,13 +217,39 @@ export const StatusHistoryPanel = ({
}); });
} }
if (config.featureToggles.newVizTooltips) {
return ( return (
<> <>
<ZoomPlugin config={config} onZoom={onChangeTimeRange} /> {options.tooltip.mode !== TooltipDisplayMode.None && (
{renderTooltip(alignedFrame)} <TooltipPlugin2
<OutsideRangePlugin config={config} onChangeTimeRange={onChangeTimeRange} /> config={builder}
hoverMode={TooltipHoverMode.xyOne}
queryZoom={onChangeTimeRange}
render={(u, dataIdxs, seriesIdx, isPinned, dismiss) => {
return (
<StatusHistoryTooltip2
data={frames ?? []}
dataIdxs={dataIdxs}
alignedData={alignedFrame}
seriesIdx={seriesIdx}
timeZone={timeZone}
isPinned={isPinned}
/>
);
}}
/>
)}
</> </>
); );
} else {
return (
<>
<ZoomPlugin config={builder} onZoom={onChangeTimeRange} />
{renderTooltip(alignedFrame)}
<OutsideRangePlugin config={builder} onChangeTimeRange={onChangeTimeRange} />
</>
);
}
}} }}
</TimelineChart> </TimelineChart>
); );

View File

@ -0,0 +1,108 @@
import { css } from '@emotion/css';
import React from 'react';
import {
DataFrame,
Field,
formattedValueToString,
getDisplayProcessor,
getFieldDisplayName,
GrafanaTheme2,
TimeZone,
LinkModel,
} from '@grafana/data';
import { useStyles2 } 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';
import { LabelValue } from '@grafana/ui/src/components/VizTooltip/types';
interface StatusHistoryTooltipProps {
data: DataFrame[];
dataIdxs: Array<number | null>;
alignedData: DataFrame;
seriesIdx: number | null | undefined;
timeZone: TimeZone;
isPinned: boolean;
}
function fmt(field: Field, val: number): string {
if (field.display) {
return formattedValueToString(field.display(val));
}
return `${val}`;
}
export const StatusHistoryTooltip2 = ({
data,
dataIdxs,
alignedData,
seriesIdx,
timeZone,
isPinned,
}: StatusHistoryTooltipProps) => {
const styles = useStyles2(getStyles);
// @todo: check other dataIdx, it can be undefined or null in array
const datapointIdx = dataIdxs.find((idx) => idx !== undefined);
if (!data || datapointIdx == null) {
return null;
}
const xField = alignedData.fields[0];
const field = alignedData.fields[seriesIdx!];
const links: Array<LinkModel<Field>> = [];
const linkLookup = new Set<string>();
if (field.getLinks) {
const v = field.values[datapointIdx];
const disp = field.display ? field.display(v) : { text: `${v}`, numeric: +v };
field.getLinks({ calculatedValue: disp, valueRowIndex: datapointIdx }).forEach((link) => {
const key = `${link.title}/${link.href}`;
if (!linkLookup.has(key)) {
links.push(link);
linkLookup.add(key);
}
});
}
const fieldFmt = field.display || getDisplayProcessor();
const value = field.values[datapointIdx!];
const display = fieldFmt(value);
const getHeaderLabel = (): LabelValue => {
return {
label: '',
value: fmt(xField, xField.values[datapointIdx]),
};
};
const getContentLabelValue = () => {
return [
{
label: getFieldDisplayName(field),
value: fmt(field, field.values[datapointIdx]),
color: display.color,
},
];
};
return (
<div className={styles.wrapper}>
<VizTooltipHeader headerLabel={getHeaderLabel()} />
<VizTooltipContent contentLabelValue={getContentLabelValue()} />
{isPinned && <VizTooltipFooter dataLinks={links} canAnnotate={false} />}
</div>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
wrapper: css({
display: 'flex',
flexDirection: 'column',
width: '280px',
}),
});