mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-10-29 17:49:14 +00:00
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:
parent
d58cc28fba
commit
83968f529c
@ -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 {
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 }) => {
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
|
||||
@ -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: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user