Timeseries Panel: render threshold at either top or bottom of graph when it is out… (#41649)

This commit is contained in:
Gilles De Mey 2021-11-23 16:18:07 +01:00 committed by GitHub
parent 20e1457a78
commit 3e16abc939
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 102 additions and 26 deletions

View File

@ -54,16 +54,15 @@ export const ThresholdControlsPlugin: React.FC<ThresholdControlsPluginProps> = (
if (Number.isNaN(yPos) || !Number.isFinite(yPos)) { if (Number.isNaN(yPos) || !Number.isFinite(yPos)) {
continue; continue;
} }
if (yPos < 0 || yPos > plot.bbox.height / window.devicePixelRatio) {
continue; const height = plot.bbox.height / window.devicePixelRatio;
}
const handle = ( const handle = (
<ThresholdDragHandle <ThresholdDragHandle
key={`${step.value}-${i}`} key={`${step.value}-${i}`}
step={step} step={step}
y={yPos} y={yPos}
dragBounds={{ top: 0, bottom: plot.bbox.height / window.devicePixelRatio }} dragBounds={{ top: 0, bottom: height }}
mapPositionToValue={(y) => plot.posToVal(y, scale)} mapPositionToValue={(y) => plot.posToVal(y, scale)}
formatValue={(v) => getValueFormat(scale)(v, decimals).text} formatValue={(v) => getValueFormat(scale)(v, decimals).text}
onChange={(value) => { onChange={(value) => {
@ -80,6 +79,7 @@ export const ThresholdControlsPlugin: React.FC<ThresholdControlsPluginProps> = (
}} }}
/> />
); );
handles.push(handle); handles.push(handle);
} }

View File

@ -1,9 +1,11 @@
import React, { useState } from 'react'; import React, { useMemo, useState } from 'react';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { Threshold, GrafanaTheme2 } from '@grafana/data'; import { Threshold, GrafanaTheme2 } from '@grafana/data';
import { useStyles2, useTheme2 } from '@grafana/ui'; import { useStyles2, useTheme2 } from '@grafana/ui';
import Draggable, { DraggableBounds } from 'react-draggable'; import Draggable, { DraggableBounds } from 'react-draggable';
type OutOfBounds = 'top' | 'bottom' | 'none';
interface ThresholdDragHandleProps { interface ThresholdDragHandleProps {
step: Threshold; step: Threshold;
y: number; y: number;
@ -22,10 +24,33 @@ export const ThresholdDragHandle: React.FC<ThresholdDragHandleProps> = ({
onChange, onChange,
}) => { }) => {
const theme = useTheme2(); 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 [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 (
<Draggable <Draggable
@ -37,13 +62,10 @@ export const ThresholdDragHandle: React.FC<ThresholdDragHandleProps> = ({
return false; return false;
}} }}
onDrag={(_e, d) => setCurrentValue(mapPositionToValue(d.lastY))} onDrag={(_e, d) => setCurrentValue(mapPositionToValue(d.lastY))}
position={{ x: 0, y }} position={{ x: 0, y: yPos }}
bounds={dragBounds} bounds={dragBounds}
> >
<div <div className={styles.handle} style={{ color: textColor }}>
className={styles.handle}
style={{ color: textColor, background: bgColor, borderColor: bgColor, borderWidth: 0 }}
>
<span className={styles.handleText}>{formatValue(currentValue)}</span> <span className={styles.handleText}>{formatValue(currentValue)}</span>
</div> </div>
</Draggable> </Draggable>
@ -52,32 +74,38 @@ export const ThresholdDragHandle: React.FC<ThresholdDragHandleProps> = ({
ThresholdDragHandle.displayName = 'ThresholdDragHandle'; 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 { return {
handle: css` handle: css`
display: flex;
align-items: center;
position: absolute; position: absolute;
left: 0; left: 0;
width: calc(100% - 9px); width: calc(100% - 9px);
height: 18px; height: 18px;
margin-left: 9px;
margin-top: -9px; margin-top: -9px;
border-color: ${mainColor};
cursor: grab; 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}; font-size: ${theme.typography.bodySmall.fontSize};
&:before { &:before {
content: ''; ${arrowStyles};
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;
} }
`, `,
handleText: css` handleText: css`
text-align: center;
width: 100%;
display: block; display: block;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; 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 '';
}