Minor: dynamic min/max bound in graph for test case details page (#16944)

* Minor: dynamic min/max bound in graph for test case details page

* added dynamic graph

* minor fix
This commit is contained in:
Shailesh Parmar 2024-07-09 20:31:55 +05:30 committed by GitHub
parent d58cc28fba
commit 83968f529c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 299 additions and 73 deletions

View File

@ -133,7 +133,7 @@ export type TestCaseAction = {
export type TestCaseChartDataType = {
information: { label: string; color: string }[];
data: Record<string, string | number | undefined | Thread>[];
data: Record<string, string | number | undefined | Thread | number[]>[];
};
export interface LineChartRef {

View File

@ -12,14 +12,15 @@
*/
import { Typography } from 'antd';
import { first, isEmpty, isUndefined, omitBy, round } from 'lodash';
import { first, isEmpty, isUndefined } from 'lodash';
import React, { ReactElement, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Area,
CartesianGrid,
ComposedChart,
Legend,
Line,
LineChart,
LineProps,
ReferenceArea,
ReferenceLine,
@ -39,7 +40,6 @@ import {
YELLOW_2,
} from '../../../../constants/Color.constants';
import { GRAPH_BACKGROUND_COLOR } from '../../../../constants/constants';
import { COLORS } from '../../../../constants/profiler.constant';
import { ERROR_PLACEHOLDER_TYPE } from '../../../../enums/common.enum';
import {
Thread,
@ -47,13 +47,11 @@ import {
} from '../../../../generated/entity/feed/thread';
import { TestCaseStatus } from '../../../../generated/tests/testCase';
import { axisTickFormatter } from '../../../../utils/ChartUtils';
import { prepareChartData } from '../../../../utils/DataQuality/TestSummaryGraphUtils';
import { formatDateTime } from '../../../../utils/date-time/DateTimeUtils';
import { useActivityFeedProvider } from '../../../ActivityFeed/ActivityFeedProvider/ActivityFeedProvider';
import ErrorPlaceHolder from '../../../common/ErrorWithPlaceholder/ErrorPlaceHolder';
import {
LineChartRef,
TestCaseChartDataType,
} from '../ProfilerDashboard/profilerDashboard.interface';
import { LineChartRef } from '../ProfilerDashboard/profilerDashboard.interface';
import TestSummaryCustomTooltip from '../TestSummaryCustomTooltip/TestSummaryCustomTooltip.component';
import { TestSummaryGraphProps } from './TestSummaryGraph.interface';
@ -83,50 +81,12 @@ function TestSummaryGraph({
}, [chartRef, chartMouseEvent]);
const chartData = useMemo(() => {
const chartData: TestCaseChartDataType['data'] = [];
testCaseResults.forEach((result) => {
const values = result.testResultValue?.reduce((acc, curr) => {
const value = round(parseFloat(curr.value ?? ''), 2) || 0;
return {
...acc,
[curr.name ?? 'value']: value,
};
}, {});
const metric = {
passedRows: result.passedRows,
failedRows: result.failedRows,
passedRowsPercentage: isUndefined(result.passedRowsPercentage)
? undefined
: `${round(result.passedRowsPercentage, 2)}%`,
failedRowsPercentage: isUndefined(result.failedRowsPercentage)
? undefined
: `${round(result.failedRowsPercentage, 2)}%`,
};
chartData.push({
name: result.timestamp,
status: result.testCaseStatus,
...values,
...omitBy(metric, isUndefined),
incidentId: result.incidentId,
task: entityThread.find(
(task) => task.task?.testCaseResolutionStatusId === result.incidentId
),
});
return prepareChartData({
testCaseParameterValue: testCaseParameterValue ?? [],
testCaseResults,
entityThread,
});
chartData.reverse();
return {
information:
testCaseResults[0]?.testResultValue?.map((info, i) => ({
label: info.name ?? '',
color: COLORS[i],
})) ?? [],
data: chartData,
};
}, [testCaseResults, entityThread]);
}, [testCaseResults, entityThread, testCaseParameterValue]);
const incidentData = useMemo(() => {
const data = chartData.data ?? [];
@ -191,10 +151,10 @@ function TestSummaryGraph({
);
};
const referenceArea = () => {
const referenceArea = useMemo(() => {
const params = testCaseParameterValue ?? [];
if (params.length && params.length < 2) {
if (params.length === 1) {
return (
<ReferenceLine
label={params[0].name}
@ -203,20 +163,9 @@ function TestSummaryGraph({
/>
);
}
const yValues = params.reduce((acc, curr, i) => {
return { ...acc, [`y${i + 1}`]: parseInt(curr.value ?? '') };
}, {});
return (
<ReferenceArea
fill={GREEN_3_OPACITY}
ifOverflow="extendDomain"
stroke={GREEN_3}
strokeDasharray="4"
{...yValues}
/>
);
};
return <></>;
}, [testCaseParameterValue]);
if (isEmpty(testCaseResults)) {
return (
@ -246,7 +195,7 @@ function TestSummaryGraph({
className="bg-white"
id={`${testCaseName}_graph`}
minHeight={minHeight ?? 400}>
<LineChart
<ComposedChart
data={chartData.data}
margin={{
top: 16,
@ -278,8 +227,18 @@ function TestSummaryGraph({
position={{ y: 100 }}
wrapperStyle={{ pointerEvents: 'auto' }}
/>
{referenceArea()}
{referenceArea}
<Legend payload={customLegendPayLoad} />
<Area
connectNulls
activeDot={false}
dataKey="boundArea"
dot={false}
fill={GREEN_3_OPACITY}
stroke={GREEN_3}
strokeDasharray="4"
type="monotone"
/>
{chartData?.information?.map((info) => (
<Line
dataKey={info.label}
@ -300,7 +259,7 @@ function TestSummaryGraph({
x2={data.x2}
/>
))}
</LineChart>
</ComposedChart>
</ResponsiveContainer>
);
}

View File

@ -27,7 +27,7 @@ const TestSummaryCustomTooltip = (
const { t } = useTranslation();
const { active, payload = [] } = props;
const data = payload.length
? entries(omit(payload[0].payload, ['name', 'incidentId']))
? entries(omit(payload[0].payload, ['name', 'incidentId', 'boundArea']))
: [];
if (!active || payload.length === 0) {

View File

@ -39,7 +39,7 @@ import {
PAGE_SIZE_MEDIUM,
} from '../../constants/constants';
import { PAGE_HEADERS } from '../../constants/PageHeaders.constant';
import { DEFAULT_SELECTED_RANGE } from '../../constants/profiler.constant';
import { PROFILER_FILTER_RANGE } from '../../constants/profiler.constant';
import { usePermissionProvider } from '../../context/PermissionProvider/PermissionProvider';
import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum';
import { EntityTabs, EntityType, FqnPart } from '../../enums/entity.enum';
@ -84,13 +84,20 @@ import { Option } from '../TasksPage/TasksPage.interface';
import { TestCaseIncidentStatusData } from './IncidentManager.interface';
const IncidentManagerPage = () => {
const defaultRange = useMemo(
() => ({
key: 'last30days',
title: PROFILER_FILTER_RANGE.last30days.title,
}),
[]
);
const [testCaseListData, setTestCaseListData] =
useState<TestCaseIncidentStatusData>({
data: [],
isLoading: true,
});
const [filters, setFilters] = useState<TestCaseIncidentStatusParams>({
startTs: getEpochMillisForPastDays(DEFAULT_SELECTED_RANGE.days),
startTs: getEpochMillisForPastDays(PROFILER_FILTER_RANGE.last30days.days),
endTs: getCurrentMillis(),
});
const [users, setUsers] = useState<{
@ -506,6 +513,7 @@ const IncidentManagerPage = () => {
</Space>
<DatePickerMenu
showSelectedCustomRange
defaultDateRange={defaultRange}
handleDateRangeChange={handleDateRangeChange}
/>
</Col>

View File

@ -135,12 +135,16 @@ describe('Tests for CommonUtils', () => {
{ value: 1000, result: '1K' },
{ value: 10000, result: '10K' },
{ value: 10200, result: '10.2K' },
{ value: 10230, result: '10.23K' },
{ value: 1000000, result: '1M' },
{ value: 1230000, result: '1.23M' },
{ value: 100000000, result: '100M' },
{ value: 1000000000, result: '1B' },
{ value: 1500000000, result: '1.5B' },
{ value: 1550000000, result: '1.55B' },
{ value: 1000000000000, result: '1T' },
{ value: 1100000000000, result: '1.1T' },
{ value: 1110000000000, result: '1.11T' },
];
values.map(({ value, result }) => {

View File

@ -594,7 +594,7 @@ export const digitFormatter = (value: number) => {
// convert 1000 to 1k
return Intl.NumberFormat('en', {
notation: 'compact',
maximumFractionDigits: 1,
maximumFractionDigits: 2,
}).format(value);
};

View File

@ -0,0 +1,170 @@
/*
* Copyright 2024 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
prepareChartData,
PrepareChartDataType,
} from './TestSummaryGraphUtils';
jest.mock('../../constants/profiler.constant', () => {
return {
COLORS: ['#7147E8', '#B02AAC', '#B02AAC', '#1890FF', '#008376'],
};
});
describe('prepareChartData', () => {
it('should prepare chart data correctly', () => {
const testObj = {
testCaseParameterValue: [
{
name: 'minValueForMaxInCol',
value: '1720165283528',
},
{
name: 'maxValueForMaxInCol',
value: '1720275283528',
},
],
testCaseResults: [
{
timestamp: 1720525804736,
testCaseStatus: 'Failed',
result:
'Found max=1720520076998 vs. the expected min=1720165283528.0, max=1720275283528.0.',
testResultValue: [
{
name: 'max',
value: '1720520076998',
},
],
incidentId: '3093dbee-196b-4284-9f97-7103063d0dd7',
maxBound: 1720275283528,
minBound: 1720165283528,
},
{
timestamp: 1720525503943,
testCaseStatus: 'Failed',
result:
'Found max=1720520076998 vs. the expected min=1720165283528.0, max=1720275283528.0.',
testResultValue: [
{
name: 'max',
value: '1720520076998',
},
],
incidentId: '3093dbee-196b-4284-9f97-7103063d0dd7',
maxBound: 1720275283528,
minBound: 1720165283528,
},
],
entityThread: [],
} as PrepareChartDataType;
const result = prepareChartData(testObj);
expect(result).toEqual({
data: [
{
boundArea: [1720165283528, 1720275283528],
incidentId: '3093dbee-196b-4284-9f97-7103063d0dd7',
max: 1720520076998,
name: 1720525503943,
status: 'Failed',
task: undefined,
},
{
boundArea: [1720165283528, 1720275283528],
incidentId: '3093dbee-196b-4284-9f97-7103063d0dd7',
max: 1720520076998,
name: 1720525804736,
status: 'Failed',
task: undefined,
},
],
information: [
{
color: '#7147E8',
label: 'max',
},
],
});
});
it('should handle empty testCaseParameterValue correctly', () => {
const testObj = {
testCaseParameterValue: [],
testCaseResults: [
{
timestamp: 1720525804736,
testCaseStatus: 'Failed',
result:
'Found max=1720520076998 vs. the expected min=1720165283528.0, max=1720275283528.0.',
testResultValue: [
{
name: 'max',
value: '1720520076998',
},
],
incidentId: '3093dbee-196b-4284-9f97-7103063d0dd7',
maxBound: 1720275283528,
minBound: 1720165283528,
},
],
entityThread: [],
} as PrepareChartDataType;
const result = prepareChartData(testObj);
expect(result).toEqual({
data: [
{
boundArea: [1720165283528, 1720275283528],
incidentId: '3093dbee-196b-4284-9f97-7103063d0dd7',
max: 1720520076998,
name: 1720525804736,
status: 'Failed',
task: undefined,
},
],
information: [
{
color: '#7147E8',
label: 'max',
},
],
});
});
it('should handle empty testCaseResults correctly', () => {
const testObj = {
testCaseParameterValue: [
{
name: 'minValueForMaxInCol',
value: '1720165283528',
},
{
name: 'maxValueForMaxInCol',
value: '1720275283528',
},
],
testCaseResults: [],
entityThread: [],
} as PrepareChartDataType;
const result = prepareChartData(testObj);
expect(result).toEqual({
data: [],
information: [],
});
});
});

View File

@ -0,0 +1,85 @@
/*
* Copyright 2024 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { isUndefined, omitBy, round } from 'lodash';
import { TestCaseChartDataType } from '../../components/Database/Profiler/ProfilerDashboard/profilerDashboard.interface';
import { COLORS } from '../../constants/profiler.constant';
import { Thread } from '../../generated/entity/feed/thread';
import {
TestCaseParameterValue,
TestCaseResult,
} from '../../generated/tests/testCase';
export type PrepareChartDataType = {
testCaseParameterValue: TestCaseParameterValue[];
testCaseResults: TestCaseResult[];
entityThread: Thread[];
};
export const prepareChartData = ({
testCaseParameterValue,
testCaseResults,
entityThread,
}: PrepareChartDataType) => {
const params =
testCaseParameterValue.length === 2 ? testCaseParameterValue : [];
const dataPoints: TestCaseChartDataType['data'] = [];
const yValues = params.reduce((acc, curr, i) => {
return { ...acc, [`y${i + 1}`]: parseInt(curr.value ?? '') };
}, {});
testCaseResults.forEach((result) => {
const values = result.testResultValue?.reduce((acc, curr) => {
const value = round(parseFloat(curr.value ?? ''), 2) || 0;
return {
...acc,
[curr.name ?? 'value']: value,
};
}, {});
const metric = {
passedRows: result.passedRows,
failedRows: result.failedRows,
passedRowsPercentage: isUndefined(result.passedRowsPercentage)
? undefined
: `${round(result.passedRowsPercentage, 2)}%`,
failedRowsPercentage: isUndefined(result.failedRowsPercentage)
? undefined
: `${round(result.failedRowsPercentage, 2)}%`,
};
dataPoints.push({
name: result.timestamp,
status: result.testCaseStatus,
...values,
...omitBy(metric, isUndefined),
boundArea: [
result?.minBound ?? yValues.y1,
result?.maxBound ?? yValues.y2,
],
incidentId: result.incidentId,
task: entityThread.find(
(task) => task.task?.testCaseResolutionStatusId === result.incidentId
),
});
});
dataPoints.reverse();
return {
information:
testCaseResults[0]?.testResultValue?.map((info, i) => ({
label: info.name ?? '',
color: COLORS[i],
})) ?? [],
data: dataPoints,
};
};