[Feat]: Uplot Threshold in Time Series. (#3974)

* refactor: resolve merge conflict

* refactor: added support to value conversion

* refactor: linter fixes

* refactor: build fixes
This commit is contained in:
Rajat Dabade 2023-11-15 19:17:06 +05:30 committed by GitHub
parent 58ccbdbec4
commit 9333fdcd0b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 401 additions and 54 deletions

View File

@ -118,10 +118,18 @@ function ChartPreview({
apiResponse: queryResponse?.data?.payload,
dimensions: containerDimensions,
isDarkMode,
thresholdText: `${t(
'preview_chart_threshold_label',
)} (y=${thresholdValue} ${query?.unit || ''})`,
thresholdValue,
thresholds: [
{
index: '0', // no impact
keyIndex: 0,
moveThreshold: (): void => {},
selectedGraph: PANEL_TYPES.TIME_SERIES, // no impact
thresholdValue,
thresholdLabel: `${t(
'preview_chart_threshold_label',
)} (y=${thresholdValue} ${query?.unit || ''})`,
},
],
}),
[
query?.unit,

View File

@ -113,6 +113,7 @@ function FullView({
onDragSelect,
graphsVisibilityStates,
setGraphsVisibilityStates,
thresholds: widget.thresholds,
});
setChartOptions(newChartOptions);

View File

@ -108,10 +108,12 @@ function GridCardGraph({
onDragSelect,
yAxisUnit: widget?.yAxisUnit,
onClickHandler,
thresholds: widget.thresholds,
}),
[
widget?.id,
widget?.yAxisUnit,
widget.thresholds,
queryResponse.data?.payload,
containerDimensions,
isDarkMode,

View File

@ -61,6 +61,7 @@ function WidgetGraph({
dimensions: containerDimensions,
isDarkMode,
onDragSelect,
thresholds,
fillSpans,
}),
[
@ -70,6 +71,7 @@ function WidgetGraph({
containerDimensions,
isDarkMode,
onDragSelect,
thresholds,
fillSpans,
],
);

View File

@ -1,7 +1,16 @@
import './Threshold.styles.scss';
import { CheckOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons';
import { Card, Divider, InputNumber, Select, Space, Typography } from 'antd';
import {
Card,
Divider,
Input,
InputNumber,
Select,
Space,
Typography,
} from 'antd';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useRef, useState } from 'react';
import { useDrag, useDrop, XYCoord } from 'react-dnd';
@ -24,6 +33,8 @@ function Threshold({
setThresholds,
keyIndex,
moveThreshold,
selectedGraph,
thresholdLabel = '',
}: ThresholdProps): JSX.Element {
const [isEditMode, setIsEditMode] = useState<boolean>(isEditEnabled);
const [operator, setOperator] = useState<string | number>(
@ -35,6 +46,7 @@ function Threshold({
const [format, setFormat] = useState<ThresholdProps['thresholdFormat']>(
thresholdFormat,
);
const [label, setLabel] = useState<string>(thresholdLabel);
const isDarkMode = useIsDarkMode();
@ -54,6 +66,7 @@ function Threshold({
thresholdOperator: operator as ThresholdProps['thresholdOperator'],
thresholdUnit: unit,
thresholdValue: value,
thresholdLabel: label,
};
}
return threshold;
@ -148,6 +161,11 @@ function Threshold({
const opacity = isDragging ? 0 : 1;
drag(drop(ref));
const handleLabelChange = (
event: React.ChangeEvent<HTMLInputElement>,
): void => {
setLabel(event.target.value);
};
return (
<div
@ -178,18 +196,30 @@ function Threshold({
</div>
<div>
<Space>
<Typography.Text>If value is</Typography.Text>
{isEditMode ? (
<Select
style={{ maxWidth: '73px', backgroundColor: '#141414' }}
bordered={false}
defaultValue={operator}
options={operatorOptions}
onChange={handleOperatorChange}
showSearch
/>
) : (
<ShowCaseValue width="49px" value={operator} />
{selectedGraph === PANEL_TYPES.TIME_SERIES && (
<>
<Typography.Text>Label</Typography.Text>
{isEditMode ? (
<Input defaultValue={label} onChange={handleLabelChange} />
) : (
<ShowCaseValue width="180px" value={label} />
)}
</>
)}
{selectedGraph === PANEL_TYPES.VALUE && (
<>
<Typography.Text>If value is</Typography.Text>
{isEditMode ? (
<Select
style={{ minWidth: '73px' }}
defaultValue={operator}
options={operatorOptions}
onChange={handleOperatorChange}
/>
) : (
<ShowCaseValue width="49px" value={operator} />
)}
</>
)}
</Space>
</div>
@ -228,18 +258,17 @@ function Threshold({
) : (
<ShowCaseValue width="100px" value={<CustomColor color={color} />} />
)}
{isEditMode ? (
<Select
style={{ maxWidth: '100px', backgroundColor: '#141414' }}
bordered={false}
defaultValue={format}
options={showAsOptions}
onChange={handlerFormatChange}
showSearch
/>
) : (
<ShowCaseValue width="100px" value={format} />
)}
{isEditMode && selectedGraph === PANEL_TYPES.VALUE ? (
<>
<Select
style={{ minWidth: '100px' }}
defaultValue={format}
options={showAsOptions}
onChange={handlerFormatChange}
/>
<ShowCaseValue width="100px" value={format} />
</>
) : null}
</Space>
</Space>
</div>
@ -255,6 +284,7 @@ Threshold.defaultProps = {
thresholdUnit: undefined,
thresholdColor: undefined,
thresholdFormat: undefined,
thresholdLabel: undefined,
isEditEnabled: false,
thresholdDeleteHandler: undefined,
};

View File

@ -13,6 +13,7 @@ function ThresholdSelector({
thresholds,
setThresholds,
yAxisUnit,
selectedGraph,
}: ThresholdSelectorProps): JSX.Element {
const moveThreshold = useCallback(
(dragIndex: number, hoverIndex: number) => {
@ -42,6 +43,7 @@ function ThresholdSelector({
thresholdValue: 0,
moveThreshold,
keyIndex: thresholds.length,
selectedGraph,
},
]);
};
@ -71,6 +73,8 @@ function ThresholdSelector({
setThresholds={setThresholds}
keyIndex={idx}
moveThreshold={moveThreshold}
selectedGraph={selectedGraph}
thresholdLabel={threshold.thresholdLabel}
/>
))}
<Button className="threshold-selector-button" onClick={addThresholdHandler}>

View File

@ -1,3 +1,4 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { Dispatch, ReactNode, SetStateAction } from 'react';
type ThresholdOperators = '>' | '<' | '>=' | '<=' | '=';
@ -12,8 +13,10 @@ export type ThresholdProps = {
thresholdColor?: string;
thresholdFormat?: 'Text' | 'Background';
isEditEnabled?: boolean;
thresholdLabel?: string;
setThresholds?: Dispatch<SetStateAction<ThresholdProps[]>>;
moveThreshold: (dragIndex: number, hoverIndex: number) => void;
selectedGraph: PANEL_TYPES;
};
export type ShowCaseValueProps = {
@ -29,4 +32,5 @@ export type ThresholdSelectorProps = {
yAxisUnit: string;
thresholds: ThresholdProps[];
setThresholds: Dispatch<SetStateAction<ThresholdProps[]>>;
selectedGraph: PANEL_TYPES;
};

View File

@ -182,6 +182,7 @@ function RightContainer({
thresholds={thresholds}
setThresholds={setThresholds}
yAxisUnit={yAxisUnit}
selectedGraph={selectedGraph}
/>
</Container>
);

View File

@ -0,0 +1,283 @@
const unitsMapping = [
{
label: 'Data',
options: [
{
label: 'bytes(IEC)',
value: 'bytes',
factor: 1,
},
{
label: 'bytes(SI)',
value: 'decbytes',
factor: 1,
},
{
label: 'bits(IEC)',
value: 'bits',
factor: 8, // 1 byte = 8 bits
},
{
label: 'bits(SI)',
value: 'decbits',
factor: 8, // 1 byte = 8 bits
},
{
label: 'kibibytes',
value: 'kbytes',
factor: 1024,
},
{
label: 'kilobytes',
value: 'deckbytes',
factor: 1000,
},
{
label: 'mebibytes',
value: 'mbytes',
factor: 1024 * 1024,
},
{
label: 'megabytes',
value: 'decmbytes',
factor: 1000 * 1000,
},
{
label: 'gibibytes',
value: 'gbytes',
factor: 1024 * 1024 * 1024,
},
{
label: 'gigabytes',
value: 'decgbytes',
factor: 1000 * 1000 * 1000,
},
{
label: 'tebibytes',
value: 'tbytes',
factor: 1024 * 1024 * 1024 * 1024,
},
{
label: 'terabytes',
value: 'dectbytes',
factor: 1000 * 1000 * 1000 * 1000,
},
{
label: 'pebibytes',
value: 'pbytes',
factor: 1024 * 1024 * 1024 * 1024 * 1024,
},
{
label: 'petabytes',
value: 'decpbytes',
factor: 1000 * 1000 * 1000 * 1000 * 1000,
},
],
},
{
label: 'DataRate',
options: [
{
label: 'bytes/sec(IEC)',
value: 'binBps',
factor: 1,
},
{
label: 'bytes/sec(SI)',
value: 'Bps',
factor: 1,
},
{
label: 'bits/sec(IEC)',
value: 'binbps',
factor: 8, // 1 byte = 8 bits
},
{
label: 'bits/sec(SI)',
value: 'bps',
factor: 8, // 1 byte = 8 bits
},
{
label: 'kibibytes/sec',
value: 'KiBs',
factor: 1024,
},
{
label: 'kibibits/sec',
value: 'Kibits',
factor: 8 * 1024, // 1 KiB = 8 Kibits
},
{
label: 'kilobytes/sec',
value: 'KBs',
factor: 1000,
},
{
label: 'kilobits/sec',
value: 'Kbits',
factor: 8 * 1000, // 1 KB = 8 Kbits
},
{
label: 'mebibytes/sec',
value: 'MiBs',
factor: 1024 * 1024,
},
{
label: 'mebibits/sec',
value: 'Mibits',
factor: 8 * 1024 * 1024, // 1 MiB = 8 Mibits
},
// ... (other options)
],
},
{
label: 'Time',
options: [
{
label: 'nanoseconds (ns)',
value: 'ns',
factor: 1,
},
{
label: 'microseconds (µs)',
value: 'µs',
factor: 1000, // 1 ms = 1000 µs
},
{
label: 'milliseconds (ms)',
value: 'ms',
factor: 1000 * 1000, // 1 s = 1000 ms
},
{
label: 'seconds (s)',
value: 's',
factor: 1000 * 1000 * 1000, // 1 s = 1000 ms
},
{
label: 'minutes (m)',
value: 'm',
factor: 60 * 1000 * 1000 * 1000, // 1 m = 60 s
},
{
label: 'hours (h)',
value: 'h',
factor: 60 * 60 * 1000 * 1000 * 1000, // 1 h = 60 m
},
{
label: 'days (d)',
value: 'd',
factor: 24 * 60 * 60 * 1000 * 1000 * 1000, // 1 d = 24 h
},
],
},
{
label: 'Throughput',
options: [
{
label: 'counts/sec (cps)',
value: 'cps',
factor: 1,
},
{
label: 'ops/sec (ops)',
value: 'ops',
factor: 1,
},
{
label: 'requests/sec (reqps)',
value: 'reqps',
factor: 1,
},
{
label: 'reads/sec (rps)',
value: 'rps',
factor: 1,
},
{
label: 'writes/sec (wps)',
value: 'wps',
factor: 1,
},
{
label: 'I/O operations/sec (iops)',
value: 'iops',
factor: 1,
},
{
label: 'counts/min (cpm)',
value: 'cpm',
factor: 60, // 1 cpm = 60 cps
},
{
label: 'ops/min (opm)',
value: 'opm',
factor: 60, // 1 opm = 60 ops
},
{
label: 'reads/min (rpm)',
value: 'rpm',
factor: 60, // 1 rpm = 60 rps
},
{
label: 'writes/min (wpm)',
value: 'wpm',
factor: 60, // 1 wpm = 60 wps
},
// ... (other options)
],
},
{
label: 'Miscellaneous',
options: [
{
label: 'Percent (0.0-1.0)',
value: 'percentunit',
factor: 1,
},
],
},
{
label: 'Boolean',
options: [
{
label: 'True / False',
value: 'bool',
factor: 1,
},
{
label: 'Yes / No',
value: 'bool_yes_no',
factor: 1,
},
],
},
];
function findUnitObject(
unitValue: string,
): { label: string; value: string; factor: number } | null {
const unitObj = unitsMapping
.map((category) => category.options.find((unit) => unit.value === unitValue))
.find(Boolean);
return unitObj || null;
}
export function convertValue(
value: number,
currentUnit: string,
targetUnit: string,
): number | null {
if (targetUnit === 'none') {
return value;
}
const currentUnitObj = findUnitObject(currentUnit);
const targetUnitObj = findUnitObject(targetUnit);
if (currentUnitObj && targetUnitObj) {
const baseValue = value * currentUnitObj.factor;
return baseValue / targetUnitObj.factor;
}
return null;
}

View File

@ -4,7 +4,9 @@
import './uPlotLib.styles.scss';
import { FullViewProps } from 'container/GridCardLayout/GridCard/FullView/types';
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
import { Dimensions } from 'hooks/useDimensions';
import { convertValue } from 'lib/getConvertedValue';
import _noop from 'lodash-es/noop';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import uPlot from 'uplot';
@ -24,6 +26,7 @@ interface GetUPlotChartOptions {
onClickHandler?: OnClickPluginOpts['onClick'];
graphsVisibilityStates?: boolean[];
setGraphsVisibilityStates?: FullViewProps['setGraphsVisibilityStates'];
thresholds?: ThresholdProps[];
thresholdValue?: number;
thresholdText?: string;
fillSpans?: boolean;
@ -39,8 +42,7 @@ export const getUPlotChartOptions = ({
onClickHandler = _noop,
graphsVisibilityStates,
setGraphsVisibilityStates,
thresholdValue,
thresholdText,
thresholds,
fillSpans,
}: GetUPlotChartOptions): uPlot.Options => ({
id,
@ -86,37 +88,47 @@ export const getUPlotChartOptions = ({
hooks: {
draw: [
(u): void => {
if (thresholdValue) {
const { ctx } = u;
ctx.save();
thresholds?.forEach((threshold) => {
if (threshold.thresholdValue !== undefined) {
const { ctx } = u;
ctx.save();
const yPos = u.valToPos(thresholdValue, 'y', true);
const yPos = u.valToPos(
convertValue(
threshold.thresholdValue,
threshold.thresholdUnit,
yAxisUnit,
),
'y',
true,
);
ctx.strokeStyle = 'red';
ctx.lineWidth = 2;
ctx.setLineDash([10, 5]);
ctx.strokeStyle = threshold.thresholdColor || 'red';
ctx.lineWidth = 2;
ctx.setLineDash([10, 5]);
ctx.beginPath();
ctx.beginPath();
const plotLeft = u.bbox.left; // left edge of the plot area
const plotRight = plotLeft + u.bbox.width; // right edge of the plot area
const plotLeft = u.bbox.left; // left edge of the plot area
const plotRight = plotLeft + u.bbox.width; // right edge of the plot area
ctx.moveTo(plotLeft, yPos);
ctx.lineTo(plotRight, yPos);
ctx.moveTo(plotLeft, yPos);
ctx.lineTo(plotRight, yPos);
ctx.stroke();
ctx.stroke();
// Text configuration
if (thresholdText) {
const text = thresholdText;
const textX = plotRight - ctx.measureText(text).width - 20;
const textY = yPos - 15;
ctx.fillStyle = 'red';
ctx.fillText(text, textX, textY);
// Text configuration
if (threshold.thresholdLabel) {
const text = threshold.thresholdLabel;
const textX = plotRight - ctx.measureText(text).width - 20;
const textY = yPos - 15;
ctx.fillStyle = threshold.thresholdColor || 'red';
ctx.fillText(text, textX, textY);
}
ctx.restore();
}
ctx.restore();
}
});
},
],
setSelect: [