@grafana/ui
Version:
Grafana Components Library
533 lines (530 loc) • 18.2 kB
JavaScript
import { jsxs, jsx } from 'react/jsx-runtime';
import { css, cx } from '@emotion/css';
import { useRef, useReducer, useLayoutEffect } from 'react';
import { createPortal } from 'react-dom';
import { DashboardCursorSync } from '@grafana/schema';
import { useStyles2 } from '../../../themes/ThemeContext.mjs';
import { getPortalContainer } from '../../Portal/Portal.mjs';
import { CloseButton } from './CloseButton.mjs';
;
const DEFAULT_TOOLTIP_WIDTH = void 0;
const TOOLTIP_OFFSET = 10;
var TooltipHoverMode = /* @__PURE__ */ ((TooltipHoverMode2) => {
TooltipHoverMode2[TooltipHoverMode2["xOne"] = 0] = "xOne";
TooltipHoverMode2[TooltipHoverMode2["xAll"] = 1] = "xAll";
TooltipHoverMode2[TooltipHoverMode2["xyOne"] = 2] = "xyOne";
return TooltipHoverMode2;
})(TooltipHoverMode || {});
function mergeState(prevState, nextState) {
return {
...prevState,
...nextState,
style: {
...prevState.style,
...nextState.style
}
};
}
function initState() {
return {
style: { transform: "", pointerEvents: "none" },
isHovering: false,
isPinned: false,
contents: null,
plot: null,
dismiss: () => {
}
};
}
const MIN_ZOOM_DIST = 5;
const maybeZoomAction = (e) => e != null && !e.ctrlKey && !e.metaKey;
const getDataLinksFallback = () => [];
const getAdHocFiltersFallback = () => [];
const userAgentIsMobile = /Android|iPhone|iPad/i.test(navigator.userAgent);
const TooltipPlugin2 = ({
config,
hoverMode,
render,
clientZoom = false,
queryZoom,
onSelectRange,
maxWidth,
syncMode = DashboardCursorSync.Off,
syncScope = "global",
// eventsScope
getDataLinks = getDataLinksFallback,
getAdHocFilters = getAdHocFiltersFallback
}) => {
const domRef = useRef(null);
const portalRoot = useRef(null);
if (portalRoot.current == null) {
portalRoot.current = getPortalContainer();
}
const [{ plot, isHovering, isPinned, contents, style, dismiss }, setState] = useReducer(mergeState, null, initState);
const sizeRef = useRef();
const styles = useStyles2(getStyles, maxWidth);
const renderRef = useRef(render);
renderRef.current = render;
const getLinksRef = useRef(getDataLinks);
getLinksRef.current = getDataLinks;
const getAdHocFiltersRef = useRef(getAdHocFilters);
getAdHocFiltersRef.current = getAdHocFilters;
useLayoutEffect(() => {
var _a;
(_a = sizeRef.current) == null ? void 0 : _a.observer.disconnect();
sizeRef.current = {
width: 0,
height: 0,
observer: new ResizeObserver((entries) => {
var _a2;
let size = sizeRef.current;
for (const entry of entries) {
if (((_a2 = entry.borderBoxSize) == null ? void 0 : _a2.length) > 0) {
size.width = entry.borderBoxSize[0].inlineSize;
size.height = entry.borderBoxSize[0].blockSize;
} else {
size.width = entry.contentRect.width;
size.height = entry.contentRect.height;
}
}
})
};
let yZoomed = false;
let yDrag = false;
let _plot = plot;
let _isHovering = isHovering;
let _someSeriesIdx = false;
let _isPinned = isPinned;
let _style = style;
let plotVisible = false;
const syncTooltip = syncMode === DashboardCursorSync.Tooltip;
if (syncMode !== DashboardCursorSync.Off && config.scales[0].props.isTime) {
config.setCursor({
sync: {
key: syncScope,
scales: ["x", null]
}
});
}
const updateHovering = () => {
if (viaSync) {
_isHovering = plotVisible && _someSeriesIdx && syncTooltip;
} else {
_isHovering = closestSeriesIdx != null || hoverMode === 1 /* xAll */ && _someSeriesIdx;
}
};
let offsetX = 0;
let offsetY = 0;
let selectedRange = null;
let seriesIdxs = [];
let closestSeriesIdx = null;
let viaSync = false;
let dataLinks = [];
let adHocFilters = [];
let persistentLinks = [];
let pendingRender = false;
let pendingPinned = false;
const scheduleRender = (setPinned = false) => {
if (!pendingRender) {
if (!_isHovering) {
setTimeout(_render, 100);
} else {
queueMicrotask(_render);
}
pendingRender = true;
}
if (setPinned) {
pendingPinned = true;
}
};
const downEventOutside = (e) => {
const isModalOrPortaled = '[role="dialog"], #grafana-portal-container';
if (e.target.closest(isModalOrPortaled) == null) {
dismiss2();
}
};
const _render = () => {
pendingRender = false;
if (pendingPinned) {
_style = { pointerEvents: _isPinned ? "all" : "none" };
_plot.cursor._lock = _isPinned;
if (_isPinned) {
document.addEventListener("mousedown", downEventOutside, true);
document.addEventListener("keydown", downEventOutside, true);
} else {
document.removeEventListener("mousedown", downEventOutside, true);
document.removeEventListener("keydown", downEventOutside, true);
}
pendingPinned = false;
}
let state = {
style: _style,
isPinned: _isPinned,
isHovering: _isHovering,
contents: _isHovering || selectedRange != null ? renderRef.current(
_plot,
seriesIdxs,
closestSeriesIdx,
_isPinned,
dismiss2,
selectedRange,
viaSync,
_isPinned ? dataLinks : closestSeriesIdx != null ? persistentLinks[closestSeriesIdx] : [],
_isPinned ? adHocFilters : []
) : null,
dismiss: dismiss2
};
setState(state);
selectedRange = null;
};
const dismiss2 = () => {
let prevIsPinned = _isPinned;
_isPinned = false;
_isHovering = false;
_plot.setCursor({ left: -10, top: -10 });
dataLinks = [];
adHocFilters = [];
scheduleRender(prevIsPinned);
};
config.addHook("init", (u) => {
setState({ plot: _plot = u });
if (clientZoom) {
u.over.addEventListener(
"mousedown",
(e) => {
if (!maybeZoomAction(e)) {
return;
}
if (e.button === 0 && e.shiftKey) {
yDrag = true;
u.cursor.drag.x = false;
u.cursor.drag.y = true;
let onUp = (e2) => {
u.cursor.drag.x = true;
u.cursor.drag.y = false;
document.removeEventListener("mouseup", onUp, true);
};
document.addEventListener("mouseup", onUp, true);
}
},
true
);
}
u.over.addEventListener("click", (e) => {
var _a2;
if (e.target === u.over) {
if (e.ctrlKey || e.metaKey) {
let xVal;
const isXAxisHorizontal = u.scales.x.ori === 0;
if (isXAxisHorizontal) {
xVal = u.posToVal(u.cursor.left, "x");
} else {
xVal = u.posToVal(u.select.top + u.select.height, "x");
}
selectedRange = {
from: xVal,
to: xVal
};
scheduleRender(false);
} else if (_isHovering && !_isPinned && closestSeriesIdx != null) {
dataLinks = getLinksRef.current(closestSeriesIdx, seriesIdxs[closestSeriesIdx]);
adHocFilters = getAdHocFiltersRef.current(closestSeriesIdx, seriesIdxs[closestSeriesIdx]);
const oneClickLink = dataLinks.find((dataLink) => dataLink.oneClick === true);
if (oneClickLink != null) {
window.open(oneClickLink.href, (_a2 = oneClickLink.target) != null ? _a2 : "_self");
} else {
setTimeout(() => {
_isPinned = true;
scheduleRender(true);
}, 0);
}
}
}
});
});
config.addHook("setSelect", (u) => {
const isXAxisHorizontal = u.scales.x.ori === 0;
if (!viaSync && (clientZoom || queryZoom != null)) {
if (maybeZoomAction(u.cursor.event)) {
if (onSelectRange != null) {
let selections = [];
const yDrag2 = Boolean(u.cursor.drag.y);
const xDrag = Boolean(u.cursor.drag.x);
let xSel = null;
let ySels = [];
if (xDrag) {
xSel = {
from: isXAxisHorizontal ? u.posToVal(u.select.left, "x") : u.posToVal(u.select.top + u.select.height, "x"),
to: isXAxisHorizontal ? u.posToVal(u.select.left + u.select.width, "x") : u.posToVal(u.select.top, "x")
};
}
if (yDrag2) {
config.scales.forEach((scale) => {
const key = scale.props.scaleKey;
if (key !== "x") {
let ySel = {
from: isXAxisHorizontal ? u.posToVal(u.select.top + u.select.height, key) : u.posToVal(u.select.left + u.select.width, key),
to: isXAxisHorizontal ? u.posToVal(u.select.top, key) : u.posToVal(u.select.left, key)
};
ySels.push(ySel);
}
});
}
if (xDrag) {
if (yDrag2) {
selections = ySels.map((ySel) => ({ x: xSel, y: ySel }));
} else {
selections = [{ x: xSel }];
}
} else {
if (yDrag2) {
selections = ySels.map((ySel) => ({ y: ySel }));
}
}
onSelectRange(selections);
} else if (clientZoom && yDrag) {
if (u.select.height >= MIN_ZOOM_DIST) {
for (let key in u.scales) {
if (key !== "x") {
const maxY = isXAxisHorizontal ? u.posToVal(u.select.top, key) : u.posToVal(u.select.left + u.select.width, key);
const minY = isXAxisHorizontal ? u.posToVal(u.select.top + u.select.height, key) : u.posToVal(u.select.left, key);
u.setScale(key, { min: minY, max: maxY });
}
}
yZoomed = true;
}
yDrag = false;
} else if (queryZoom != null) {
if (u.select.width >= MIN_ZOOM_DIST) {
const minX = isXAxisHorizontal ? u.posToVal(u.select.left, "x") : u.posToVal(u.select.top + u.select.height, "x");
const maxX = isXAxisHorizontal ? u.posToVal(u.select.left + u.select.width, "x") : u.posToVal(u.select.top, "x");
queryZoom({ from: minX, to: maxX });
yZoomed = false;
}
}
} else {
selectedRange = {
from: isXAxisHorizontal ? u.posToVal(u.select.left, "x") : u.posToVal(u.select.top + u.select.height, "x"),
to: isXAxisHorizontal ? u.posToVal(u.select.left + u.select.width, "x") : u.posToVal(u.select.top, "x")
};
scheduleRender(true);
}
}
u.setSelect({ left: 0, width: 0, top: 0, height: 0 }, false);
});
if (clientZoom || queryZoom != null) {
config.setCursor({
bind: {
dblclick: (u) => () => {
if (!maybeZoomAction(u.cursor.event)) {
return null;
}
if (clientZoom && yZoomed) {
for (let key in u.scales) {
if (key !== "x") {
u.setScale(key, { min: null, max: null });
}
}
yZoomed = false;
} else if (queryZoom != null) {
let xScale = u.scales.x;
const frTs = xScale.min;
const toTs = xScale.max;
const pad = (toTs - frTs) / 2;
queryZoom({ from: frTs - pad, to: toTs + pad });
}
return null;
}
}
});
}
config.addHook("setData", (u) => {
yZoomed = false;
yDrag = false;
if (_isPinned) {
dismiss2();
}
});
config.addHook("setSeries", (u, seriesIdx) => {
closestSeriesIdx = seriesIdx;
viaSync = u.cursor.event == null;
updateHovering();
scheduleRender();
});
config.addHook("setLegend", (u) => {
seriesIdxs = _plot == null ? void 0 : _plot.cursor.idxs.slice();
_someSeriesIdx = seriesIdxs.some((v, i) => i > 0 && v != null);
if (persistentLinks.length === 0) {
persistentLinks = seriesIdxs.map((v, seriesIdx) => {
if (seriesIdx > 0) {
const links = getDataLinks(seriesIdx, seriesIdxs[seriesIdx]);
const oneClickLink = links.find((dataLink) => dataLink.oneClick === true);
if (oneClickLink) {
return [oneClickLink];
}
}
return [];
});
}
viaSync = u.cursor.event == null;
let prevIsHovering = _isHovering;
updateHovering();
if (_isHovering || _isHovering !== prevIsHovering) {
scheduleRender();
}
});
const scrollbarWidth = 16;
let winWid = 0;
let winHgt = 0;
const updateWinSize = () => {
_isHovering && !_isPinned && dismiss2();
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);
config.addHook("setCursor", (u) => {
viaSync = u.cursor.event == null;
if (!_isHovering) {
return;
}
let { left = -10, top = -10 } = u.cursor;
if (left >= 0 || top >= 0) {
let clientX = u.rect.left + left;
let clientY = u.rect.top + top;
let transform = "";
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 {
if (clientY + height > winHgt && clientY - height >= 0) {
offsetY = -height;
}
}
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%)";
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 = (e) => {
updatePlotVisible();
_isHovering && e.target instanceof Node && e.target.contains(_plot.root) && dismiss2();
};
window.addEventListener("resize", updateWinSize);
window.addEventListener("scroll", onscroll, true);
return () => {
var _a2;
(_a2 = sizeRef.current) == null ? void 0 : _a2.observer.disconnect();
window.removeEventListener("resize", updateWinSize);
window.removeEventListener("scroll", onscroll, true);
document.removeEventListener("mousedown", downEventOutside, true);
document.removeEventListener("keydown", downEventOutside, true);
};
}, [config]);
useLayoutEffect(() => {
const size = sizeRef.current;
if (domRef.current != null) {
size.observer.disconnect();
size.observer.observe(domRef.current);
const { width, height } = domRef.current.getBoundingClientRect();
size.width = width;
size.height = height;
let event = plot.cursor.event;
if (event != null) {
const isMobile = event.type !== "mousemove" || userAgentIsMobile;
if (isMobile) {
event = new MouseEvent("mousemove", {
view: window,
bubbles: true,
cancelable: true,
clientX: event.clientX,
clientY: event.clientY,
screenX: event.screenX,
screenY: event.screenY
});
}
const isStaleEvent = isMobile ? false : performance.now() - event.timeStamp > 16;
!isStaleEvent && plot.over.dispatchEvent(event);
} else {
plot.setCursor(
{
left: plot.cursor.left,
top: plot.cursor.top
},
true
);
}
} else {
size.width = 0;
size.height = 0;
}
}, [isHovering]);
if (plot && isHovering) {
return createPortal(
/* @__PURE__ */ jsxs(
"div",
{
className: cx(styles.tooltipWrapper, isPinned && styles.pinned),
style,
"aria-live": "polite",
"aria-atomic": "true",
ref: domRef,
children: [
isPinned && /* @__PURE__ */ jsx(CloseButton, { onClick: dismiss }),
contents
]
}
),
portalRoot.current
);
}
return null;
};
const getStyles = (theme, maxWidth) => ({
tooltipWrapper: css({
top: 0,
left: 0,
zIndex: theme.zIndex.tooltip,
whiteSpace: "pre",
borderRadius: theme.shape.radius.default,
position: "fixed",
background: theme.colors.background.elevated,
border: `1px solid ${theme.colors.border.weak}`,
boxShadow: theme.shadows.z2,
userSelect: "text",
maxWidth: maxWidth != null ? maxWidth : "none"
}),
pinned: css({
boxShadow: theme.shadows.z3
})
});
export { DEFAULT_TOOLTIP_WIDTH, TOOLTIP_OFFSET, TooltipHoverMode, TooltipPlugin2 };
//# sourceMappingURL=TooltipPlugin2.mjs.map