1import React, { HTMLAttributes, useCallback, useRef, useState } from 'react';
2import { GrafanaTheme2, dateTimeFormat, systemDateFormats, TimeZone } from '@grafana/data';
3import { Portal, useStyles2, usePanelContext } from '@grafana/ui';
4import { css } from '@emotion/css';
5import { AnnotationEditorForm } from './AnnotationEditorForm';
6import { getCommonAnnotationStyles } from '../styles';
7import { usePopper } from 'react-popper';
8import { getTooltipContainerStyles } from '@grafana/ui/src/themes/mixins';
9import { AnnotationTooltip } from './AnnotationTooltip';
10
11interface Props extends HTMLAttributes<HTMLDivElement> {
12  timeZone: TimeZone;
13  annotation: AnnotationsDataFrameViewDTO;
14}
15
16const POPPER_CONFIG = {
17  modifiers: [
18    { name: 'arrow', enabled: false },
19    {
20      name: 'preventOverflow',
21      enabled: true,
22      options: {
23        rootBoundary: 'viewport',
24      },
25    },
26  ],
27};
28
29export function AnnotationMarker({ annotation, timeZone, style }: Props) {
30  const { canAddAnnotations, ...panelCtx } = usePanelContext();
31  const commonStyles = useStyles2(getCommonAnnotationStyles);
32  const styles = useStyles2(getStyles);
33
34  const [isOpen, setIsOpen] = useState(false);
35  const [isEditing, setIsEditing] = useState(false);
36  const [markerRef, setMarkerRef] = useState<HTMLDivElement | null>(null);
37  const [tooltipRef, setTooltipRef] = useState<HTMLDivElement | null>(null);
38  const [editorRef, setEditorRef] = useState<HTMLDivElement | null>(null);
39
40  const popoverRenderTimeout = useRef<NodeJS.Timer>();
41
42  const popper = usePopper(markerRef, tooltipRef, POPPER_CONFIG);
43  const editorPopper = usePopper(markerRef, editorRef, POPPER_CONFIG);
44
45  const onAnnotationEdit = useCallback(() => {
46    setIsEditing(true);
47    setIsOpen(false);
48  }, [setIsEditing, setIsOpen]);
49
50  const onAnnotationDelete = useCallback(() => {
51    if (panelCtx.onAnnotationDelete) {
52      panelCtx.onAnnotationDelete(annotation.id);
53    }
54  }, [annotation, panelCtx]);
55
56  const onMouseEnter = useCallback(() => {
57    if (popoverRenderTimeout.current) {
58      clearTimeout(popoverRenderTimeout.current);
59    }
60    setIsOpen(true);
61  }, [setIsOpen]);
62
63  const onPopoverMouseEnter = useCallback(() => {
64    if (popoverRenderTimeout.current) {
65      clearTimeout(popoverRenderTimeout.current);
66    }
67  }, []);
68
69  const onMouseLeave = useCallback(() => {
70    popoverRenderTimeout.current = setTimeout(() => {
71      setIsOpen(false);
72    }, 100);
73  }, [setIsOpen]);
74
75  const timeFormatter = useCallback(
76    (value: number) => {
77      return dateTimeFormat(value, {
78        format: systemDateFormats.fullDate,
79        timeZone,
80      });
81    },
82    [timeZone]
83  );
84
85  const renderTooltip = useCallback(() => {
86    return (
87      <AnnotationTooltip
88        annotation={annotation}
89        timeFormatter={timeFormatter}
90        onEdit={onAnnotationEdit}
91        onDelete={onAnnotationDelete}
92        editable={Boolean(canAddAnnotations && canAddAnnotations())}
93      />
94    );
95  }, [canAddAnnotations, onAnnotationDelete, onAnnotationEdit, timeFormatter, annotation]);
96
97  const isRegionAnnotation = Boolean(annotation.isRegion);
98
99  let marker = (
100    <div className={commonStyles(annotation).markerTriangle} style={{ transform: 'translate3d(-100%,-50%, 0)' }} />
101  );
102
103  if (isRegionAnnotation) {
104    marker = (
105      <div className={commonStyles(annotation).markerBar} style={{ ...style, transform: 'translate3d(0,-50%, 0)' }} />
106    );
107  }
108  return (
109    <>
110      <div
111        ref={setMarkerRef}
112        onMouseEnter={onMouseEnter}
113        onMouseLeave={onMouseLeave}
114        className={!isRegionAnnotation ? styles.markerWrapper : undefined}
115      >
116        {marker}
117      </div>
118
119      {isOpen && (
120        <Portal>
121          <div
122            ref={setTooltipRef}
123            style={popper.styles.popper}
124            {...popper.attributes.popper}
125            className={styles.tooltip}
126            onMouseEnter={onPopoverMouseEnter}
127            onMouseLeave={onMouseLeave}
128          >
129            {renderTooltip()}
130          </div>
131        </Portal>
132      )}
133
134      {isEditing && (
135        <Portal>
136          <AnnotationEditorForm
137            onDismiss={() => setIsEditing(false)}
138            onSave={() => setIsEditing(false)}
139            timeFormatter={timeFormatter}
140            annotation={annotation}
141            ref={setEditorRef}
142            style={editorPopper.styles.popper}
143            {...editorPopper.attributes.popper}
144          />
145        </Portal>
146      )}
147    </>
148  );
149}
150
151const getStyles = (theme: GrafanaTheme2) => {
152  return {
153    markerWrapper: css`
154      label: markerWrapper;
155      padding: 0 4px 4px 4px;
156    `,
157    wrapper: css`
158      max-width: 400px;
159    `,
160    tooltip: css`
161      ${getTooltipContainerStyles(theme)};
162      padding: 0;
163    `,
164  };
165};
166