From 83968f529c90f452442ca97b7e86a0648d9c9ade Mon Sep 17 00:00:00 2001 From: Shailesh Parmar Date: Tue, 9 Jul 2024 20:31:55 +0530 Subject: [PATCH] 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 --- .../profilerDashboard.interface.ts | 2 +- .../Profiler/TestSummary/TestSummaryGraph.tsx | 95 +++------- .../TestSummaryCustomTooltip.component.tsx | 2 +- .../IncidentManager/IncidentManagerPage.tsx | 12 +- .../ui/src/utils/CommonUtils.test.ts | 4 + .../resources/ui/src/utils/CommonUtils.tsx | 2 +- .../DataQuality/TestSummaryGraphUtils.test.ts | 170 ++++++++++++++++++ .../DataQuality/TestSummaryGraphUtils.ts | 85 +++++++++ 8 files changed, 299 insertions(+), 73 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/utils/DataQuality/TestSummaryGraphUtils.test.ts create mode 100644 openmetadata-ui/src/main/resources/ui/src/utils/DataQuality/TestSummaryGraphUtils.ts diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/ProfilerDashboard/profilerDashboard.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/ProfilerDashboard/profilerDashboard.interface.ts index d9169d39e5d..10eb4e80f5d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/ProfilerDashboard/profilerDashboard.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/ProfilerDashboard/profilerDashboard.interface.ts @@ -133,7 +133,7 @@ export type TestCaseAction = { export type TestCaseChartDataType = { information: { label: string; color: string }[]; - data: Record[]; + data: Record[]; }; export interface LineChartRef { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TestSummary/TestSummaryGraph.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TestSummary/TestSummaryGraph.tsx index e5f837c2f8c..2b32881a971 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TestSummary/TestSummaryGraph.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TestSummary/TestSummaryGraph.tsx @@ -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 ( ); } - const yValues = params.reduce((acc, curr, i) => { - return { ...acc, [`y${i + 1}`]: parseInt(curr.value ?? '') }; - }, {}); - return ( - - ); - }; + return <>; + }, [testCaseParameterValue]); if (isEmpty(testCaseResults)) { return ( @@ -246,7 +195,7 @@ function TestSummaryGraph({ className="bg-white" id={`${testCaseName}_graph`} minHeight={minHeight ?? 400}> - - {referenceArea()} + {referenceArea} + {chartData?.information?.map((info) => ( ))} - + ); } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TestSummaryCustomTooltip/TestSummaryCustomTooltip.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TestSummaryCustomTooltip/TestSummaryCustomTooltip.component.tsx index 251dc15cf00..e2a66454f33 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TestSummaryCustomTooltip/TestSummaryCustomTooltip.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TestSummaryCustomTooltip/TestSummaryCustomTooltip.component.tsx @@ -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) { diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/IncidentManager/IncidentManagerPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/IncidentManager/IncidentManagerPage.tsx index dee42c94c43..597704ed1f5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/IncidentManager/IncidentManagerPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/IncidentManager/IncidentManagerPage.tsx @@ -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({ data: [], isLoading: true, }); const [filters, setFilters] = useState({ - 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 = () => { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.test.ts b/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.test.ts index 00025773456..e2b58ee716a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.test.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.test.ts @@ -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 }) => { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.tsx index 3da09bb2de7..4c9b387c017 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.tsx @@ -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); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DataQuality/TestSummaryGraphUtils.test.ts b/openmetadata-ui/src/main/resources/ui/src/utils/DataQuality/TestSummaryGraphUtils.test.ts new file mode 100644 index 00000000000..ee4a5ffba2d --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DataQuality/TestSummaryGraphUtils.test.ts @@ -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: [], + }); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/DataQuality/TestSummaryGraphUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/DataQuality/TestSummaryGraphUtils.ts new file mode 100644 index 00000000000..e07859597ff --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/DataQuality/TestSummaryGraphUtils.ts @@ -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, + }; +};