mirror of
				https://github.com/open-metadata/OpenMetadata.git
				synced 2025-10-30 18:17:53 +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
	 Shailesh Parmar
						Shailesh Parmar