mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-14 05:25:57 +08:00
[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:
parent
58ccbdbec4
commit
9333fdcd0b
@ -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,
|
||||
|
@ -113,6 +113,7 @@ function FullView({
|
||||
onDragSelect,
|
||||
graphsVisibilityStates,
|
||||
setGraphsVisibilityStates,
|
||||
thresholds: widget.thresholds,
|
||||
});
|
||||
|
||||
setChartOptions(newChartOptions);
|
||||
|
@ -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,
|
||||
|
@ -61,6 +61,7 @@ function WidgetGraph({
|
||||
dimensions: containerDimensions,
|
||||
isDarkMode,
|
||||
onDragSelect,
|
||||
thresholds,
|
||||
fillSpans,
|
||||
}),
|
||||
[
|
||||
@ -70,6 +71,7 @@ function WidgetGraph({
|
||||
containerDimensions,
|
||||
isDarkMode,
|
||||
onDragSelect,
|
||||
thresholds,
|
||||
fillSpans,
|
||||
],
|
||||
);
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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}>
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -182,6 +182,7 @@ function RightContainer({
|
||||
thresholds={thresholds}
|
||||
setThresholds={setThresholds}
|
||||
yAxisUnit={yAxisUnit}
|
||||
selectedGraph={selectedGraph}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
|
283
frontend/src/lib/getConvertedValue.ts
Normal file
283
frontend/src/lib/getConvertedValue.ts
Normal 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;
|
||||
}
|
@ -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: [
|
||||
|
Loading…
x
Reference in New Issue
Block a user