import { css, cx } from '@emotion/css'; import { useDialog } from '@react-aria/dialog'; import { useOverlay } from '@react-aria/overlays'; import { createRef } from 'react'; import { Field, LinkModel, FieldType, GrafanaTheme2, formattedValueToString, getFieldDisplayName, ScopedVars, ValueLinkConfig, } from '@grafana/data/src'; import { ActionModel } from '@grafana/data/src/types/action'; import { Portal, useStyles2, VizTooltipContainer } 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 { VizTooltipItem } from '@grafana/ui/src/components/VizTooltip/types'; import { CloseButton } from '@grafana/ui/src/components/uPlot/plugins/CloseButton'; import { getActions, getActionsDefaultField } from 'app/features/actions/utils'; import { Scene } from 'app/features/canvas/runtime/scene'; import { getDataLinks } from '../../status-history/utils'; import { getElementFields, getRowIndex } from '../utils'; interface Props { scene: Scene; } export const CanvasTooltip = ({ scene }: Props) => { const styles = useStyles2(getStyles); const onClose = () => { if (scene?.tooltipCallback && scene.tooltip) { scene.tooltipCallback(undefined); } }; const ref = createRef(); const { overlayProps } = useOverlay({ onClose: onClose, isDismissable: true }, ref); const { dialogProps } = useDialog({}, ref); const element = scene.tooltip?.element; if (!element) { return <>; } // Retrieve timestamp of the last data point if available const timeField = scene.data?.series[0]?.fields?.find((field) => field.type === FieldType.time); const lastTimeValue = timeField?.values[timeField.values.length - 1]; const shouldDisplayTimeContentItem = timeField && lastTimeValue && element.data.field && getFieldDisplayName(timeField) !== element.data.field; const headerItem: VizTooltipItem | null = { label: element.getName(), value: '', }; const contentItems: VizTooltipItem[] = [ { label: element.data.field ?? 'Fixed', value: element.data.text, }, ...(shouldDisplayTimeContentItem ? [ { label: 'Time', value: formattedValueToString(timeField?.display!(lastTimeValue)), }, ] : []), ]; // NOTE: almost identical to getDataLinks() helper const links: Array> = []; if ((element.options.links?.length ?? 0) > 0 && element.getLinks) { const linkLookup = new Set(); element.getLinks({ valueRowIndex: getRowIndex(element.data.field, scene) }).forEach((link) => { const key = `${link.title}/${link.href}`; if (!linkLookup.has(key)) { links.push(link); linkLookup.add(key); } }); } if (scene.data?.series) { getElementFields(scene.data?.series, element.options).forEach((field) => { links.push(...getDataLinks(field, getRowIndex(element.data.field, scene))); }); } const actions: Array> = []; const actionLookup = new Set(); const elementHasActions = (element.options.actions?.length ?? 0) > 0; const frames = scene.data?.series; if (elementHasActions && frames) { const defaultField = getActionsDefaultField(element.options.links ?? [], element.options.actions ?? []); const scopedVars: ScopedVars = { __dataContext: { value: { data: frames, field: defaultField, frame: frames[0], frameIndex: 0, }, }, }; const config: ValueLinkConfig = { valueRowIndex: getRowIndex(element.data.field, scene) }; const actionsModel = getActions( frames[0], defaultField, scopedVars, scene.panel.props.replaceVariables!, element.options.actions ?? [], config ); actionsModel.forEach((action) => { const key = `${action.title}/${Math.random()}`; if (!actionLookup.has(key)) { actions.push(action); actionLookup.add(key); } }); } return ( <> {scene.tooltip?.element && scene.tooltip.anchorPoint && (
{scene.tooltip.isOpen && } {element.data.text && } {(links.length > 0 || actions.length > 0) && }
)} ); }; const getStyles = (theme: GrafanaTheme2) => ({ wrapper: css({ marginTop: '20px', background: theme.colors.background.primary, }), tooltipWrapper: css({ top: 0, left: 0, zIndex: theme.zIndex.portal, whiteSpace: 'pre', borderRadius: theme.shape.radius.default, position: 'fixed', background: theme.colors.background.primary, border: `1px solid ${theme.colors.border.weak}`, boxShadow: theme.shadows.z2, userSelect: 'text', padding: 0, fontSize: theme.typography.bodySmall.fontSize, }), pinned: css({ boxShadow: theme.shadows.z3, }), });