Frontend: Make datalinks work with status history and state timeline (#50226)

* fix datalinks for state and status panels

* Add close button on tooltip

* fix links from all series displayed in tooltip

* Allow annotations creation, tweak UI

* Nits

* Remove unused property

* fix returns made from review

* setupUPlotConfig renamed to addTooltipSupport

Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
Co-authored-by: Victor Marin <victor.marin@grafana.com>
This commit is contained in:
J-Loup 2022-08-03 10:58:01 +02:00 committed by GitHub
parent c56aae6f63
commit 2054414d37
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 445 additions and 98 deletions

View File

@ -1,6 +1,6 @@
import { css, cx } from '@emotion/css';
import React, { useState, HTMLAttributes, useMemo, useRef, useLayoutEffect } from 'react';
import useWindowSize from 'react-use/lib/useWindowSize';
import { useWindowSize } from 'react-use';
import { Dimensions2D, GrafanaTheme2 } from '@grafana/data';

View File

@ -1,8 +1,10 @@
import { Dispatch, MutableRefObject, SetStateAction } from 'react';
import { CartesianCoords2D } from '@grafana/data';
import { UPlotConfigBuilder } from '@grafana/ui';
import { positionTooltip } from '@grafana/ui/src/components/uPlot/plugins/TooltipPlugin';
import { positionTooltip } from '../plugins/TooltipPlugin';
import { UPlotConfigBuilder } from './UPlotConfigBuilder';
export type HoverEvent = {
xIndex: number;
@ -16,14 +18,14 @@ type SetupConfigParams = {
onUPlotClick: () => void;
setFocusedSeriesIdx: Dispatch<SetStateAction<number | null>>;
setFocusedPointIdx: Dispatch<SetStateAction<number | null>>;
setCoords: Dispatch<SetStateAction<CartesianCoords2D | null>>;
setCoords: Dispatch<SetStateAction<{ viewport: CartesianCoords2D; canvas: CartesianCoords2D } | null>>;
setHover: Dispatch<SetStateAction<HoverEvent | undefined>>;
isToolTipOpen: MutableRefObject<boolean>;
};
// This applies config hooks to setup tooltip listener. Ideally this could happen in the same `prepConfig` function
// however the GraphNG structures do not allow access to the `setHover` callback
export const setupConfig = ({
export const addTooltipSupport = ({
config,
onUPlotClick,
setFocusedSeriesIdx,
@ -32,13 +34,37 @@ export const setupConfig = ({
setHover,
isToolTipOpen,
}: SetupConfigParams): UPlotConfigBuilder => {
// Ensure tooltip is closed on config changes
isToolTipOpen.current = false;
var onMouseLeave = () => {
if (!isToolTipOpen.current) {
setCoords(null);
}
};
let ref_parent: HTMLElement | null = null;
let ref_over: HTMLElement | null = null;
config.addHook('init', (u) => {
u.root.parentElement?.addEventListener('click', onUPlotClick);
u.over.addEventListener('mouseleave', () => {
if (!isToolTipOpen.current) {
setCoords(null);
}
});
ref_parent = u.root.parentElement;
ref_over = u.over;
ref_parent?.addEventListener('click', onUPlotClick);
ref_over.addEventListener('mouseleave', onMouseLeave);
});
var clearPopupIfOpened = () => {
if (isToolTipOpen.current) {
setCoords(null);
onUPlotClick();
}
};
config.addHook('drawClear', clearPopupIfOpened);
config.addHook('destroy', () => {
ref_parent?.removeEventListener('click', onUPlotClick);
ref_over?.removeEventListener('mouseleave', onMouseLeave);
clearPopupIfOpened();
});
let rect: DOMRect;
@ -65,7 +91,7 @@ export const setupConfig = ({
const { x, y } = positionTooltip(u, rect);
if (x !== undefined && y !== undefined && !isToolTipOpen.current) {
setCoords({ x, y });
setCoords({ canvas: { x: u.cursor.left!, y: u.cursor.top! }, viewport: { x, y } });
}
},
u

View File

@ -1,4 +1,3 @@
import { css } from '@emotion/css';
import React, { useMemo, useRef, useState } from 'react';
import {
@ -6,7 +5,6 @@ import {
compareDataFrameStructures,
DataFrame,
getFieldDisplayName,
GrafanaTheme2,
PanelProps,
TimeRange,
VizOrientation,
@ -22,19 +20,18 @@ import {
UPlotConfigBuilder,
UPLOT_AXIS_FONT_SIZE,
usePanelContext,
useStyles2,
useTheme2,
VizLayout,
VizLegend,
VizTooltipContainer,
} from '@grafana/ui';
import { PropDiffFn } from '@grafana/ui/src/components/GraphNG/GraphNG';
import { HoverEvent, addTooltipSupport } from '@grafana/ui/src/components/uPlot/config/addTooltipSupport';
import { CloseButton } from 'app/core/components/CloseButton/CloseButton';
import { DataHoverView } from '../geomap/components/DataHoverView';
import { getFieldLegendItem } from '../state-timeline/utils';
import { HoverEvent, setupConfig } from './config';
import { PanelOptions } from './models.gen';
import { prepareBarChartDisplayValues, preparePlotConfigBuilder } from './utils';
@ -75,14 +72,13 @@ export const BarChartPanel: React.FunctionComponent<Props> = ({
id,
}) => {
const theme = useTheme2();
const styles = useStyles2(getStyles);
const { eventBus } = usePanelContext();
const oldConfig = useRef<UPlotConfigBuilder | undefined>(undefined);
const isToolTipOpen = useRef<boolean>(false);
const [hover, setHover] = useState<HoverEvent | undefined>(undefined);
const [coords, setCoords] = useState<CartesianCoords2D | null>(null);
const [coords, setCoords] = useState<{ viewport: CartesianCoords2D; canvas: CartesianCoords2D } | null>(null);
const [focusedSeriesIdx, setFocusedSeriesIdx] = useState<number | null>(null);
const [focusedPointIdx, setFocusedPointIdx] = useState<number | null>(null);
const [shouldDisplayCloseButton, setShouldDisplayCloseButton] = useState<boolean>(false);
@ -105,6 +101,7 @@ export const BarChartPanel: React.FunctionComponent<Props> = ({
const chartDisplay = 'viz' in info ? info : null;
const structureRef = useRef(10000);
useMemo(() => {
structureRef.current++;
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -166,10 +163,23 @@ export const BarChartPanel: React.FunctionComponent<Props> = ({
return (
<>
{shouldDisplayCloseButton && (
<>
<CloseButton onClick={onCloseToolTip} />
<div className={styles.closeButtonSpacer} />
</>
<div
style={{
width: '100%',
display: 'flex',
justifyContent: 'flex-end',
}}
>
<CloseButton
onClick={onCloseToolTip}
style={{
position: 'relative',
top: 'auto',
right: 'auto',
marginRight: 0,
}}
/>
</div>
)}
<DataHoverView
data={info.aligned}
@ -275,7 +285,7 @@ export const BarChartPanel: React.FunctionComponent<Props> = ({
>
{(config) => {
if (oldConfig.current !== config) {
oldConfig.current = setupConfig({
oldConfig.current = addTooltipSupport({
config,
onUPlotClick,
setFocusedSeriesIdx,
@ -294,7 +304,7 @@ export const BarChartPanel: React.FunctionComponent<Props> = ({
<Portal>
{hover && coords && (
<VizTooltipContainer
position={{ x: coords.x, y: coords.y }}
position={{ x: coords.viewport.x, y: coords.viewport.y }}
offset={{ x: TOOLTIP_OFFSET, y: TOOLTIP_OFFSET }}
allowPointerEvents={isToolTipOpen.current}
>
@ -307,9 +317,3 @@ export const BarChartPanel: React.FunctionComponent<Props> = ({
</GraphNG>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
closeButtonSpacer: css`
margin-bottom: 15px;
`,
});

View File

@ -200,10 +200,23 @@ export const HeatmapPanel: React.FC<HeatmapPanelProps> = ({
allowPointerEvents={isToolTipOpen.current}
>
{shouldDisplayCloseButton && (
<>
<CloseButton onClick={onCloseToolTip} />
<div className={styles.closeButtonSpacer} />
</>
<div
style={{
width: '100%',
display: 'flex',
justifyContent: 'flex-end',
}}
>
<CloseButton
onClick={onCloseToolTip}
style={{
position: 'relative',
top: 'auto',
right: 'auto',
marginRight: 0,
}}
/>
</div>
)}
<HeatmapHoverView data={info} hover={hover} showHistogram={options.tooltip.yHistogram} />
</VizTooltipContainer>
@ -214,9 +227,6 @@ export const HeatmapPanel: React.FC<HeatmapPanelProps> = ({
};
const getStyles = (theme: GrafanaTheme2) => ({
closeButtonSpacer: css`
margin-bottom: 15px;
`,
colorScaleWrapper: css`
margin-left: 25px;
padding: 10px 0;

View File

@ -1,12 +1,13 @@
import React, { useCallback, useMemo } from 'react';
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { DataFrame, FieldType, PanelProps } from '@grafana/data';
import { TooltipPlugin, useTheme2, ZoomPlugin, usePanelContext } from '@grafana/ui';
import { CartesianCoords2D, DataFrame, FieldType, PanelProps } from '@grafana/data';
import { Portal, UPlotConfigBuilder, usePanelContext, useTheme2, VizTooltipContainer, ZoomPlugin } from '@grafana/ui';
import { HoverEvent, addTooltipSupport } from '@grafana/ui/src/components/uPlot/config/addTooltipSupport';
import { CloseButton } from 'app/core/components/CloseButton/CloseButton';
import { getLastStreamingDataFramePacket } from 'app/features/live/data/StreamingDataFrame';
import { AnnotationEditorPlugin } from '../timeseries/plugins/AnnotationEditorPlugin';
import { AnnotationsPlugin } from '../timeseries/plugins/AnnotationsPlugin';
import { ContextMenuPlugin } from '../timeseries/plugins/ContextMenuPlugin';
import { OutsideRangePlugin } from '../timeseries/plugins/OutsideRangePlugin';
import { getTimezones } from '../timeseries/utils';
@ -15,6 +16,8 @@ import { TimelineChart } from './TimelineChart';
import { TimelineMode, TimelineOptions } from './types';
import { prepareTimelineFields, prepareTimelineLegendItems } from './utils';
const TOOLTIP_OFFSET = 10;
interface TimelinePanelProps extends PanelProps<TimelineOptions> {}
/**
@ -31,7 +34,28 @@ export const StateTimelinePanel: React.FC<TimelinePanelProps> = ({
onChangeTimeRange,
}) => {
const theme = useTheme2();
const { sync, canAddAnnotations } = usePanelContext();
const oldConfig = useRef<UPlotConfigBuilder | undefined>(undefined);
const isToolTipOpen = useRef<boolean>(false);
const [hover, setHover] = useState<HoverEvent | undefined>(undefined);
const [coords, setCoords] = useState<{ viewport: CartesianCoords2D; canvas: CartesianCoords2D } | null>(null);
const [focusedSeriesIdx, setFocusedSeriesIdx] = useState<number | null>(null);
const [focusedPointIdx, setFocusedPointIdx] = useState<number | null>(null);
const [shouldDisplayCloseButton, setShouldDisplayCloseButton] = useState<boolean>(false);
const { canAddAnnotations } = usePanelContext();
const onCloseToolTip = () => {
isToolTipOpen.current = false;
setCoords(null);
setShouldDisplayCloseButton(false);
};
const onUPlotClick = () => {
isToolTipOpen.current = !isToolTipOpen.current;
// Linking into useState required to re-render tooltip
setShouldDisplayCloseButton(isToolTipOpen.current);
};
const { frames, warn } = useMemo(
() => prepareTimelineFields(data?.series, options.mergeValues ?? true, timeRange, theme),
@ -46,7 +70,7 @@ export const StateTimelinePanel: React.FC<TimelinePanelProps> = ({
const timezones = useMemo(() => getTimezones(options.timezones, timeZone), [options.timezones, timeZone]);
const renderCustomTooltip = useCallback(
(alignedData: DataFrame, seriesIdx: number | null, datapointIdx: number | null) => {
(alignedData: DataFrame, seriesIdx: number | null, datapointIdx: number | null, onAnnotationAdd?: () => void) => {
const data = frames ?? [];
// Count value fields in the state-timeline-ready frame
const valueFieldsCount = data.reduce(
@ -73,16 +97,38 @@ export const StateTimelinePanel: React.FC<TimelinePanelProps> = ({
}
return (
<StateTimelineTooltip
data={data}
alignedData={alignedData}
seriesIdx={seriesIdx}
datapointIdx={datapointIdx}
timeZone={timeZone}
/>
<>
{shouldDisplayCloseButton && (
<div
style={{
width: '100%',
display: 'flex',
justifyContent: 'flex-end',
}}
>
<CloseButton
onClick={onCloseToolTip}
style={{
position: 'relative',
top: 'auto',
right: 'auto',
marginRight: 0,
}}
/>
</div>
)}
<StateTimelineTooltip
data={data}
alignedData={alignedData}
seriesIdx={seriesIdx}
datapointIdx={datapointIdx}
timeZone={timeZone}
onAnnotationAdd={onAnnotationAdd}
/>
</>
);
},
[timeZone, frames]
[timeZone, frames, shouldDisplayCloseButton]
);
if (!frames || warn) {
@ -115,53 +161,60 @@ export const StateTimelinePanel: React.FC<TimelinePanelProps> = ({
mode={TimelineMode.Changes}
>
{(config, alignedFrame) => {
if (oldConfig.current !== config) {
oldConfig.current = addTooltipSupport({
config,
onUPlotClick,
setFocusedSeriesIdx,
setFocusedPointIdx,
setCoords,
setHover,
isToolTipOpen,
});
}
return (
<>
<ZoomPlugin config={config} onZoom={onChangeTimeRange} />
<TooltipPlugin
data={alignedFrame}
sync={sync}
config={config}
mode={options.tooltip.mode}
timeZone={timeZone}
renderTooltip={renderCustomTooltip}
/>
<OutsideRangePlugin config={config} onChangeTimeRange={onChangeTimeRange} />
{data.annotations && (
<AnnotationsPlugin annotations={data.annotations} config={config} timeZone={timeZone} />
)}
{enableAnnotationCreation && (
{enableAnnotationCreation ? (
<AnnotationEditorPlugin data={alignedFrame} timeZone={timeZone} config={config}>
{({ startAnnotating }) => {
return (
<ContextMenuPlugin
data={alignedFrame}
config={config}
timeZone={timeZone}
replaceVariables={replaceVariables}
defaultItems={[
{
items: [
{
label: 'Add annotation',
ariaLabel: 'Add annotation',
icon: 'comment-alt',
onClick: (e, p) => {
if (!p) {
return;
}
startAnnotating({ coords: p.coords });
},
},
],
},
]}
/>
<Portal>
{hover && coords && (
<VizTooltipContainer
position={{ x: coords.viewport.x, y: coords.viewport.y }}
offset={{ x: TOOLTIP_OFFSET, y: TOOLTIP_OFFSET }}
allowPointerEvents={isToolTipOpen.current}
>
{renderCustomTooltip(alignedFrame, focusedSeriesIdx, focusedPointIdx, () => {
startAnnotating({ coords: { plotCanvas: coords.canvas, viewport: coords.viewport } });
onCloseToolTip();
})}
</VizTooltipContainer>
)}
</Portal>
);
}}
</AnnotationEditorPlugin>
) : (
<Portal>
{hover && coords && (
<VizTooltipContainer
position={{ x: coords.viewport.x, y: coords.viewport.y }}
offset={{ x: TOOLTIP_OFFSET, y: TOOLTIP_OFFSET }}
allowPointerEvents={isToolTipOpen.current}
>
{renderCustomTooltip(alignedFrame, focusedSeriesIdx, focusedPointIdx)}
</VizTooltipContainer>
)}
</Portal>
)}
</>
);

View File

@ -1,7 +1,15 @@
import React from 'react';
import { DataFrame, FALLBACK_COLOR, getDisplayProcessor, getFieldDisplayName, TimeZone } from '@grafana/data';
import { SeriesTableRow, useTheme2 } from '@grafana/ui';
import {
DataFrame,
FALLBACK_COLOR,
Field,
getDisplayProcessor,
getFieldDisplayName,
TimeZone,
LinkModel,
} from '@grafana/data';
import { MenuItem, SeriesTableRow, useTheme2 } from '@grafana/ui';
import { findNextStateIndex, fmtDuration } from './utils';
@ -11,6 +19,7 @@ interface StateTimelineTooltipProps {
seriesIdx: number;
datapointIdx: number;
timeZone: TimeZone;
onAnnotationAdd?: () => void;
}
export const StateTimelineTooltip: React.FC<StateTimelineTooltipProps> = ({
@ -19,14 +28,34 @@ export const StateTimelineTooltip: React.FC<StateTimelineTooltipProps> = ({
seriesIdx,
datapointIdx,
timeZone,
onAnnotationAdd,
}) => {
const theme = useTheme2();
const xField = alignedData.fields[0];
const xFieldFmt = xField.display || getDisplayProcessor({ field: xField, timeZone, theme });
if (!data || datapointIdx == null) {
return null;
}
const field = alignedData.fields[seriesIdx!];
const links: Array<LinkModel<Field>> = [];
const linkLookup = new Set<string>();
if (field.getLinks) {
const v = field.values.get(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 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.get(datapointIdx!);
@ -66,13 +95,34 @@ export const StateTimelineTooltip: React.FC<StateTimelineTooltipProps> = ({
}
return (
<div style={{ fontSize: theme.typography.bodySmall.fontSize }}>
{fieldDisplayName}
<br />
<SeriesTableRow label={display.text} color={display.color || FALLBACK_COLOR} isActive />
From <strong>{xFieldFmt(xField.values.get(datapointIdx!)).text}</strong>
{toFragment}
{durationFragment}
<div>
<div style={{ fontSize: theme.typography.bodySmall.fontSize }}>
{fieldDisplayName}
<br />
<SeriesTableRow label={display.text} color={display.color || FALLBACK_COLOR} isActive />
From <strong>{xFieldFmt(xField.values.get(datapointIdx!)).text}</strong>
{toFragment}
{durationFragment}
</div>
<div
style={{
margin: theme.spacing(1, -1, -1, -1),
borderTop: `1px solid ${theme.colors.border.weak}`,
}}
>
{onAnnotationAdd && <MenuItem label={'Add annotation'} icon={'comment-alt'} onClick={onAnnotationAdd} />}
{links.length > 0 &&
links.map((link, i) => (
<MenuItem
key={i}
icon={'external-link-alt'}
target={link.target}
label={link.title}
url={link.href}
onClick={link.onClick}
/>
))}
</div>
</div>
);
};

View File

@ -1,7 +1,9 @@
import React, { useMemo } from 'react';
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { PanelProps } from '@grafana/data';
import { TooltipPlugin, useTheme2, ZoomPlugin } from '@grafana/ui';
import { CartesianCoords2D, DataFrame, FieldType, PanelProps } from '@grafana/data';
import { Portal, UPlotConfigBuilder, useTheme2, VizTooltipContainer, ZoomPlugin } from '@grafana/ui';
import { HoverEvent, addTooltipSupport } from '@grafana/ui/src/components/uPlot/config/addTooltipSupport';
import { CloseButton } from 'app/core/components/CloseButton/CloseButton';
import { TimelineChart } from '../state-timeline/TimelineChart';
import { TimelineMode } from '../state-timeline/types';
@ -9,8 +11,11 @@ import { prepareTimelineFields, prepareTimelineLegendItems } from '../state-time
import { OutsideRangePlugin } from '../timeseries/plugins/OutsideRangePlugin';
import { getTimezones } from '../timeseries/utils';
import { StatusHistoryTooltip } from './StatusHistoryTooltip';
import { StatusPanelOptions } from './types';
const TOOLTIP_OFFSET = 10;
interface TimelinePanelProps extends PanelProps<StatusPanelOptions> {}
/**
@ -27,6 +32,28 @@ export const StatusHistoryPanel: React.FC<TimelinePanelProps> = ({
}) => {
const theme = useTheme2();
const oldConfig = useRef<UPlotConfigBuilder | undefined>(undefined);
const isToolTipOpen = useRef<boolean>(false);
const [hover, setHover] = useState<HoverEvent | undefined>(undefined);
const [coords, setCoords] = useState<{ viewport: CartesianCoords2D; canvas: CartesianCoords2D } | null>(null);
const [focusedSeriesIdx, setFocusedSeriesIdx] = useState<number | null>(null);
const [focusedPointIdx, setFocusedPointIdx] = useState<number | null>(null);
const [shouldDisplayCloseButton, setShouldDisplayCloseButton] = useState<boolean>(false);
const onCloseToolTip = () => {
isToolTipOpen.current = false;
setCoords(null);
setShouldDisplayCloseButton(false);
};
const onUPlotClick = () => {
isToolTipOpen.current = !isToolTipOpen.current;
// Linking into useState required to re-render tooltip
setShouldDisplayCloseButton(isToolTipOpen.current);
};
const { frames, warn } = useMemo(
() => prepareTimelineFields(data?.series, false, timeRange, theme),
[data, timeRange, theme]
@ -37,6 +64,68 @@ export const StatusHistoryPanel: React.FC<TimelinePanelProps> = ({
[frames, options.legend, theme]
);
const renderCustomTooltip = useCallback(
(alignedData: DataFrame, seriesIdx: number | null, datapointIdx: number | null) => {
const data = frames ?? [];
// Count value fields in the state-timeline-ready frame
const valueFieldsCount = data.reduce(
(acc, frame) => acc + frame.fields.filter((field) => field.type !== FieldType.time).length,
0
);
// Not caring about multi mode in StatusHistory
if (seriesIdx === null || datapointIdx === null) {
return null;
}
/**
* There could be a case when the tooltip shows a data from one of a multiple query and the other query finishes first
* from refreshing. This causes data to be out of sync. alignedData - 1 because Time field doesn't count.
* Render nothing in this case to prevent error.
* See https://github.com/grafana/support-escalations/issues/932
*/
if (
(!alignedData.meta?.transformations?.length && alignedData.fields.length - 1 !== valueFieldsCount) ||
!alignedData.fields[seriesIdx]
) {
return null;
}
return (
<>
{shouldDisplayCloseButton && (
<div
style={{
width: '100%',
display: 'flex',
justifyContent: 'flex-end',
}}
>
<CloseButton
onClick={onCloseToolTip}
style={{
position: 'relative',
top: 'auto',
right: 'auto',
marginRight: 0,
}}
/>
</div>
)}
<StatusHistoryTooltip
data={data}
alignedData={alignedData}
seriesIdx={seriesIdx}
datapointIdx={datapointIdx}
timeZone={timeZone}
/>
</>
);
},
[timeZone, frames, shouldDisplayCloseButton]
);
const timezones = useMemo(() => getTimezones(options.timezones, timeZone), [options.timezones, timeZone]);
if (!frames || warn) {
@ -74,10 +163,31 @@ export const StatusHistoryPanel: React.FC<TimelinePanelProps> = ({
mode={TimelineMode.Samples}
>
{(config, alignedFrame) => {
if (oldConfig.current !== config) {
oldConfig.current = addTooltipSupport({
config,
onUPlotClick,
setFocusedSeriesIdx,
setFocusedPointIdx,
setCoords,
setHover,
isToolTipOpen,
});
}
return (
<>
<ZoomPlugin config={config} onZoom={onChangeTimeRange} />
<TooltipPlugin data={alignedFrame} config={config} mode={options.tooltip.mode} timeZone={timeZone} />
<Portal>
{hover && coords && (
<VizTooltipContainer
position={{ x: coords.viewport.x, y: coords.viewport.y }}
offset={{ x: TOOLTIP_OFFSET, y: TOOLTIP_OFFSET }}
allowPointerEvents={isToolTipOpen.current}
>
{renderCustomTooltip(alignedFrame, focusedSeriesIdx, focusedPointIdx)}
</VizTooltipContainer>
)}
</Portal>
<OutsideRangePlugin config={config} onChangeTimeRange={onChangeTimeRange} />
</>
);

View File

@ -0,0 +1,94 @@
import React from 'react';
import {
DataFrame,
FALLBACK_COLOR,
Field,
getDisplayProcessor,
getFieldDisplayName,
TimeZone,
LinkModel,
} from '@grafana/data';
import { MenuItem, SeriesTableRow, useTheme2 } from '@grafana/ui';
interface StatusHistoryTooltipProps {
data: DataFrame[];
alignedData: DataFrame;
seriesIdx: number;
datapointIdx: number;
timeZone: TimeZone;
}
export const StatusHistoryTooltip: React.FC<StatusHistoryTooltipProps> = ({
data,
alignedData,
seriesIdx,
datapointIdx,
timeZone,
}) => {
const theme = useTheme2();
if (!data || datapointIdx == null) {
return null;
}
const field = alignedData.fields[seriesIdx!];
const links: Array<LinkModel<Field>> = [];
const linkLookup = new Set<string>();
if (field.getLinks) {
const v = field.values.get(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 dataFrameFieldIndex = field.state?.origin;
const fieldFmt = field.display || getDisplayProcessor({ field, timeZone, theme });
const value = field.values.get(datapointIdx!);
const display = fieldFmt(value);
const fieldDisplayName = dataFrameFieldIndex
? getFieldDisplayName(
data[dataFrameFieldIndex.frameIndex].fields[dataFrameFieldIndex.fieldIndex],
data[dataFrameFieldIndex.frameIndex],
data
)
: null;
return (
<div>
<div style={{ fontSize: theme.typography.bodySmall.fontSize }}>
{fieldDisplayName}
<br />
<SeriesTableRow label={display.text} color={display.color || FALLBACK_COLOR} isActive />
</div>
{links.length > 0 && (
<div
style={{
margin: theme.spacing(1, -1, -1, -1),
borderTop: `1px solid ${theme.colors.border.weak}`,
}}
>
{links.map((link, i) => (
<MenuItem
key={i}
icon={'external-link-alt'}
target={link.target}
label={link.title}
url={link.href}
onClick={link.onClick}
/>
))}
</div>
)}
</div>
);
};
StatusHistoryTooltip.displayName = 'StatusHistoryTooltip';