diff --git a/public/app/plugins/panel/timeseries/plugins/ThresholdControlsPlugin.tsx b/public/app/plugins/panel/timeseries/plugins/ThresholdControlsPlugin.tsx index 3be5e750016..6071bf912cf 100644 --- a/public/app/plugins/panel/timeseries/plugins/ThresholdControlsPlugin.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ThresholdControlsPlugin.tsx @@ -54,16 +54,15 @@ export const ThresholdControlsPlugin: React.FC = ( if (Number.isNaN(yPos) || !Number.isFinite(yPos)) { continue; } - if (yPos < 0 || yPos > plot.bbox.height / window.devicePixelRatio) { - continue; - } + + const height = plot.bbox.height / window.devicePixelRatio; const handle = ( plot.posToVal(y, scale)} formatValue={(v) => getValueFormat(scale)(v, decimals).text} onChange={(value) => { @@ -80,6 +79,7 @@ export const ThresholdControlsPlugin: React.FC = ( }} /> ); + handles.push(handle); } diff --git a/public/app/plugins/panel/timeseries/plugins/ThresholdDragHandle.tsx b/public/app/plugins/panel/timeseries/plugins/ThresholdDragHandle.tsx index 26ef7e18307..33430101ad8 100644 --- a/public/app/plugins/panel/timeseries/plugins/ThresholdDragHandle.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ThresholdDragHandle.tsx @@ -1,9 +1,11 @@ -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { css } from '@emotion/css'; import { Threshold, GrafanaTheme2 } from '@grafana/data'; import { useStyles2, useTheme2 } from '@grafana/ui'; import Draggable, { DraggableBounds } from 'react-draggable'; +type OutOfBounds = 'top' | 'bottom' | 'none'; + interface ThresholdDragHandleProps { step: Threshold; y: number; @@ -22,10 +24,33 @@ export const ThresholdDragHandle: React.FC = ({ onChange, }) => { const theme = useTheme2(); - const styles = useStyles2(getStyles); + let yPos = y; + let outOfBounds: OutOfBounds = 'none'; + + if (y < (dragBounds.top ?? 0)) { + outOfBounds = 'top'; + } + + // there seems to be a 22px offset at the bottom where the threshold line is still drawn + // this is probably offset by the size of the x-axis component + if (y > (dragBounds.bottom ?? 0) + 22) { + outOfBounds = 'bottom'; + } + + if (outOfBounds === 'bottom') { + yPos = dragBounds.bottom ?? y; + } + + if (outOfBounds === 'top') { + yPos = dragBounds.top ?? y; + } + + const styles = useStyles2((theme) => getStyles(theme, step, outOfBounds)); const [currentValue, setCurrentValue] = useState(step.value); - const bgColor = theme.visualization.getColorByName(step.color); - const textColor = theme.colors.getContrastText(bgColor); + + const textColor = useMemo(() => { + return theme.colors.getContrastText(theme.visualization.getColorByName(step.color)); + }, [step.color, theme]); return ( = ({ return false; }} onDrag={(_e, d) => setCurrentValue(mapPositionToValue(d.lastY))} - position={{ x: 0, y }} + position={{ x: 0, y: yPos }} bounds={dragBounds} > -
+
{formatValue(currentValue)}
@@ -52,32 +74,38 @@ export const ThresholdDragHandle: React.FC = ({ ThresholdDragHandle.displayName = 'ThresholdDragHandle'; -const getStyles = (theme: GrafanaTheme2) => { +const getStyles = (theme: GrafanaTheme2, step: Threshold, outOfBounds: OutOfBounds) => { + const mainColor = theme.visualization.getColorByName(step.color); + const arrowStyles = getArrowStyles(outOfBounds); + const isOutOfBounds = outOfBounds !== 'none'; + return { handle: css` + display: flex; + align-items: center; position: absolute; left: 0; width: calc(100% - 9px); height: 18px; - margin-left: 9px; margin-top: -9px; + border-color: ${mainColor}; cursor: grab; + border-top-right-radius: ${theme.shape.borderRadius(1)}; + border-bottom-right-radius: ${theme.shape.borderRadius(1)}; + ${isOutOfBounds && + css` + margin-top: 0; + border-radius: ${theme.shape.borderRadius(1)}; + `} + background: ${mainColor}; font-size: ${theme.typography.bodySmall.fontSize}; &:before { - content: ''; - position: absolute; - left: -9px; - bottom: 0; - width: 0; - height: 0; - border-right-style: solid; - border-right-width: 9px; - border-right-color: inherit; - border-top: 9px solid transparent; - border-bottom: 9px solid transparent; + ${arrowStyles}; } `, handleText: css` + text-align: center; + width: 100%; display: block; text-overflow: ellipsis; white-space: nowrap; @@ -85,3 +113,51 @@ const getStyles = (theme: GrafanaTheme2) => { `, }; }; + +function getArrowStyles(outOfBounds: OutOfBounds) { + const inBounds = outOfBounds === 'none'; + + const triangle = (size: number) => css` + content: ''; + position: absolute; + + bottom: 0; + top: 0; + width: 0; + height: 0; + left: 0; + + border-right-style: solid; + border-right-width: ${size}px; + border-right-color: inherit; + border-top: ${size}px solid transparent; + border-bottom: ${size}px solid transparent; + `; + + if (inBounds) { + return css` + ${triangle(9)}; + left: -9px; + `; + } + + if (outOfBounds === 'top') { + return css` + ${triangle(5)}; + left: calc(50% - 2.5px); + top: -7px; + transform: rotate(90deg); + `; + } + + if (outOfBounds === 'bottom') { + return css` + ${triangle(5)}; + left: calc(50% - 2.5px); + top: calc(100% - 2.5px); + transform: rotate(-90deg); + `; + } + + return ''; +}