VizTooltips: Use global portal (#81986)
Co-authored-by: Adela Almasan <adela.almasan@grafana.com>
This commit is contained in:
co-authored by
Adela Almasan
parent
e4276a4ede
commit
6e6b9a62a2
@@ -1,12 +1,12 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import React, { useLayoutEffect, useRef, useReducer, CSSProperties, useContext, useEffect } from 'react';
|
||||
import React, { useLayoutEffect, useRef, useReducer, CSSProperties } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
import { useStyles2 } from '../../../themes';
|
||||
import { LayoutItemContext } from '../../Layout/LayoutItemContext';
|
||||
import { getPortalContainer } from '../../Portal/Portal';
|
||||
import { UPlotConfigBuilder } from '../config/UPlotConfigBuilder';
|
||||
|
||||
import { CloseButton } from './CloseButton';
|
||||
@@ -29,6 +29,8 @@ interface TooltipPlugin2Props {
|
||||
config: UPlotConfigBuilder;
|
||||
hoverMode: TooltipHoverMode;
|
||||
|
||||
syncTooltip?: () => boolean;
|
||||
|
||||
// x only
|
||||
queryZoom?: (range: { from: number; to: number }) => void;
|
||||
// y-only, via shiftKey
|
||||
@@ -80,14 +82,16 @@ function mergeState(prevState: TooltipContainerState, nextState: Partial<Tooltip
|
||||
};
|
||||
}
|
||||
|
||||
const INITIAL_STATE: TooltipContainerState = {
|
||||
style: { transform: '', pointerEvents: 'none' },
|
||||
isHovering: false,
|
||||
isPinned: false,
|
||||
contents: null,
|
||||
plot: null,
|
||||
dismiss: () => {},
|
||||
};
|
||||
function initState(): TooltipContainerState {
|
||||
return {
|
||||
style: { transform: '', pointerEvents: 'none' },
|
||||
isHovering: false,
|
||||
isPinned: false,
|
||||
contents: null,
|
||||
plot: null,
|
||||
dismiss: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
// min px width that triggers zoom
|
||||
const MIN_ZOOM_DIST = 5;
|
||||
@@ -105,13 +109,16 @@ export const TooltipPlugin2 = ({
|
||||
queryZoom,
|
||||
maxWidth,
|
||||
maxHeight,
|
||||
syncTooltip = () => false,
|
||||
}: TooltipPlugin2Props) => {
|
||||
const domRef = useRef<HTMLDivElement>(null);
|
||||
const portalRoot = useRef<HTMLElement | null>(null);
|
||||
|
||||
const [{ plot, isHovering, isPinned, contents, style, dismiss }, setState] = useReducer(mergeState, INITIAL_STATE);
|
||||
if (portalRoot.current == null) {
|
||||
portalRoot.current = getPortalContainer();
|
||||
}
|
||||
|
||||
const { boostZIndex } = useContext(LayoutItemContext);
|
||||
useEffect(() => (isPinned ? boostZIndex() : undefined), [isPinned]);
|
||||
const [{ plot, isHovering, isPinned, contents, style, dismiss }, setState] = useReducer(mergeState, null, initState);
|
||||
|
||||
const sizeRef = useRef<TooltipContainerSize>();
|
||||
|
||||
@@ -150,20 +157,19 @@ export const TooltipPlugin2 = ({
|
||||
let _isPinned = isPinned;
|
||||
let _style = style;
|
||||
|
||||
let plotVisible = false;
|
||||
|
||||
const updateHovering = () => {
|
||||
_isHovering = closestSeriesIdx != null || (hoverMode === TooltipHoverMode.xAll && _someSeriesIdx);
|
||||
if (viaSync) {
|
||||
_isHovering = plotVisible && _someSeriesIdx && syncTooltip();
|
||||
} else {
|
||||
_isHovering = closestSeriesIdx != null || (hoverMode === TooltipHoverMode.xAll && _someSeriesIdx);
|
||||
}
|
||||
};
|
||||
|
||||
let offsetX = 0;
|
||||
let offsetY = 0;
|
||||
|
||||
let containRect = {
|
||||
lft: 0,
|
||||
top: 0,
|
||||
rgt: screen.width,
|
||||
btm: screen.height,
|
||||
};
|
||||
|
||||
let selectedRange: TimeRange2 | null = null;
|
||||
let seriesIdxs: Array<number | null> = plot?.cursor.idxs!.slice()!;
|
||||
let closestSeriesIdx: number | null = null;
|
||||
@@ -231,7 +237,6 @@ export const TooltipPlugin2 = ({
|
||||
setState(state);
|
||||
|
||||
selectedRange = null;
|
||||
viaSync = false;
|
||||
};
|
||||
|
||||
const dismiss = () => {
|
||||
@@ -293,58 +298,11 @@ export const TooltipPlugin2 = ({
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const haltAncestorId = 'pageContent';
|
||||
const scrollbarWidth = 16;
|
||||
|
||||
// if we're in a container that can clip the tooltip, we should try to stay within that rather than window edges
|
||||
u.over.addEventListener(
|
||||
'mouseenter',
|
||||
() => {
|
||||
// clamp to viewport bounds
|
||||
let htmlEl = document.documentElement;
|
||||
let winWid = htmlEl.clientWidth - scrollbarWidth;
|
||||
let winHgt = htmlEl.clientHeight - scrollbarWidth;
|
||||
|
||||
let lft = 0,
|
||||
top = 0,
|
||||
rgt = winWid,
|
||||
btm = winHgt;
|
||||
|
||||
// find nearest scrollable container where overflow is not visible, (stop at #pageContent)
|
||||
let par: HTMLElement | null = u.root;
|
||||
|
||||
while (par != null && par.id !== haltAncestorId) {
|
||||
let style = getComputedStyle(par);
|
||||
let overflowX = style.getPropertyValue('overflow-x');
|
||||
let overflowY = style.getPropertyValue('overflow-y');
|
||||
|
||||
if (overflowX !== 'visible' || overflowY !== 'visible') {
|
||||
let rect = par.getBoundingClientRect();
|
||||
lft = Math.max(rect.x, lft);
|
||||
top = Math.max(rect.y, top);
|
||||
rgt = Math.min(lft + rect.width, rgt);
|
||||
btm = Math.min(top + rect.height, btm);
|
||||
break;
|
||||
}
|
||||
|
||||
par = par.parentElement;
|
||||
}
|
||||
|
||||
containRect.lft = lft;
|
||||
containRect.top = top;
|
||||
containRect.rgt = rgt;
|
||||
containRect.btm = btm;
|
||||
},
|
||||
{ capture: true }
|
||||
);
|
||||
});
|
||||
|
||||
config.addHook('setSelect', (u) => {
|
||||
let e = u.cursor!.event;
|
||||
|
||||
if (e != null && (clientZoom || queryZoom != null)) {
|
||||
if (maybeZoomAction(e)) {
|
||||
if (!viaSync && (clientZoom || queryZoom != null)) {
|
||||
if (maybeZoomAction(u.cursor!.event)) {
|
||||
if (clientZoom && yDrag) {
|
||||
if (u.select.height >= MIN_ZOOM_DIST) {
|
||||
for (let key in u.scales!) {
|
||||
@@ -427,6 +385,8 @@ export const TooltipPlugin2 = ({
|
||||
// TODO: we only need this for multi/all mode?
|
||||
config.addHook('setSeries', (u, seriesIdx) => {
|
||||
closestSeriesIdx = seriesIdx;
|
||||
|
||||
viaSync = u.cursor.event == null;
|
||||
updateHovering();
|
||||
scheduleRender();
|
||||
});
|
||||
@@ -436,78 +396,107 @@ export const TooltipPlugin2 = ({
|
||||
seriesIdxs = _plot?.cursor!.idxs!.slice()!;
|
||||
_someSeriesIdx = seriesIdxs.some((v, i) => i > 0 && v != null);
|
||||
|
||||
viaSync = u.cursor.event == null;
|
||||
updateHovering();
|
||||
scheduleRender();
|
||||
});
|
||||
|
||||
const scrollbarWidth = 16;
|
||||
let winWid = 0;
|
||||
let winHgt = 0;
|
||||
|
||||
const updateWinSize = () => {
|
||||
_isHovering && !_isPinned && dismiss();
|
||||
|
||||
winWid = window.innerWidth - scrollbarWidth;
|
||||
winHgt = window.innerHeight - scrollbarWidth;
|
||||
};
|
||||
|
||||
const updatePlotVisible = () => {
|
||||
plotVisible =
|
||||
_plot!.rect.bottom <= winHgt && _plot!.rect.top >= 0 && _plot!.rect.left >= 0 && _plot!.rect.right <= winWid;
|
||||
};
|
||||
|
||||
updateWinSize();
|
||||
config.addHook('ready', updatePlotVisible);
|
||||
|
||||
// fires on mousemoves
|
||||
config.addHook('setCursor', (u) => {
|
||||
let { left = -10, top = -10, event } = u.cursor;
|
||||
viaSync = u.cursor.event == null;
|
||||
|
||||
if (!_isHovering) {
|
||||
return;
|
||||
}
|
||||
|
||||
let { left = -10, top = -10 } = u.cursor;
|
||||
|
||||
if (left >= 0 || top >= 0) {
|
||||
viaSync = event == null;
|
||||
let clientX = u.rect.left + left;
|
||||
let clientY = u.rect.top + top;
|
||||
|
||||
let transform = '';
|
||||
|
||||
// this means it's a synthetic event from uPlot's sync
|
||||
if (viaSync) {
|
||||
// TODO: smarter positioning here to avoid viewport clipping?
|
||||
transform = `translateX(${left}px) translateY(${u.rect.height / 2}px) translateY(-50%)`;
|
||||
let { width, height } = sizeRef.current!;
|
||||
|
||||
width += TOOLTIP_OFFSET;
|
||||
height += TOOLTIP_OFFSET;
|
||||
|
||||
if (offsetY !== 0) {
|
||||
if (clientY + height < winHgt || clientY - height < 0) {
|
||||
offsetY = 0;
|
||||
} else if (offsetY !== -height) {
|
||||
offsetY = -height;
|
||||
}
|
||||
} else {
|
||||
let { width, height } = sizeRef.current!;
|
||||
|
||||
width += TOOLTIP_OFFSET;
|
||||
height += TOOLTIP_OFFSET;
|
||||
|
||||
let clientX = u.rect.left + left;
|
||||
let clientY = u.rect.top + top;
|
||||
|
||||
if (offsetY !== 0) {
|
||||
if (clientY + height < containRect.btm || clientY - height < 0) {
|
||||
offsetY = 0;
|
||||
} else if (offsetY !== -height) {
|
||||
offsetY = -height;
|
||||
}
|
||||
} else {
|
||||
if (clientY + height > containRect.btm && clientY - height >= 0) {
|
||||
offsetY = -height;
|
||||
}
|
||||
if (clientY + height > winHgt && clientY - height >= 0) {
|
||||
offsetY = -height;
|
||||
}
|
||||
|
||||
if (offsetX !== 0) {
|
||||
if (clientX + width < containRect.rgt || clientX - width < 0) {
|
||||
offsetX = 0;
|
||||
} else if (offsetX !== -width) {
|
||||
offsetX = -width;
|
||||
}
|
||||
} else {
|
||||
if (clientX + width > containRect.rgt && clientX - width >= 0) {
|
||||
offsetX = -width;
|
||||
}
|
||||
}
|
||||
|
||||
const shiftX = left + (offsetX === 0 ? TOOLTIP_OFFSET : -TOOLTIP_OFFSET);
|
||||
const shiftY = top + (offsetY === 0 ? TOOLTIP_OFFSET : -TOOLTIP_OFFSET);
|
||||
|
||||
const reflectX = offsetX === 0 ? '' : 'translateX(-100%)';
|
||||
const reflectY = offsetY === 0 ? '' : 'translateY(-100%)';
|
||||
|
||||
// TODO: to a transition only when switching sides
|
||||
// transition: transform 100ms;
|
||||
|
||||
transform = `translateX(${shiftX}px) ${reflectX} translateY(${shiftY}px) ${reflectY}`;
|
||||
}
|
||||
|
||||
if (_isHovering) {
|
||||
if (domRef.current != null) {
|
||||
domRef.current.style.transform = transform;
|
||||
} else {
|
||||
_style.transform = transform;
|
||||
scheduleRender();
|
||||
if (offsetX !== 0) {
|
||||
if (clientX + width < winWid || clientX - width < 0) {
|
||||
offsetX = 0;
|
||||
} else if (offsetX !== -width) {
|
||||
offsetX = -width;
|
||||
}
|
||||
} else {
|
||||
if (clientX + width > winWid && clientX - width >= 0) {
|
||||
offsetX = -width;
|
||||
}
|
||||
}
|
||||
|
||||
const shiftX = clientX + (offsetX === 0 ? TOOLTIP_OFFSET : -TOOLTIP_OFFSET);
|
||||
const shiftY = clientY + (offsetY === 0 ? TOOLTIP_OFFSET : -TOOLTIP_OFFSET);
|
||||
|
||||
const reflectX = offsetX === 0 ? '' : 'translateX(-100%)';
|
||||
const reflectY = offsetY === 0 ? '' : 'translateY(-100%)';
|
||||
|
||||
// TODO: to a transition only when switching sides
|
||||
// transition: transform 100ms;
|
||||
|
||||
transform = `translateX(${shiftX}px) ${reflectX} translateY(${shiftY}px) ${reflectY}`;
|
||||
|
||||
if (domRef.current != null) {
|
||||
domRef.current.style.transform = transform;
|
||||
} else {
|
||||
_style.transform = transform;
|
||||
scheduleRender();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const onscroll = () => {
|
||||
updatePlotVisible();
|
||||
_isHovering && !_isPinned && dismiss();
|
||||
};
|
||||
|
||||
window.addEventListener('resize', updateWinSize);
|
||||
window.addEventListener('scroll', onscroll, true);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', updateWinSize);
|
||||
window.removeEventListener('scroll', onscroll, true);
|
||||
};
|
||||
}, [config]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
@@ -524,7 +513,7 @@ export const TooltipPlugin2 = ({
|
||||
{isPinned && <CloseButton onClick={dismiss} />}
|
||||
{contents}
|
||||
</div>,
|
||||
plot.over
|
||||
portalRoot.current
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -66,15 +66,16 @@ export function addTooltipOptions<T extends OptionsWithTooltip>(
|
||||
settings: {
|
||||
integer: true,
|
||||
},
|
||||
showIf: (options: T) => options.tooltip?.mode !== TooltipDisplayMode.None,
|
||||
showIf: (options: T) => false, // options.tooltip?.mode !== TooltipDisplayMode.None,
|
||||
})
|
||||
.addNumberInput({
|
||||
path: 'tooltip.maxHeight',
|
||||
name: 'Max height',
|
||||
category,
|
||||
defaultValue: 600,
|
||||
settings: {
|
||||
integer: true,
|
||||
},
|
||||
showIf: (options: T) => options.tooltip?.mode !== TooltipDisplayMode.None,
|
||||
showIf: (options: T) => false, //options.tooltip?.mode !== TooltipDisplayMode.None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { alpha } from '@grafana/data/src/themes/colorManipulator';
|
||||
import { TimelineValueAlignment, VisibilityMode } from '@grafana/schema';
|
||||
import { FIXED_UNIT } from '@grafana/ui';
|
||||
import { distribute, SPACE_BETWEEN } from 'app/plugins/panel/barchart/distribute';
|
||||
import { pointWithin, Quadtree, Rect } from 'app/plugins/panel/barchart/quadtree';
|
||||
import { Quadtree, Rect } from 'app/plugins/panel/barchart/quadtree';
|
||||
import { FieldConfig as StateTimeLineFieldConfig } from 'app/plugins/panel/state-timeline/panelcfg.gen';
|
||||
import { FieldConfig as StatusHistoryFieldConfig } from 'app/plugins/panel/status-history/panelcfg.gen';
|
||||
|
||||
@@ -384,7 +384,7 @@ export function getConfig(opts: TimelineCoreOptions) {
|
||||
});
|
||||
};
|
||||
|
||||
function setHovered(cx: number, cy: number, cys: number[]) {
|
||||
function setHovered(cx: number, cy: number, viaSync = false) {
|
||||
hovered.fill(null);
|
||||
hoveredAtCursor = null;
|
||||
|
||||
@@ -392,19 +392,21 @@ export function getConfig(opts: TimelineCoreOptions) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < cys.length; i++) {
|
||||
let cy2 = cys[i];
|
||||
|
||||
qt.get(cx, cy2, 1, 1, (o) => {
|
||||
if (pointWithin(cx, cy2, o.x, o.y, o.x + o.w, o.y + o.h)) {
|
||||
hovered[o.sidx] = o;
|
||||
|
||||
if (Math.abs(cy - cy2) <= o.h / 2) {
|
||||
hoveredAtCursor = o;
|
||||
}
|
||||
// first gets all items in all quads intersected by a 1px wide by 10k high rect at the x cursor position and 0 y position.
|
||||
// (we use 10k instead of plot area height for simplicity and not having to pass around the uPlot instance)
|
||||
qt.get(cx, 0, uPlot.pxRatio, 1e4, (o) => {
|
||||
// filter only rects that intersect along x dir
|
||||
if (cx >= o.x && cx <= o.x + o.w) {
|
||||
// if also intersect along y dir, set both "direct hovered" and "one-of hovered"
|
||||
if (cy >= o.y && cy <= o.y + o.h) {
|
||||
hovered[o.sidx] = hoveredAtCursor = o;
|
||||
}
|
||||
});
|
||||
}
|
||||
// else only set "one-of hovered" (no "direct hovered") in multi mode or when synced
|
||||
else if (hoverMulti || viaSync) {
|
||||
hovered[o.sidx] = o;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const cursor: uPlot.Cursor = {
|
||||
@@ -426,7 +428,7 @@ export function getConfig(opts: TimelineCoreOptions) {
|
||||
|
||||
let prevHovered = hoveredAtCursor;
|
||||
|
||||
setHovered(cx, cy, hoverMulti ? yMids : [cy]);
|
||||
setHovered(cx, cy, u.cursor.event == null);
|
||||
|
||||
if (hoveredAtCursor != null) {
|
||||
if (hoveredAtCursor !== prevHovered) {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// this file is pretty much a copy-paste of TimeSeriesPanel.tsx :(
|
||||
// with some extra renderers passed to the <TimeSeries> component
|
||||
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import React, { useMemo, useState, useCallback } from 'react';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import { DashboardCursorSync, Field, getDisplayProcessor, getLinksSupplier, PanelProps } from '@grafana/data';
|
||||
import { Field, getDisplayProcessor, getLinksSupplier, PanelProps } from '@grafana/data';
|
||||
import { PanelDataErrorView } from '@grafana/runtime';
|
||||
import { TooltipDisplayMode } from '@grafana/schema';
|
||||
import { DashboardCursorSync, TooltipDisplayMode } from '@grafana/schema';
|
||||
import { TooltipPlugin, TooltipPlugin2, UPlotConfigBuilder, usePanelContext, useTheme2, ZoomPlugin } from '@grafana/ui';
|
||||
import { AxisProps } from '@grafana/ui/src/components/uPlot/config/UPlotAxisBuilder';
|
||||
import { ScaleProps } from '@grafana/ui/src/components/uPlot/config/UPlotScaleBuilder';
|
||||
@@ -46,6 +46,13 @@ export const CandlestickPanel = ({
|
||||
|
||||
const theme = useTheme2();
|
||||
|
||||
// TODO: we should just re-init when this changes, and have this be a static setting
|
||||
const syncTooltip = useCallback(
|
||||
() => sync?.() === DashboardCursorSync.Tooltip,
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
|
||||
const info = useMemo(() => {
|
||||
return prepareCandlestickFields(data.series, options, theme, timeRange);
|
||||
}, [data.series, options, theme, timeRange]);
|
||||
@@ -233,9 +240,8 @@ export const CandlestickPanel = ({
|
||||
}
|
||||
}
|
||||
|
||||
const enableAnnotationCreation = Boolean(canAddAnnotations && canAddAnnotations());
|
||||
const showNewVizTooltips =
|
||||
config.featureToggles.newVizTooltips && (sync == null || sync() !== DashboardCursorSync.Tooltip);
|
||||
const enableAnnotationCreation = Boolean(canAddAnnotations?.());
|
||||
const showNewVizTooltips = Boolean(config.featureToggles.newVizTooltips);
|
||||
|
||||
return (
|
||||
<TimeSeries
|
||||
@@ -272,11 +278,8 @@ export const CandlestickPanel = ({
|
||||
}
|
||||
queryZoom={onChangeTimeRange}
|
||||
clientZoom={true}
|
||||
syncTooltip={syncTooltip}
|
||||
render={(u, dataIdxs, seriesIdx, isPinned = false, dismiss, timeRange2, viaSync) => {
|
||||
if (viaSync) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (enableAnnotationCreation && timeRange2 != null) {
|
||||
setNewAnnotationRange(timeRange2);
|
||||
dismiss();
|
||||
@@ -296,7 +299,7 @@ export const CandlestickPanel = ({
|
||||
seriesFrame={alignedDataFrame}
|
||||
dataIdxs={dataIdxs}
|
||||
seriesIdx={seriesIdx}
|
||||
mode={options.tooltip.mode}
|
||||
mode={viaSync ? TooltipDisplayMode.Multi : options.tooltip.mode}
|
||||
sortOrder={options.tooltip.sort}
|
||||
isPinned={isPinned}
|
||||
annotate={enableAnnotationCreation ? annotate : undefined}
|
||||
|
||||
@@ -60,6 +60,13 @@ export const HeatmapPanel = ({
|
||||
const styles = useStyles2(getStyles);
|
||||
const { sync, canAddAnnotations } = usePanelContext();
|
||||
|
||||
// TODO: we should just re-init when this changes, and have this be a static setting
|
||||
const syncTooltip = useCallback(
|
||||
() => sync?.() === DashboardCursorSync.Tooltip,
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
|
||||
// temp range set for adding new annotation set by TooltipPlugin2, consumed by AnnotationPlugin2
|
||||
const [newAnnotationRange, setNewAnnotationRange] = useState<TimeRange2 | null>(null);
|
||||
|
||||
@@ -159,8 +166,7 @@ export const HeatmapPanel = ({
|
||||
// ugh
|
||||
const dataRef = useRef(info);
|
||||
dataRef.current = info;
|
||||
const showNewVizTooltips =
|
||||
config.featureToggles.newVizTooltips && (sync == null || sync() !== DashboardCursorSync.Tooltip);
|
||||
const showNewVizTooltips = Boolean(config.featureToggles.newVizTooltips);
|
||||
|
||||
const builder = useMemo(() => {
|
||||
const scaleConfig: ScaleDistributionConfig = dataRef.current?.heatmap?.fields[1].config?.custom?.scaleDistribution;
|
||||
@@ -243,11 +249,8 @@ export const HeatmapPanel = ({
|
||||
config={builder}
|
||||
hoverMode={TooltipHoverMode.xyOne}
|
||||
queryZoom={onChangeTimeRange}
|
||||
syncTooltip={syncTooltip}
|
||||
render={(u, dataIdxs, seriesIdx, isPinned, dismiss, timeRange2, viaSync) => {
|
||||
if (viaSync) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (enableAnnotationCreation && timeRange2 != null) {
|
||||
setNewAnnotationRange(timeRange2);
|
||||
dismiss();
|
||||
@@ -263,7 +266,7 @@ export const HeatmapPanel = ({
|
||||
|
||||
return (
|
||||
<HeatmapHoverView
|
||||
mode={options.tooltip.mode}
|
||||
mode={viaSync ? TooltipDisplayMode.Multi : options.tooltip.mode}
|
||||
dataIdxs={dataIdxs}
|
||||
seriesIdx={seriesIdx}
|
||||
dataRef={dataRef}
|
||||
|
||||
@@ -429,17 +429,18 @@ export const plugin = new PanelPlugin<Options, GraphFieldConfig>(HeatmapPanel)
|
||||
settings: {
|
||||
integer: true,
|
||||
},
|
||||
showIf: (options) => config.featureToggles.newVizTooltips && options.tooltip?.mode !== TooltipDisplayMode.None,
|
||||
showIf: (options) => false, // config.featureToggles.newVizTooltips && options.tooltip?.mode !== TooltipDisplayMode.None,
|
||||
});
|
||||
|
||||
builder.addNumberInput({
|
||||
path: 'tooltip.maxHeight',
|
||||
name: 'Max height',
|
||||
category,
|
||||
defaultValue: 600,
|
||||
settings: {
|
||||
integer: true,
|
||||
},
|
||||
showIf: (options) => config.featureToggles.newVizTooltips && options.tooltip?.mode !== TooltipDisplayMode.None,
|
||||
showIf: (options) => false, // config.featureToggles.newVizTooltips && options.tooltip?.mode !== TooltipDisplayMode.None,
|
||||
});
|
||||
|
||||
category = ['Legend'];
|
||||
|
||||
@@ -52,6 +52,13 @@ export const StateTimelinePanel = ({
|
||||
}: TimelinePanelProps) => {
|
||||
const theme = useTheme2();
|
||||
|
||||
// TODO: we should just re-init when this changes, and have this be a static setting
|
||||
const syncTooltip = useCallback(
|
||||
() => sync?.() === DashboardCursorSync.Tooltip,
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
|
||||
const oldConfig = useRef<UPlotConfigBuilder | undefined>(undefined);
|
||||
const isToolTipOpen = useRef<boolean>(false);
|
||||
|
||||
@@ -163,8 +170,7 @@ export const StateTimelinePanel = ({
|
||||
}
|
||||
}
|
||||
const enableAnnotationCreation = Boolean(canAddAnnotations && canAddAnnotations());
|
||||
const showNewVizTooltips =
|
||||
config.featureToggles.newVizTooltips && (sync == null || sync() !== DashboardCursorSync.Tooltip);
|
||||
const showNewVizTooltips = Boolean(config.featureToggles.newVizTooltips);
|
||||
|
||||
return (
|
||||
<TimelineChart
|
||||
@@ -202,13 +208,12 @@ export const StateTimelinePanel = ({
|
||||
{options.tooltip.mode !== TooltipDisplayMode.None && (
|
||||
<TooltipPlugin2
|
||||
config={builder}
|
||||
hoverMode={TooltipHoverMode.xOne}
|
||||
hoverMode={
|
||||
options.tooltip.mode === TooltipDisplayMode.Multi ? TooltipHoverMode.xAll : TooltipHoverMode.xOne
|
||||
}
|
||||
queryZoom={onChangeTimeRange}
|
||||
syncTooltip={syncTooltip}
|
||||
render={(u, dataIdxs, seriesIdx, isPinned, dismiss, timeRange2, viaSync) => {
|
||||
if (viaSync) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (enableAnnotationCreation && timeRange2 != null) {
|
||||
setNewAnnotationRange(timeRange2);
|
||||
dismiss();
|
||||
@@ -228,7 +233,7 @@ export const StateTimelinePanel = ({
|
||||
seriesFrame={alignedFrame}
|
||||
dataIdxs={dataIdxs}
|
||||
seriesIdx={seriesIdx}
|
||||
mode={options.tooltip.mode}
|
||||
mode={viaSync ? TooltipDisplayMode.Multi : options.tooltip.mode}
|
||||
sortOrder={options.tooltip.sort}
|
||||
isPinned={isPinned}
|
||||
timeRange={timeRange}
|
||||
|
||||
@@ -49,6 +49,13 @@ export const StatusHistoryPanel = ({
|
||||
}: TimelinePanelProps) => {
|
||||
const theme = useTheme2();
|
||||
|
||||
// TODO: we should just re-init when this changes, and have this be a static setting
|
||||
const syncTooltip = useCallback(
|
||||
() => sync?.() === DashboardCursorSync.Tooltip,
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
|
||||
const oldConfig = useRef<UPlotConfigBuilder | undefined>(undefined);
|
||||
const isToolTipOpen = useRef<boolean>(false);
|
||||
|
||||
@@ -192,8 +199,7 @@ export const StatusHistoryPanel = ({
|
||||
);
|
||||
}
|
||||
|
||||
const showNewVizTooltips =
|
||||
config.featureToggles.newVizTooltips && (sync == null || sync() !== DashboardCursorSync.Tooltip);
|
||||
const showNewVizTooltips = Boolean(config.featureToggles.newVizTooltips);
|
||||
|
||||
return (
|
||||
<TimelineChart
|
||||
@@ -230,13 +236,12 @@ export const StatusHistoryPanel = ({
|
||||
{options.tooltip.mode !== TooltipDisplayMode.None && (
|
||||
<TooltipPlugin2
|
||||
config={builder}
|
||||
hoverMode={TooltipHoverMode.xyOne}
|
||||
hoverMode={
|
||||
options.tooltip.mode === TooltipDisplayMode.Multi ? TooltipHoverMode.xAll : TooltipHoverMode.xOne
|
||||
}
|
||||
queryZoom={onChangeTimeRange}
|
||||
syncTooltip={syncTooltip}
|
||||
render={(u, dataIdxs, seriesIdx, isPinned, dismiss, timeRange2, viaSync) => {
|
||||
if (viaSync) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (enableAnnotationCreation && timeRange2 != null) {
|
||||
setNewAnnotationRange(timeRange2);
|
||||
dismiss();
|
||||
@@ -256,7 +261,7 @@ export const StatusHistoryPanel = ({
|
||||
seriesFrame={alignedFrame}
|
||||
dataIdxs={dataIdxs}
|
||||
seriesIdx={seriesIdx}
|
||||
mode={options.tooltip.mode}
|
||||
mode={viaSync ? TooltipDisplayMode.Multi : options.tooltip.mode}
|
||||
sortOrder={options.tooltip.sort}
|
||||
isPinned={isPinned}
|
||||
timeRange={timeRange}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import React, { useMemo, useState, useCallback } from 'react';
|
||||
|
||||
import { PanelProps, DataFrameType, DashboardCursorSync } from '@grafana/data';
|
||||
import { PanelDataErrorView } from '@grafana/runtime';
|
||||
@@ -51,11 +51,17 @@ export const TimeSeriesPanel = ({
|
||||
}, [frames, id]);
|
||||
|
||||
const enableAnnotationCreation = Boolean(canAddAnnotations && canAddAnnotations());
|
||||
const showNewVizTooltips =
|
||||
config.featureToggles.newVizTooltips && (sync == null || sync() !== DashboardCursorSync.Tooltip);
|
||||
const showNewVizTooltips = Boolean(config.featureToggles.newVizTooltips);
|
||||
// temp range set for adding new annotation set by TooltipPlugin2, consumed by AnnotationPlugin2
|
||||
const [newAnnotationRange, setNewAnnotationRange] = useState<TimeRange2 | null>(null);
|
||||
|
||||
// TODO: we should just re-init when this changes, and have this be a static setting
|
||||
const syncTooltip = useCallback(
|
||||
() => sync?.() === DashboardCursorSync.Tooltip,
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
|
||||
if (!frames || suggestions) {
|
||||
return (
|
||||
<PanelDataErrorView
|
||||
@@ -102,7 +108,7 @@ export const TimeSeriesPanel = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
{!showNewVizTooltips && <KeyboardPlugin config={uplotConfig} />}
|
||||
<KeyboardPlugin config={uplotConfig} />
|
||||
{options.tooltip.mode === TooltipDisplayMode.None || (
|
||||
<>
|
||||
{showNewVizTooltips ? (
|
||||
@@ -113,11 +119,8 @@ export const TimeSeriesPanel = ({
|
||||
}
|
||||
queryZoom={onChangeTimeRange}
|
||||
clientZoom={true}
|
||||
syncTooltip={syncTooltip}
|
||||
render={(u, dataIdxs, seriesIdx, isPinned = false, dismiss, timeRange2, viaSync) => {
|
||||
if (viaSync) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (enableAnnotationCreation && timeRange2 != null) {
|
||||
setNewAnnotationRange(timeRange2);
|
||||
dismiss();
|
||||
@@ -138,7 +141,7 @@ export const TimeSeriesPanel = ({
|
||||
seriesFrame={alignedDataFrame}
|
||||
dataIdxs={dataIdxs}
|
||||
seriesIdx={seriesIdx}
|
||||
mode={options.tooltip.mode}
|
||||
mode={viaSync ? TooltipDisplayMode.Multi : options.tooltip.mode}
|
||||
sortOrder={options.tooltip.sort}
|
||||
isPinned={isPinned}
|
||||
annotate={enableAnnotationCreation ? annotate : undefined}
|
||||
|
||||
@@ -4,9 +4,9 @@ import { createPortal } from 'react-dom';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import { arrayToDataFrame, colorManipulator, DataFrame, DataTopic, GrafanaTheme2 } from '@grafana/data';
|
||||
import { arrayToDataFrame, colorManipulator, DataFrame, DataTopic } from '@grafana/data';
|
||||
import { TimeZone } from '@grafana/schema';
|
||||
import { DEFAULT_ANNOTATION_COLOR, UPlotConfigBuilder, useStyles2, useTheme2 } from '@grafana/ui';
|
||||
import { DEFAULT_ANNOTATION_COLOR, getPortalContainer, UPlotConfigBuilder, useStyles2, useTheme2 } from '@grafana/ui';
|
||||
|
||||
import { AnnotationMarker2 } from './annotations2/AnnotationMarker2';
|
||||
|
||||
@@ -65,6 +65,8 @@ export const AnnotationsPlugin2 = ({
|
||||
}: AnnotationsPluginProps) => {
|
||||
const [plot, setPlot] = useState<uPlot>();
|
||||
|
||||
const [portalRoot] = useState(() => getPortalContainer());
|
||||
|
||||
const styles = useStyles2(getStyles);
|
||||
const getColorByName = useTheme2().visualization.getColorByName;
|
||||
|
||||
@@ -221,9 +223,10 @@ export const AnnotationsPlugin2 = ({
|
||||
annoVals={vals}
|
||||
className={className}
|
||||
style={style}
|
||||
timezone={timeZone}
|
||||
timeZone={timeZone}
|
||||
key={`${frameIdx}:${i}`}
|
||||
exitWipEdit={isWip ? exitWipEdit : null}
|
||||
portalRoot={portalRoot}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -238,14 +241,14 @@ export const AnnotationsPlugin2 = ({
|
||||
return null;
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
const getStyles = () => ({
|
||||
annoMarker: css({
|
||||
position: 'absolute',
|
||||
width: 0,
|
||||
height: 0,
|
||||
borderLeft: '6px solid transparent',
|
||||
borderRight: '6px solid transparent',
|
||||
borderBottomWidth: '6px',
|
||||
borderLeft: '5px solid transparent',
|
||||
borderRight: '5px solid transparent',
|
||||
borderBottomWidth: '5px',
|
||||
borderBottomStyle: 'solid',
|
||||
transform: 'translateX(-50%)',
|
||||
cursor: 'pointer',
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import { useAsyncFn } from 'react-use';
|
||||
import React, { useContext, useEffect, useRef } from 'react';
|
||||
import { useAsyncFn, useClickAway } from 'react-use';
|
||||
|
||||
import { AnnotationEventUIModel, GrafanaTheme2 } from '@grafana/data';
|
||||
import { AnnotationEventUIModel, GrafanaTheme2, dateTimeFormat, systemDateFormats } from '@grafana/data';
|
||||
import {
|
||||
Button,
|
||||
Field,
|
||||
@@ -20,7 +20,7 @@ import { getAnnotationTags } from 'app/features/annotations/api';
|
||||
interface Props {
|
||||
annoVals: Record<string, any[]>;
|
||||
annoIdx: number;
|
||||
timeFormatter: (v: number) => string;
|
||||
timeZone: string;
|
||||
dismiss: () => void;
|
||||
}
|
||||
|
||||
@@ -29,25 +29,35 @@ interface AnnotationEditFormDTO {
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
export const AnnotationEditor2 = ({ annoVals, annoIdx, dismiss, timeFormatter, ...otherProps }: Props) => {
|
||||
export const AnnotationEditor2 = ({ annoVals, annoIdx, dismiss, timeZone, ...otherProps }: Props) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const panelContext = usePanelContext();
|
||||
const { onAnnotationCreate, onAnnotationUpdate } = usePanelContext();
|
||||
|
||||
const clickAwayRef = useRef(null);
|
||||
|
||||
useClickAway(clickAwayRef, dismiss);
|
||||
|
||||
const layoutCtx = useContext(LayoutItemContext);
|
||||
useEffect(() => layoutCtx.boostZIndex(), [layoutCtx]);
|
||||
|
||||
const [createAnnotationState, createAnnotation] = useAsyncFn(async (event: AnnotationEventUIModel) => {
|
||||
const result = await panelContext.onAnnotationCreate!(event);
|
||||
const result = await onAnnotationCreate!(event);
|
||||
dismiss();
|
||||
return result;
|
||||
});
|
||||
|
||||
const [updateAnnotationState, updateAnnotation] = useAsyncFn(async (event: AnnotationEventUIModel) => {
|
||||
const result = await panelContext.onAnnotationUpdate!(event);
|
||||
const result = await onAnnotationUpdate!(event);
|
||||
dismiss();
|
||||
return result;
|
||||
});
|
||||
|
||||
const timeFormatter = (value: number) =>
|
||||
dateTimeFormat(value, {
|
||||
format: systemDateFormats.fullDate,
|
||||
timeZone,
|
||||
});
|
||||
|
||||
const isUpdatingAnnotation = annoVals.id?.[annoIdx] != null;
|
||||
const isRegionAnnotation = annoVals.isRegion?.[annoIdx];
|
||||
const operation = isUpdatingAnnotation ? updateAnnotation : createAnnotation;
|
||||
@@ -68,7 +78,7 @@ export const AnnotationEditor2 = ({ annoVals, annoIdx, dismiss, timeFormatter, .
|
||||
|
||||
// Annotation editor
|
||||
return (
|
||||
<div className={styles.editor} {...otherProps}>
|
||||
<div ref={clickAwayRef} className={styles.editor} {...otherProps}>
|
||||
<div className={styles.header}>
|
||||
<HorizontalGroup justify={'space-between'} align={'center'}>
|
||||
<div>{isUpdatingAnnotation ? 'Edit annotation' : 'Add annotation'}</div>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useCallback, useLayoutEffect, useRef, useState } from 'react';
|
||||
import useClickAway from 'react-use/lib/useClickAway';
|
||||
import { flip, shift, autoUpdate } from '@floating-ui/dom';
|
||||
import { useFloating } from '@floating-ui/react';
|
||||
import React, { useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import { dateTimeFormat, GrafanaTheme2, systemDateFormats } from '@grafana/data';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { TimeZone } from '@grafana/schema';
|
||||
import { usePanelContext, useStyles2 } from '@grafana/ui';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { AnnotationEditor2 } from './AnnotationEditor2';
|
||||
import { AnnotationTooltip2 } from './AnnotationTooltip2';
|
||||
@@ -14,131 +16,94 @@ interface AnnoBoxProps {
|
||||
annoIdx: number;
|
||||
style: React.CSSProperties | null;
|
||||
className: string;
|
||||
timezone: TimeZone;
|
||||
timeZone: TimeZone;
|
||||
exitWipEdit?: null | (() => void);
|
||||
portalRoot: HTMLElement;
|
||||
}
|
||||
|
||||
const STATE_DEFAULT = 0;
|
||||
const STATE_EDITING = 1;
|
||||
const STATE_HOVERED = 2;
|
||||
|
||||
export const AnnotationMarker2 = ({ annoVals, annoIdx, className, style, exitWipEdit, timezone }: AnnoBoxProps) => {
|
||||
const { canEditAnnotations, canDeleteAnnotations, ...panelCtx } = usePanelContext();
|
||||
|
||||
export const AnnotationMarker2 = ({
|
||||
annoVals,
|
||||
annoIdx,
|
||||
className,
|
||||
style,
|
||||
exitWipEdit,
|
||||
timeZone,
|
||||
portalRoot,
|
||||
}: AnnoBoxProps) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const [state, setState] = useState(STATE_DEFAULT);
|
||||
|
||||
const clickAwayRef = useRef(null);
|
||||
|
||||
useClickAway(clickAwayRef, () => {
|
||||
if (state === STATE_EDITING) {
|
||||
setIsEditingWrap(false);
|
||||
}
|
||||
const [state, setState] = useState(exitWipEdit != null ? STATE_EDITING : STATE_DEFAULT);
|
||||
const { refs, floatingStyles } = useFloating({
|
||||
open: true,
|
||||
placement: 'bottom',
|
||||
middleware: [
|
||||
flip({
|
||||
fallbackAxisSideDirection: 'end',
|
||||
// see https://floating-ui.com/docs/flip#combining-with-shift
|
||||
crossAxis: false,
|
||||
boundary: document.body,
|
||||
}),
|
||||
shift(),
|
||||
],
|
||||
whileElementsMounted: autoUpdate,
|
||||
strategy: 'fixed',
|
||||
});
|
||||
|
||||
const domRef = React.createRef<HTMLDivElement>();
|
||||
|
||||
// similar to TooltipPlugin2, when editing annotation (pinned), it should boost z-index
|
||||
const setIsEditingWrap = useCallback(
|
||||
(isEditing: boolean) => {
|
||||
setState(isEditing ? STATE_EDITING : STATE_DEFAULT);
|
||||
if (!isEditing && exitWipEdit != null) {
|
||||
exitWipEdit();
|
||||
}
|
||||
},
|
||||
[exitWipEdit]
|
||||
);
|
||||
|
||||
const onAnnotationEdit = useCallback(() => {
|
||||
setIsEditingWrap(true);
|
||||
}, [setIsEditingWrap]);
|
||||
|
||||
const onAnnotationDelete = useCallback(() => {
|
||||
if (panelCtx.onAnnotationDelete) {
|
||||
panelCtx.onAnnotationDelete(annoVals.id?.[annoIdx]);
|
||||
}
|
||||
}, [annoIdx, annoVals.id, panelCtx]);
|
||||
|
||||
const timeFormatter = useCallback(
|
||||
(value: number) => {
|
||||
return dateTimeFormat(value, {
|
||||
format: systemDateFormats.fullDate,
|
||||
timeZone: timezone,
|
||||
});
|
||||
},
|
||||
[timezone]
|
||||
);
|
||||
|
||||
useLayoutEffect(
|
||||
() => {
|
||||
if (exitWipEdit != null) {
|
||||
setIsEditingWrap(true);
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
|
||||
const renderAnnotationTooltip = useCallback(() => {
|
||||
let dashboardUID = annoVals.dashboardUID?.[annoIdx];
|
||||
|
||||
return (
|
||||
const contents =
|
||||
state === STATE_HOVERED ? (
|
||||
<AnnotationTooltip2
|
||||
timeFormatter={timeFormatter}
|
||||
onEdit={onAnnotationEdit}
|
||||
onDelete={onAnnotationDelete}
|
||||
canEdit={canEditAnnotations ? canEditAnnotations(dashboardUID) : false}
|
||||
canDelete={canDeleteAnnotations ? canDeleteAnnotations(dashboardUID) : false}
|
||||
annoIdx={annoIdx}
|
||||
annoVals={annoVals}
|
||||
timeZone={timeZone}
|
||||
onEdit={() => setState(STATE_EDITING)}
|
||||
/>
|
||||
);
|
||||
}, [
|
||||
timeFormatter,
|
||||
onAnnotationEdit,
|
||||
onAnnotationDelete,
|
||||
canEditAnnotations,
|
||||
annoVals,
|
||||
annoIdx,
|
||||
canDeleteAnnotations,
|
||||
]);
|
||||
|
||||
const renderAnnotationEditor = useCallback(() => {
|
||||
return (
|
||||
) : state === STATE_EDITING ? (
|
||||
<AnnotationEditor2
|
||||
dismiss={() => setIsEditingWrap(false)}
|
||||
timeFormatter={timeFormatter}
|
||||
annoIdx={annoIdx}
|
||||
annoVals={annoVals}
|
||||
timeZone={timeZone}
|
||||
dismiss={() => {
|
||||
exitWipEdit?.();
|
||||
setState(STATE_DEFAULT);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}, [annoIdx, annoVals, timeFormatter, setIsEditingWrap]);
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={domRef}
|
||||
ref={refs.setReference}
|
||||
className={className}
|
||||
style={style!}
|
||||
onMouseEnter={() => state !== STATE_EDITING && setState(STATE_HOVERED)}
|
||||
onMouseLeave={() => state !== STATE_EDITING && setState(STATE_DEFAULT)}
|
||||
>
|
||||
<div className={styles.annoInfo} ref={clickAwayRef}>
|
||||
{state === STATE_HOVERED && renderAnnotationTooltip()}
|
||||
{state === STATE_EDITING && renderAnnotationEditor()}
|
||||
</div>
|
||||
{contents &&
|
||||
createPortal(
|
||||
<div ref={refs.setFloating} className={styles.annoBox} style={floatingStyles}>
|
||||
{contents}
|
||||
</div>,
|
||||
portalRoot
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
annoInfo: css({
|
||||
background: theme.colors.background.secondary,
|
||||
minWidth: '300px',
|
||||
// maxWidth: '400px',
|
||||
// NOTE: shares much with TooltipPlugin2
|
||||
annoBox: css({
|
||||
top: 0,
|
||||
left: 0,
|
||||
zIndex: theme.zIndex.tooltip,
|
||||
borderRadius: theme.shape.radius.default,
|
||||
position: 'absolute',
|
||||
top: '5px',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
background: theme.colors.background.primary,
|
||||
border: `1px solid ${theme.colors.border.weak}`,
|
||||
boxShadow: theme.shadows.z2,
|
||||
userSelect: 'text',
|
||||
minWidth: '300px',
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1,34 +1,39 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
|
||||
import { GrafanaTheme2, textUtil } from '@grafana/data';
|
||||
import { HorizontalGroup, IconButton, LayoutItemContext, Tag, useStyles2 } from '@grafana/ui';
|
||||
import { GrafanaTheme2, dateTimeFormat, systemDateFormats, textUtil } from '@grafana/data';
|
||||
import { HorizontalGroup, IconButton, LayoutItemContext, Tag, usePanelContext, useStyles2 } from '@grafana/ui';
|
||||
import alertDef from 'app/features/alerting/state/alertDef';
|
||||
|
||||
interface Props {
|
||||
annoVals: Record<string, any[]>;
|
||||
annoIdx: number;
|
||||
timeFormatter: (v: number) => string;
|
||||
canEdit: boolean;
|
||||
canDelete: boolean;
|
||||
timeZone: string;
|
||||
onEdit: () => void;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
export const AnnotationTooltip2 = ({
|
||||
annoVals,
|
||||
annoIdx,
|
||||
timeFormatter,
|
||||
canEdit,
|
||||
canDelete,
|
||||
onEdit,
|
||||
onDelete,
|
||||
}: Props) => {
|
||||
const retFalse = () => false;
|
||||
|
||||
export const AnnotationTooltip2 = ({ annoVals, annoIdx, timeZone, onEdit }: Props) => {
|
||||
const annoId = annoVals.id?.[annoIdx];
|
||||
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const { canEditAnnotations = retFalse, canDeleteAnnotations = retFalse, onAnnotationDelete } = usePanelContext();
|
||||
|
||||
const dashboardUID = annoVals.dashboardUID?.[annoIdx];
|
||||
const canEdit = canEditAnnotations(dashboardUID);
|
||||
const canDelete = canDeleteAnnotations(dashboardUID) && onAnnotationDelete != null;
|
||||
|
||||
const layoutCtx = useContext(LayoutItemContext);
|
||||
useEffect(() => layoutCtx.boostZIndex(), [layoutCtx]);
|
||||
|
||||
const timeFormatter = (value: number) =>
|
||||
dateTimeFormat(value, {
|
||||
format: systemDateFormats.fullDate,
|
||||
timeZone,
|
||||
});
|
||||
|
||||
let time = timeFormatter(annoVals.time[annoIdx]);
|
||||
let text = annoVals.text[annoIdx];
|
||||
|
||||
@@ -75,9 +80,8 @@ export const AnnotationTooltip2 = ({
|
||||
<IconButton
|
||||
name={'trash-alt'}
|
||||
size={'sm'}
|
||||
onClick={onDelete}
|
||||
onClick={() => onAnnotationDelete(annoId)}
|
||||
tooltip="Delete"
|
||||
disabled={!annoVals.id?.[annoIdx]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user