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