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)) {
continue;
}
if (yPos < 0 || yPos > plot.bbox.height / window.devicePixelRatio) {
continue;
}
const height = plot.bbox.height / window.devicePixelRatio;
const handle = (
<ThresholdDragHandle
key={`${step.value}-${i}`}
step={step}
y={yPos}
dragBounds={{ top: 0, bottom: plot.bbox.height / window.devicePixelRatio }}
dragBounds={{ top: 0, bottom: height }}
mapPositionToValue={(y) => plot.posToVal(y, scale)}
formatValue={(v) => getValueFormat(scale)(v, decimals).text}
onChange={(value) => {
@ -80,6 +79,7 @@ export const ThresholdControlsPlugin: React.FC<ThresholdControlsPluginProps> = (
}}
/>
);
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 { 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<ThresholdDragHandleProps> = ({
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 (
<Draggable
@ -37,13 +62,10 @@ export const ThresholdDragHandle: React.FC<ThresholdDragHandleProps> = ({
return false;
}}
onDrag={(_e, d) => setCurrentValue(mapPositionToValue(d.lastY))}
position={{ x: 0, y }}
position={{ x: 0, y: yPos }}
bounds={dragBounds}
>
<div
className={styles.handle}
style={{ color: textColor, background: bgColor, borderColor: bgColor, borderWidth: 0 }}
>
<div className={styles.handle} style={{ color: textColor }}>
<span className={styles.handleText}>{formatValue(currentValue)}</span>
</div>
</Draggable>
@ -52,32 +74,38 @@ export const ThresholdDragHandle: React.FC<ThresholdDragHandleProps> = ({
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 '';
}