[Feat]: soft min and soft max in uplot chart (#4287)

* feat: soft min and soft max in uplot chart

* fix: build pipeline

* fix: tsc

* refactor: added test case

* refactor: updated logic and added unit test

* refactor: updated logic

* chore: removed placeholder

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
Co-authored-by: Yunus M <myounis.ar@live.com>
This commit is contained in:
Rajat Dabade 2024-01-09 14:19:23 +05:30 committed by GitHub
parent 5fe7948be9
commit 5b39dc36d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 498 additions and 17 deletions

View File

@ -150,6 +150,8 @@ function ChartPreview({
thresholdUnit: alertDef?.condition.targetUnit,
},
],
softMax: null,
softMin: null,
}),
[
yAxisUnit,

View File

@ -132,6 +132,8 @@ function FullView({
thresholds: widget.thresholds,
minTimeScale,
maxTimeScale,
softMax: widget.softMax,
softMin: widget.softMin,
});
setChartOptions(newChartOptions);

View File

@ -135,11 +135,15 @@ function GridCardGraph({
thresholds: widget.thresholds,
minTimeScale,
maxTimeScale,
softMax: widget.softMax,
softMin: widget.softMin,
}),
[
widget?.id,
widget?.yAxisUnit,
widget.thresholds,
widget.softMax,
widget.softMin,
queryResponse.data?.payload,
containerDimensions,
isDarkMode,

View File

@ -20,4 +20,6 @@ export const getWidgetQueryBuilder = ({
timePreferance: 'GLOBAL_TIME',
title,
yAxisUnit,
softMax: null,
softMin: null,
});

View File

@ -63,6 +63,8 @@ function DashboardGraphSlider(): JSX.Element {
panelTypes: name,
query: initialQueriesMap.metrics,
timePreferance: 'GLOBAL_TIME',
softMax: null,
softMin: null,
},
],
},

View File

@ -14,6 +14,8 @@ function WidgetGraphContainer({
selectedTime,
thresholds,
fillSpans = false,
softMax,
softMin,
}: WidgetGraphProps): JSX.Element {
const { selectedDashboard } = useDashboard();
@ -59,6 +61,8 @@ function WidgetGraphContainer({
selectedWidget={selectedWidget}
thresholds={thresholds}
fillSpans={fillSpans}
softMax={softMax}
softMin={softMin}
/>
);
}

View File

@ -23,6 +23,8 @@ function WidgetGraph({
yAxisUnit,
thresholds,
fillSpans,
softMax,
softMin,
}: WidgetGraphProps): JSX.Element {
const { stagedQuery } = useQueryBuilder();
@ -83,6 +85,8 @@ function WidgetGraph({
fillSpans,
minTimeScale,
maxTimeScale,
softMax,
softMin,
}),
[
widgetId,
@ -95,6 +99,8 @@ function WidgetGraph({
fillSpans,
minTimeScale,
maxTimeScale,
softMax,
softMin,
],
);
@ -125,6 +131,8 @@ interface WidgetGraphProps {
SuccessResponse<MetricRangePayloadProps, unknown>,
Error
>;
softMax: number | null;
softMin: number | null;
}
export default WidgetGraph;

View File

@ -17,6 +17,8 @@ function WidgetGraph({
selectedTime,
thresholds,
fillSpans,
softMax,
softMin,
}: WidgetGraphProps): JSX.Element {
const { currentQuery } = useQueryBuilder();
const { selectedDashboard } = useDashboard();
@ -53,6 +55,8 @@ function WidgetGraph({
selectedGraph={selectedGraph}
yAxisUnit={yAxisUnit}
fillSpans={fillSpans}
softMax={softMax}
softMin={softMin}
/>
</Container>
);

View File

@ -11,6 +11,8 @@ function LeftContainer({
selectedTime,
thresholds,
fillSpans,
softMax,
softMin,
}: WidgetGraphProps): JSX.Element {
return (
<>
@ -20,6 +22,8 @@ function LeftContainer({
selectedGraph={selectedGraph}
yAxisUnit={yAxisUnit}
fillSpans={fillSpans}
softMax={softMax}
softMin={softMin}
/>
<QueryContainer>
<QuerySection selectedTime={selectedTime} selectedGraph={selectedGraph} />

View File

@ -30,6 +30,15 @@ export const panelTypeVsThreshold: { [key in PANEL_TYPES]: boolean } = {
[PANEL_TYPES.EMPTY_WIDGET]: false,
} as const;
export const panelTypeVsSoftMinMax: { [key in PANEL_TYPES]: boolean } = {
[PANEL_TYPES.TIME_SERIES]: true,
[PANEL_TYPES.VALUE]: false,
[PANEL_TYPES.TABLE]: false,
[PANEL_TYPES.LIST]: false,
[PANEL_TYPES.TRACE]: false,
[PANEL_TYPES.EMPTY_WIDGET]: false,
} as const;
export const panelTypeVsDragAndDrop: { [key in PANEL_TYPES]: boolean } = {
[PANEL_TYPES.TIME_SERIES]: false,
[PANEL_TYPES.VALUE]: true,

View File

@ -3,6 +3,7 @@ import {
Button,
Divider,
Input,
InputNumber,
Select,
Space,
Switch,
@ -16,7 +17,7 @@ import useCreateAlerts from 'hooks/queryBuilder/useCreateAlerts';
import { Dispatch, SetStateAction, useCallback } from 'react';
import { Widgets } from 'types/api/dashboard/getAll';
import { panelTypeVsThreshold } from './constants';
import { panelTypeVsSoftMinMax, panelTypeVsThreshold } from './constants';
import { Container, Title } from './styles';
import ThresholdSelector from './Threshold/ThresholdSelector';
import { ThresholdProps } from './Threshold/types';
@ -42,6 +43,10 @@ function RightContainer({
selectedWidget,
isFillSpans,
setIsFillSpans,
softMax,
softMin,
setSoftMax,
setSoftMin,
}: RightContainerProps): JSX.Element {
const onChangeHandler = useCallback(
(setFunc: Dispatch<SetStateAction<string>>, value: string) => {
@ -56,6 +61,21 @@ function RightContainer({
const onCreateAlertsHandler = useCreateAlerts(selectedWidget);
const allowThreshold = panelTypeVsThreshold[selectedGraph];
const allowSoftMinMax = panelTypeVsSoftMinMax[selectedGraph];
const softMinHandler = useCallback(
(value: number | null) => {
setSoftMin(value);
},
[setSoftMin],
);
const softMaxHandler = useCallback(
(value: number | null) => {
setSoftMax(value);
},
[setSoftMax],
);
return (
<Container>
@ -129,6 +149,30 @@ function RightContainer({
)}
</Space>
{allowSoftMinMax && (
<>
<Divider />
<Typography.Text style={{ display: 'block', margin: '5px 0' }}>
Soft Min
</Typography.Text>
<InputNumber
type="number"
value={softMin}
style={{ display: 'block', width: '100%' }}
onChange={softMinHandler}
/>
<Typography.Text style={{ display: 'block', margin: '5px 0' }}>
Soft Max
</Typography.Text>
<InputNumber
value={softMax}
type="number"
style={{ display: 'block', width: '100%' }}
onChange={softMaxHandler}
/>
</>
)}
{allowThreshold && (
<>
<Divider />
@ -166,6 +210,10 @@ interface RightContainerProps {
selectedWidget?: Widgets;
isFillSpans: boolean;
setIsFillSpans: Dispatch<SetStateAction<boolean>>;
softMin: number | null;
softMax: number | null;
setSoftMin: Dispatch<SetStateAction<number | null>>;
setSoftMax: Dispatch<SetStateAction<number | null>>;
}
RightContainer.defaultProps = {

View File

@ -104,6 +104,18 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
const [saveModal, setSaveModal] = useState(false);
const [discardModal, setDiscardModal] = useState(false);
const [softMin, setSoftMin] = useState<number | null>(
selectedWidget?.softMin === null || selectedWidget?.softMin === undefined
? null
: selectedWidget?.softMin || 0,
);
const [softMax, setSoftMax] = useState<number | null>(
selectedWidget?.softMax === null || selectedWidget?.softMax === undefined
? null
: selectedWidget?.softMax || 0,
);
const closeModal = (): void => {
setSaveModal(false);
setDiscardModal(false);
@ -178,6 +190,8 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
yAxisUnit,
panelTypes: graphType,
thresholds,
softMin,
softMax,
fillSpans: isFillSpans,
},
...afterWidgets,
@ -213,6 +227,8 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
yAxisUnit,
graphType,
thresholds,
softMin,
softMax,
isFillSpans,
afterWidgets,
updateDashboardMutation,
@ -317,6 +333,8 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
yAxisUnit={yAxisUnit}
thresholds={thresholds}
fillSpans={isFillSpans}
softMax={softMax}
softMin={softMin}
/>
</LeftContainerWrapper>
@ -343,6 +361,10 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
selectedWidget={selectedWidget}
isFillSpans={isFillSpans}
setIsFillSpans={setIsFillSpans}
softMin={softMin}
setSoftMin={setSoftMin}
softMax={softMax}
setSoftMax={setSoftMax}
/>
</RightContainerWrapper>
</PanelContainer>

View File

@ -13,4 +13,6 @@ export interface NewWidgetProps {
export interface WidgetGraphProps extends NewWidgetProps {
selectedTime: timePreferance;
thresholds: ThresholdProps[];
softMin: number | null;
softMax: number | null;
}

View File

@ -60,6 +60,8 @@ function TimeSeriesView({
isDarkMode,
minTimeScale,
maxTimeScale,
softMax: null,
softMin: null,
});
return (

View File

@ -33,6 +33,8 @@ export const addEmptyWidgetInDashboardJSONWithQuery = (
title: '',
timePreferance: 'GLOBAL_TIME',
panelTypes: panelTypes || PANEL_TYPES.TIME_SERIES,
softMax: null,
softMin: null,
},
],
},

View File

@ -35,6 +35,8 @@ interface GetUPlotChartOptions {
fillSpans?: boolean;
minTimeScale?: number;
maxTimeScale?: number;
softMin: number | null;
softMax: number | null;
}
export const getUPlotChartOptions = ({
@ -51,6 +53,8 @@ export const getUPlotChartOptions = ({
setGraphsVisibilityStates,
thresholds,
fillSpans,
softMax,
softMin,
}: GetUPlotChartOptions): uPlot.Options => {
const timeScaleProps = getXAxisScale(minTimeScale, maxTimeScale);
@ -87,11 +91,13 @@ export const getUPlotChartOptions = ({
...timeScaleProps,
},
y: {
...getYAxisScale(
...getYAxisScale({
thresholds,
apiResponse?.data.newResult.data.result,
series: apiResponse?.data.newResult.data.result,
yAxisUnit,
),
softMax,
softMin,
}),
},
},
plugins: [

View File

@ -0,0 +1,211 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
import { QueryDataV3 } from 'types/api/widgets/getQuery';
import { GetYAxisScale, getYAxisScale } from './getYAxisScale';
describe('getYAxisScale', () => {
const mockThresholds: ThresholdProps[] = [
{
index: '1',
keyIndex: 1,
thresholdValue: 10,
thresholdUnit: 'percentunit',
moveThreshold(dragIndex, hoverIndex): void {
console.log(dragIndex, hoverIndex);
},
selectedGraph: PANEL_TYPES.TIME_SERIES,
},
{
index: '2',
keyIndex: 2,
thresholdValue: 20,
thresholdUnit: 'percentunit',
moveThreshold(dragIndex, hoverIndex): void {
console.log(dragIndex, hoverIndex);
},
selectedGraph: PANEL_TYPES.TIME_SERIES,
},
];
const mockSeriesData: QueryDataV3[] = [
{
list: null,
queryName: 'Mock Query',
series: [
{
labels: {},
values: [
{ timestamp: 1, value: '15' },
{ timestamp: 2, value: '25' },
],
},
],
},
];
const mockYAxisUnit = 'percentunit';
const mockSoftMin = 5;
const mockSoftMax = 30;
it('threshold absent, series data absent and softmin and softmax is absent', () => {
const result = getYAxisScale({
thresholds: [],
series: [],
yAxisUnit: undefined,
softMin: null,
softMax: null,
} as GetYAxisScale);
expect(result).toEqual({ auto: true });
});
it('Threshold absent, series data present softmin and softmax present', () => {
const result = getYAxisScale({
thresholds: [],
series: mockSeriesData,
yAxisUnit: mockYAxisUnit,
softMin: mockSoftMin,
softMax: mockSoftMax,
} as GetYAxisScale);
expect(result).toEqual({
auto: false,
range: [5, 30],
});
});
it('Only series data present', () => {
const result = getYAxisScale({
thresholds: [],
series: mockSeriesData,
yAxisUnit: mockYAxisUnit,
softMin: null,
softMax: null,
} as GetYAxisScale);
expect(result).toEqual({ auto: true });
});
it('Threshold absent, series data present, softmin present and softmax absent', () => {
const result = getYAxisScale({
thresholds: [],
series: mockSeriesData,
yAxisUnit: mockYAxisUnit,
softMin: mockSoftMin,
softMax: null,
} as GetYAxisScale);
expect(result).toEqual({
auto: false,
range: [5, 25],
});
});
it('Threshold absent, series data present, softmin absent and softmax present', () => {
const result = getYAxisScale({
thresholds: [],
series: mockSeriesData,
yAxisUnit: mockYAxisUnit,
softMin: null,
softMax: mockSoftMax,
} as GetYAxisScale);
expect(result).toEqual({
auto: false,
range: [15, 30],
});
});
it('Threshold present, series absent and softmin and softmax present', () => {
const result = getYAxisScale({
thresholds: mockThresholds,
series: [],
yAxisUnit: mockYAxisUnit,
softMin: mockSoftMin,
softMax: mockSoftMax,
} as GetYAxisScale);
expect(result).toEqual({
auto: false,
range: [5, 30],
});
});
it('Only threshold data present', () => {
const result = getYAxisScale({
thresholds: mockThresholds,
series: [],
yAxisUnit: mockYAxisUnit,
softMin: null,
softMax: null,
} as GetYAxisScale);
expect(result).toEqual({
auto: false,
range: [10, 20],
});
});
it('Threshold present, series absent, softmin absent and softmax present', () => {
const result = getYAxisScale({
thresholds: mockThresholds,
series: [],
yAxisUnit: mockYAxisUnit,
softMin: null,
softMax: mockSoftMax,
} as GetYAxisScale);
expect(result).toEqual({
auto: false,
range: [10, 30],
});
});
it('Threshold data present, series data absent, softmin present and softmax absent', () => {
const result = getYAxisScale({
thresholds: mockThresholds,
series: [],
yAxisUnit: mockYAxisUnit,
softMin: mockSoftMin,
softMax: null,
} as GetYAxisScale);
expect(result).toEqual({
auto: false,
range: [5, 20],
});
});
it('Threshold data absent, series absent, softmin and softmax present', () => {
const result = getYAxisScale({
thresholds: [],
series: [],
yAxisUnit: mockYAxisUnit,
softMin: mockSoftMin,
softMax: mockSoftMax,
} as GetYAxisScale);
expect(result).toEqual({
range: {
min: { soft: mockSoftMin, mode: 2 },
max: { soft: mockSoftMax, mode: 2 },
},
});
});
it('All data present', () => {
const result = getYAxisScale({
thresholds: mockThresholds,
series: mockSeriesData,
yAxisUnit: mockYAxisUnit,
softMin: mockSoftMin,
softMax: mockSoftMax,
} as GetYAxisScale);
expect(result).toEqual({
auto: false,
range: [5, 30],
});
});
});

View File

@ -2,6 +2,7 @@ import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/typ
import { convertValue } from 'lib/getConvertedValue';
import { isFinite } from 'lodash-es';
import { QueryDataV3 } from 'types/api/widgets/getQuery';
import uPlot from 'uplot';
function findMinMaxValues(data: QueryDataV3[]): [number, number] {
let min = Number.MAX_SAFE_INTEGER;
@ -71,23 +72,167 @@ function areAllSeriesEmpty(series: QueryDataV3[]): boolean {
});
}
export const getYAxisScale = (
thresholds?: ThresholdProps[],
series?: QueryDataV3[],
yAxisUnit?: string,
): {
auto: boolean;
range?: [number, number];
} => {
if (!thresholds || !series || thresholds.length === 0) return { auto: true };
function configSoftMinMax(
softMin: number | null,
softMax: number | null,
): { range: uPlot.Scale.Range } {
return {
range: {
min: {
soft: softMin !== null ? softMin : undefined,
mode: 2,
},
max: {
soft: softMax !== null ? softMax : undefined,
mode: 2,
},
},
};
}
if (areAllSeriesEmpty(series)) return { auto: true };
export const getYAxisScale = ({
thresholds,
series,
yAxisUnit,
softMin,
softMax,
}: // eslint-disable-next-line sonarjs/cognitive-complexity
GetYAxisScale): { auto?: boolean; range?: uPlot.Scale.Range } => {
// Situation: thresholds and series data is absent
if (
(!thresholds || thresholds.length === 0) &&
(!series || areAllSeriesEmpty(series))
) {
// Situation: softMin is not null or softMax is null
if (softMin !== null && softMax === null) {
return configSoftMinMax(softMin, softMin + 100);
}
const [min, max] = getRange(thresholds, series, yAxisUnit);
// Situation: softMin is null softMax is not null
if (softMin === null && softMax !== null) {
return configSoftMinMax(softMax - 100, softMax);
}
// Min and Max value can be same if the value is same for all the series
if (min === max) {
// Situation: softMin is not null and softMax is not null
if (softMin !== null && softMax !== null) {
return configSoftMinMax(softMin, softMax);
}
// Situation: softMin and softMax are null and no threshold and no series data
return { auto: true };
}
// Situation: thresholds are absent
if (!thresholds || thresholds.length === 0) {
// Situation: No thresholds data but series data is present
if (series && !areAllSeriesEmpty(series)) {
// Situation: softMin and softMax are null
if (softMin === null && softMax === null) {
return { auto: true };
}
// Situation: either softMin or softMax is not null
let [min, max] = findMinMaxValues(series);
if (softMin !== null) {
// Compare with softMin if it is not null
min = Math.min(min, softMin);
}
if (softMax !== null) {
// Compare with softMax if it is not null
max = Math.max(max, softMax);
}
if (min === max) {
// Min and Max value can be same if the value is same for all the series
return { auto: true };
}
return { auto: false, range: [min, max] };
}
// Situation: No thresholds data and series data is absent but either soft min and soft max is present
if (softMin !== null && softMax === null) {
return configSoftMinMax(softMin, softMin + 100);
}
if (softMin === null && softMax !== null) {
return configSoftMinMax(softMax - 100, softMax);
}
if (softMin !== null && softMax !== null) {
return configSoftMinMax(softMin, softMax);
}
return { auto: true };
}
if (!series || areAllSeriesEmpty(series)) {
// series data is absent but threshold is present
if (thresholds.length > 0) {
// Situation: thresholds are present and series data is absent
let [min, max] = findMinMaxThresholdValues(thresholds, yAxisUnit);
if (softMin !== null) {
// Compare with softMin if it is not null
min = Math.min(min, softMin);
}
if (softMax !== null) {
// Compare with softMax if it is not null
max = Math.max(max, softMax);
}
if (min === max) {
// Min and Max value can be same if the value is same for all the series
return { auto: true };
}
return { auto: false, range: [min, max] };
}
// Situation: softMin or softMax is not null
if (softMin !== null && softMax === null) {
return configSoftMinMax(softMin, softMin + 100);
}
if (softMin === null && softMax !== null) {
return configSoftMinMax(softMax - 100, softMax);
}
if (softMin !== null && softMax !== null) {
return configSoftMinMax(softMin, softMax);
}
return { auto: true };
}
// Situation: thresholds and series data are present
let [min, max] = getRange(thresholds, series, yAxisUnit);
if (softMin !== null) {
// Compare with softMin if it is not null
min = Math.min(min, softMin);
}
if (softMax !== null) {
// Compare with softMax if it is not null
max = Math.max(max, softMax);
}
if (min === max) {
// Min and Max value can be same if the value is same for all the series
return { auto: true };
}
return { auto: false, range: [min, max] };
};
export type GetYAxisScale = {
thresholds?: ThresholdProps[];
series?: QueryDataV3[];
yAxisUnit?: string;
softMin: number | null;
softMax: number | null;
};

View File

@ -73,6 +73,8 @@ export interface IBaseWidget {
stepSize?: number;
yAxisUnit?: string;
thresholds?: ThresholdProps[];
softMin: number | null;
softMax: number | null;
fillSpans?: boolean;
}
export interface Widgets extends IBaseWidget {