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
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

@@ -0,0 +1,134 @@
import { Dispatch, MutableRefObject, SetStateAction } from 'react';
import { CartesianCoords2D } from '@grafana/data';
import { positionTooltip } from '../plugins/TooltipPlugin';
import { UPlotConfigBuilder } from './UPlotConfigBuilder';
export type HoverEvent = {
xIndex: number;
yIndex: number;
pageX: number;
pageY: number;
};
type SetupConfigParams = {
config: UPlotConfigBuilder;
onUPlotClick: () => void;
setFocusedSeriesIdx: Dispatch<SetStateAction<number | null>>;
setFocusedPointIdx: Dispatch<SetStateAction<number | 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 addTooltipSupport = ({
config,
onUPlotClick,
setFocusedSeriesIdx,
setFocusedPointIdx,
setCoords,
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) => {
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;
// rect of .u-over (grid area)
config.addHook('syncRect', (u, r) => {
rect = r;
});
const tooltipInterpolator = config.getTooltipInterpolator();
if (tooltipInterpolator) {
config.addHook('setCursor', (u) => {
tooltipInterpolator(
setFocusedSeriesIdx,
setFocusedPointIdx,
(clear) => {
if (clear && !isToolTipOpen.current) {
setCoords(null);
return;
}
if (!rect) {
return;
}
const { x, y } = positionTooltip(u, rect);
if (x !== undefined && y !== undefined && !isToolTipOpen.current) {
setCoords({ canvas: { x: u.cursor.left!, y: u.cursor.top! }, viewport: { x, y } });
}
},
u
);
});
}
config.addHook('setLegend', (u) => {
if (!isToolTipOpen.current) {
setFocusedPointIdx(u.legend.idx!);
}
if (u.cursor.idxs != null) {
for (let i = 0; i < u.cursor.idxs.length; i++) {
const sel = u.cursor.idxs[i];
if (sel != null) {
const hover: HoverEvent = {
xIndex: sel,
yIndex: 0,
pageX: rect.left + u.cursor.left!,
pageY: rect.top + u.cursor.top!,
};
if (!isToolTipOpen.current || !hover) {
setHover(hover);
}
return; // only show the first one
}
}
}
});
config.addHook('setSeries', (_, idx) => {
if (!isToolTipOpen.current) {
setFocusedSeriesIdx(idx);
}
});
return config;
};