From 12249d321119ea98dbd86702c2e58bf7516366fc Mon Sep 17 00:00:00 2001 From: Galen Date: Tue, 7 Oct 2025 15:37:46 -0500 Subject: [PATCH] feat: support anchored annotation tooltips on click --- .../timeseries/plugins/AnnotationsPlugin2.tsx | 25 +++++++++- .../annotations2/AnnotationEditor2.tsx | 18 ++++++- .../annotations2/AnnotationMarker2.tsx | 47 +++++++++++++------ .../annotations2/AnnotationTooltip2.tsx | 31 +++++++++--- public/locales/en-US/grafana.json | 4 +- 5 files changed, 99 insertions(+), 26 deletions(-) diff --git a/public/app/plugins/panel/timeseries/plugins/AnnotationsPlugin2.tsx b/public/app/plugins/panel/timeseries/plugins/AnnotationsPlugin2.tsx index f01152e2f88..40127040eb9 100644 --- a/public/app/plugins/panel/timeseries/plugins/AnnotationsPlugin2.tsx +++ b/public/app/plugins/panel/timeseries/plugins/AnnotationsPlugin2.tsx @@ -67,7 +67,7 @@ export const AnnotationsPlugin2 = ({ const [plot, setPlot] = useState(); const [portalRoot] = useState(() => getPortalContainer()); - + const [annoIdx, setAnnoIdx] = useState(); const styles = useStyles2(getStyles); const getColorByName = useTheme2().visualization.getColorByName; @@ -209,6 +209,11 @@ export const AnnotationsPlugin2 = ({ } }, [annos, plot]); + // Set active annotation tooltip state + const setAnnotationIndex = useCallback((annoIdx: string | undefined) => { + setAnnoIdx(annoIdx); + }, []); + if (plot) { let markers = annos.flatMap((frame, frameIdx) => { let vals = getVals(frame); @@ -245,10 +250,20 @@ export const AnnotationsPlugin2 = ({ // @TODO: Reset newRange after annotation is saved if (isVisible) { - let isWip = frame.meta?.custom?.isWip; + const isWip = frame.meta?.custom?.isWip; + const setAnnotation = (active: boolean) => { + if (active) { + setAnnotationIndex(`${frameIdx}:${i}`); + } else { + setAnnotationIndex(undefined); + } + }; markers.push( ({ position: 'absolute', width: 0, height: 0, + border: 'none', borderLeft: '5px solid transparent', borderRight: '5px solid transparent', borderBottomWidth: '5px', @@ -283,11 +299,16 @@ const getStyles = () => ({ transform: 'translateX(-50%)', cursor: 'pointer', zIndex: 1, + padding: 0, + background: 'none', }), annoRegion: css({ + border: 'none', position: 'absolute', height: '5px', cursor: 'pointer', zIndex: 1, + padding: 0, + background: 'none', }), }); diff --git a/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationEditor2.tsx b/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationEditor2.tsx index f9fae596e3c..e7cf6684324 100644 --- a/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationEditor2.tsx +++ b/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationEditor2.tsx @@ -5,7 +5,7 @@ import { useAsyncFn, useClickAway } from 'react-use'; import { AnnotationEventUIModel, GrafanaTheme2, dateTimeFormat, systemDateFormats } from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; -import { Button, Field, Stack, TextArea, usePanelContext, useStyles2 } from '@grafana/ui'; +import { Button, Field, IconButton, Stack, TextArea, usePanelContext, useStyles2 } from '@grafana/ui'; import { Form } from 'app/core/components/Form/Form'; import { TagFilter } from 'app/core/components/TagFilter/TagFilter'; import { annotationServer } from 'app/features/annotations/api'; @@ -15,6 +15,7 @@ interface Props { annoIdx: number; timeZone: string; dismiss: () => void; + isPinned: boolean; } interface AnnotationEditFormDTO { @@ -22,7 +23,7 @@ interface AnnotationEditFormDTO { tags: string[]; } -export const AnnotationEditor2 = ({ annoVals, annoIdx, dismiss, timeZone, ...otherProps }: Props) => { +export const AnnotationEditor2 = ({ annoVals, annoIdx, dismiss, timeZone, isPinned, ...otherProps }: Props) => { const styles = useStyles2(getStyles); const { onAnnotationCreate, onAnnotationUpdate } = usePanelContext(); @@ -77,6 +78,18 @@ export const AnnotationEditor2 = ({ annoVals, annoIdx, dismiss, timeZone, ...oth : t('timeseries.annotation-editor2.add-annotation', 'Add annotation')}
{time}
+ {isPinned && ( + { + // Don't trigger onClick + e.stopPropagation(); + dismiss(); + }} + tooltip={t('timeseries.annotation-editor2.tooltip-close', 'Close')} + /> + )} @@ -88,6 +101,7 @@ export const AnnotationEditor2 = ({ annoVals, annoIdx, dismiss, timeZone, ...oth <>
void); portalRoot: HTMLElement; + pinAnnotation: (pin: boolean) => void; + isPinned: boolean; + showOnHover: boolean; } -const STATE_DEFAULT = 0; -const STATE_EDITING = 1; -const STATE_HOVERED = 2; - export const AnnotationMarker2 = ({ annoVals, annoIdx, @@ -35,11 +34,15 @@ export const AnnotationMarker2 = ({ exitWipEdit, timeZone, portalRoot, + pinAnnotation, + showOnHover, + isPinned, }: AnnoBoxProps) => { const styles = useStyles2(getStyles); const placement = 'bottom'; - const [state, setState] = useState(exitWipEdit != null ? STATE_EDITING : STATE_DEFAULT); + const [editing, setEditing] = useState(exitWipEdit != null); + const [isHovering, setIsHovering] = useState(false); const { refs, floatingStyles } = useFloating({ open: true, placement, @@ -48,43 +51,57 @@ export const AnnotationMarker2 = ({ strategy: 'fixed', }); + const onClose = () => { + pinAnnotation(false); + setIsHovering(false); + }; + const contents = - state === STATE_HOVERED ? ( + (isPinned && !editing) || (showOnHover && isHovering && !editing) ? ( setState(STATE_EDITING)} + onClose={onClose} + isPinned={isPinned} + onEdit={() => setEditing(true)} /> - ) : state === STATE_EDITING ? ( + ) : editing ? ( { exitWipEdit?.(); - setState(STATE_DEFAULT); + setEditing(false); + onClose(); }} /> ) : null; return ( -
state !== STATE_EDITING && setState(STATE_HOVERED)} - onMouseLeave={() => state !== STATE_EDITING && setState(STATE_DEFAULT)} + onFocus={() => setIsHovering(true)} + onBlur={() => setIsHovering(false)} + onClick={() => pinAnnotation(true)} + onMouseEnter={() => showOnHover && setIsHovering(true)} + onMouseLeave={() => setIsHovering(false)} data-testid={selectors.pages.Dashboard.Annotations.marker} > {contents && createPortal(
- {contents} + pinAnnotation(false)}> + {contents} +
, portalRoot )} -
+ ); }; diff --git a/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationTooltip2.tsx b/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationTooltip2.tsx index 018aff532f8..3b066537639 100644 --- a/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationTooltip2.tsx +++ b/public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationTooltip2.tsx @@ -10,24 +10,31 @@ interface Props { annoVals: Record; annoIdx: number; timeZone: string; + isPinned: boolean; + onClose: () => void; onEdit: () => void; } const retFalse = () => false; -export const AnnotationTooltip2 = ({ annoVals, annoIdx, timeZone, onEdit }: Props) => { +export const AnnotationTooltip2 = ({ annoVals, annoIdx, timeZone, isPinned, onClose, onEdit }: Props) => { const annoId = annoVals.id?.[annoIdx]; const styles = useStyles2(getStyles); - + const focusRef = React.useRef(null); const { canEditAnnotations = retFalse, canDeleteAnnotations = retFalse, onAnnotationDelete } = usePanelContext(); - const dashboardUID = annoVals.dashboardUID?.[annoIdx]; // grafana can be configured to load alert rules from loki. Those annotations cannot be edited or deleted. The id being 0 is the best indicator the annotation came from loki const canEdit = annoId !== 0 && canEditAnnotations(dashboardUID); const canDelete = annoId !== 0 && canDeleteAnnotations(dashboardUID) && onAnnotationDelete != null; + React.useEffect(() => { + if (isPinned) { + focusRef.current?.focus(); + } + }, [isPinned]); + const timeFormatter = (value: number) => dateTimeFormat(value, { format: systemDateFormats.fullDate, @@ -73,8 +80,20 @@ export const AnnotationTooltip2 = ({ annoVals, annoIdx, timeZone, onEdit }: Prop {time}
- {(canEdit || canDelete) && ( -
+ {(canEdit || canDelete || isPinned) && ( +
+ {isPinned && ( + { + // Don't trigger onClick + e.stopPropagation(); + onClose(); + }} + tooltip={t('timeseries.annotation-tooltip2.tooltip-close', 'Close')} + /> + )} {canEdit && ( ({ color: theme.colors.text.primary, fontWeight: 400, }), - editControls: css({ + controls: css({ display: 'flex', '> :last-child': { marginLeft: 0, diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 0cdda8b0a2a..91effc0d184 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -13044,9 +13044,11 @@ "label-tags": "Tags", "placeholder-add-tags": "Add tags", "save": "Save", - "saving": "Saving" + "saving": "Saving", + "tooltip-close": "Close" }, "annotation-tooltip2": { + "tooltip-close": "Close", "tooltip-delete": "Delete", "tooltip-edit": "Edit" },