1import React, { useMemo, useState } from 'react'; 2import { css } from '@emotion/css'; 3import { Threshold, GrafanaTheme2 } from '@grafana/data'; 4import { useStyles2, useTheme2 } from '@grafana/ui'; 5import Draggable, { DraggableBounds } from 'react-draggable'; 6 7type OutOfBounds = 'top' | 'bottom' | 'none'; 8 9interface ThresholdDragHandleProps { 10 step: Threshold; 11 y: number; 12 dragBounds: DraggableBounds; 13 mapPositionToValue: (y: number) => number; 14 onChange: (value: number) => void; 15 formatValue: (value: number) => string; 16} 17 18export const ThresholdDragHandle: React.FC<ThresholdDragHandleProps> = ({ 19 step, 20 y, 21 dragBounds, 22 mapPositionToValue, 23 formatValue, 24 onChange, 25}) => { 26 const theme = useTheme2(); 27 let yPos = y; 28 let outOfBounds: OutOfBounds = 'none'; 29 30 if (y < (dragBounds.top ?? 0)) { 31 outOfBounds = 'top'; 32 } 33 34 // there seems to be a 22px offset at the bottom where the threshold line is still drawn 35 // this is probably offset by the size of the x-axis component 36 if (y > (dragBounds.bottom ?? 0) + 22) { 37 outOfBounds = 'bottom'; 38 } 39 40 if (outOfBounds === 'bottom') { 41 yPos = dragBounds.bottom ?? y; 42 } 43 44 if (outOfBounds === 'top') { 45 yPos = dragBounds.top ?? y; 46 } 47 48 const styles = useStyles2((theme) => getStyles(theme, step, outOfBounds)); 49 const [currentValue, setCurrentValue] = useState(step.value); 50 51 const textColor = useMemo(() => { 52 return theme.colors.getContrastText(theme.visualization.getColorByName(step.color)); 53 }, [step.color, theme]); 54 55 return ( 56 <Draggable 57 axis="y" 58 grid={[1, 1]} 59 onStop={(_e, d) => { 60 onChange(mapPositionToValue(d.lastY)); 61 // as of https://github.com/react-grid-layout/react-draggable/issues/390#issuecomment-623237835 62 return false; 63 }} 64 onDrag={(_e, d) => setCurrentValue(mapPositionToValue(d.lastY))} 65 position={{ x: 0, y: yPos }} 66 bounds={dragBounds} 67 > 68 <div className={styles.handle} style={{ color: textColor }}> 69 <span className={styles.handleText}>{formatValue(currentValue)}</span> 70 </div> 71 </Draggable> 72 ); 73}; 74 75ThresholdDragHandle.displayName = 'ThresholdDragHandle'; 76 77const getStyles = (theme: GrafanaTheme2, step: Threshold, outOfBounds: OutOfBounds) => { 78 const mainColor = theme.visualization.getColorByName(step.color); 79 const arrowStyles = getArrowStyles(outOfBounds); 80 const isOutOfBounds = outOfBounds !== 'none'; 81 82 return { 83 handle: css` 84 display: flex; 85 align-items: center; 86 position: absolute; 87 left: 0; 88 width: calc(100% - 9px); 89 height: 18px; 90 margin-top: -9px; 91 border-color: ${mainColor}; 92 cursor: grab; 93 border-top-right-radius: ${theme.shape.borderRadius(1)}; 94 border-bottom-right-radius: ${theme.shape.borderRadius(1)}; 95 ${isOutOfBounds && 96 css` 97 margin-top: 0; 98 border-radius: ${theme.shape.borderRadius(1)}; 99 `} 100 background: ${mainColor}; 101 font-size: ${theme.typography.bodySmall.fontSize}; 102 &:before { 103 ${arrowStyles}; 104 } 105 `, 106 handleText: css` 107 text-align: center; 108 width: 100%; 109 display: block; 110 text-overflow: ellipsis; 111 white-space: nowrap; 112 overflow: hidden; 113 `, 114 }; 115}; 116 117function getArrowStyles(outOfBounds: OutOfBounds) { 118 const inBounds = outOfBounds === 'none'; 119 120 const triangle = (size: number) => css` 121 content: ''; 122 position: absolute; 123 124 bottom: 0; 125 top: 0; 126 width: 0; 127 height: 0; 128 left: 0; 129 130 border-right-style: solid; 131 border-right-width: ${size}px; 132 border-right-color: inherit; 133 border-top: ${size}px solid transparent; 134 border-bottom: ${size}px solid transparent; 135 `; 136 137 if (inBounds) { 138 return css` 139 ${triangle(9)}; 140 left: -9px; 141 `; 142 } 143 144 if (outOfBounds === 'top') { 145 return css` 146 ${triangle(5)}; 147 left: calc(50% - 2.5px); 148 top: -7px; 149 transform: rotate(90deg); 150 `; 151 } 152 153 if (outOfBounds === 'bottom') { 154 return css` 155 ${triangle(5)}; 156 left: calc(50% - 2.5px); 157 top: calc(100% - 2.5px); 158 transform: rotate(-90deg); 159 `; 160 } 161 162 return ''; 163} 164