From 3ba2fd85f1cf80044abc3b70262d24a3a7fae6af Mon Sep 17 00:00:00 2001 From: Shailesh Parmar Date: Fri, 20 Sep 2024 18:47:18 +0530 Subject: [PATCH] Minor: added seconds to human-readable format scale for test case graph (#17926) * Minor: added milliseconds to human-readable format scale for test case graph * addressing comment * fixed unit test * addressing comment --- .../Profiler/TestSummary/TestSummary.tsx | 1 + .../TestSummary/TestSummaryGraph.interface.ts | 1 + .../Profiler/TestSummary/TestSummaryGraph.tsx | 24 +++++++++-- .../TestSummaryCustomTooltip.component.tsx | 7 +++- .../TestSummaryCustomTooltip.test.tsx | 3 ++ .../ui/src/constants/TestSuite.constant.ts | 2 + .../ui/src/utils/CommonUtils.test.ts | 24 +++++++++++ .../resources/ui/src/utils/CommonUtils.tsx | 40 +++++++++++++++++++ 8 files changed, 98 insertions(+), 4 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TestSummary/TestSummary.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TestSummary/TestSummary.tsx index e8bd02881a6..88b08bb32df 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TestSummary/TestSummary.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TestSummary/TestSummary.tsx @@ -91,6 +91,7 @@ const TestSummary: React.FC = ({ data }) => { testCaseName={data.name} testCaseParameterValue={data.parameterValues} testCaseResults={results} + testDefinitionName={data.testDefinition.name} /> ); }, [isGraphLoading, data, results, selectedTimeRange]); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TestSummary/TestSummaryGraph.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TestSummary/TestSummaryGraph.interface.ts index 34fc1305405..796d06d2646 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TestSummary/TestSummaryGraph.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TestSummary/TestSummaryGraph.interface.ts @@ -22,4 +22,5 @@ export interface TestSummaryGraphProps { testCaseResults: TestCaseResult[]; selectedTimeRange: string; minHeight?: number; + testDefinitionName?: string; } 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 0a1601b648b..316188781c2 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 @@ -45,6 +45,10 @@ import { GRAPH_BACKGROUND_COLOR, HOVER_CHART_OPACITY, } from '../../../../constants/constants'; +import { + TABLE_DATA_TO_BE_FRESH, + TABLE_FRESHNESS_KEY, +} from '../../../../constants/TestSuite.constant'; import { ERROR_PLACEHOLDER_TYPE } from '../../../../enums/common.enum'; import { Thread, @@ -56,6 +60,7 @@ import { axisTickFormatter, updateActiveChartFilter, } from '../../../../utils/ChartUtils'; +import { formatTimeFromSeconds } from '../../../../utils/CommonUtils'; import { prepareChartData } from '../../../../utils/DataQuality/TestSummaryGraphUtils'; import { formatDateTime } from '../../../../utils/date-time/DateTimeUtils'; import { useActivityFeedProvider } from '../../../ActivityFeed/ActivityFeedProvider/ActivityFeedProvider'; @@ -70,6 +75,7 @@ function TestSummaryGraph({ testCaseResults, selectedTimeRange, minHeight, + testDefinitionName, }: Readonly) { const { t } = useTranslation(); const { entityThread = [] } = useActivityFeedProvider(); @@ -92,15 +98,18 @@ function TestSummaryGraph({ : -200; }, [chartRef, chartMouseEvent]); - const chartData = useMemo(() => { + const { chartData, isFreshnessTest } = useMemo(() => { const data = prepareChartData({ testCaseParameterValue: testCaseParameterValue ?? [], testCaseResults, entityThread, }); setShowAILearningBanner(data.showAILearningBanner); + const isFreshnessTest = data.information.some( + (value) => value.label === TABLE_FRESHNESS_KEY + ); - return data; + return { chartData: data, isFreshnessTest }; }, [testCaseResults, entityThread, testCaseParameterValue]); const incidentData = useMemo(() => { @@ -164,6 +173,14 @@ function TestSummaryGraph({ setActiveMouseHoverKey(''); }; + // Todo: need to find better approach to create dynamic scale for graph, need to work with @TeddyCr for the same! + const formatYAxis = (value: number) => { + // table freshness will always have output value in seconds + return testDefinitionName === TABLE_DATA_TO_BE_FRESH || isFreshnessTest + ? formatTimeFromSeconds(value) + : axisTickFormatter(value); + }; + const updatedDot: LineProps['dot'] = (props): ReactElement => { const { cx = 0, cy = 0, payload } = props; let fill = payload.status === TestCaseStatus.Success ? GREEN_3 : undefined; @@ -253,7 +270,8 @@ function TestSummaryGraph({ allowDataOverflow domain={['min', 'max']} padding={{ top: 8, bottom: 8 }} - tickFormatter={(value) => axisTickFormatter(value)} + tickFormatter={formatYAxis} + width={80} /> } 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 5eea21d4411..db4a26d897e 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 @@ -16,7 +16,9 @@ import React, { Fragment } from 'react'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; import { TooltipProps } from 'recharts'; +import { TABLE_FRESHNESS_KEY } from '../../../../constants/TestSuite.constant'; import { Thread } from '../../../../generated/entity/feed/thread'; +import { formatTimeFromSeconds } from '../../../../utils/CommonUtils'; import { formatDateTime } from '../../../../utils/date-time/DateTimeUtils'; import { getTaskDetailPath } from '../../../../utils/TasksUtils'; import { OwnerLabel } from '../../../common/OwnerLabel/OwnerLabel.component'; @@ -78,7 +80,10 @@ const TestSummaryCustomTooltip = ( {startCase(key)} - {value} + {/* freshness will always be in seconds */} + {key === TABLE_FRESHNESS_KEY && isNumber(value) + ? formatTimeFromSeconds(value) + : value} ); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TestSummaryCustomTooltip/TestSummaryCustomTooltip.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TestSummaryCustomTooltip/TestSummaryCustomTooltip.test.tsx index fa78c04d81d..68f4fe010ae 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TestSummaryCustomTooltip/TestSummaryCustomTooltip.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TestSummaryCustomTooltip/TestSummaryCustomTooltip.test.tsx @@ -49,6 +49,9 @@ jest.mock('../../../../utils/TasksUtils', () => ({ jest.mock('../../../common/OwnerLabel/OwnerLabel.component', () => ({ OwnerLabel: jest.fn().mockReturnValue(
OwnerLabel
), })); +jest.mock('../../../../utils/CommonUtils', () => ({ + formatTimeFromSeconds: jest.fn().mockReturnValue('1 hour'), +})); describe('Test AddServicePage component', () => { it('AddServicePage component should render', async () => { diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/TestSuite.constant.ts b/openmetadata-ui/src/main/resources/ui/src/constants/TestSuite.constant.ts index 3068507648e..e7877297eab 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/TestSuite.constant.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/TestSuite.constant.ts @@ -101,6 +101,8 @@ export const TEST_CASE_STATUS: Record< }; export const TABLE_DIFF = 'tableDiff'; +export const TABLE_DATA_TO_BE_FRESH = 'tableDataToBeFresh'; +export const TABLE_FRESHNESS_KEY = 'freshness'; export const SUPPORTED_SERVICES_FOR_TABLE_DIFF = [ DatabaseServiceType.Snowflake, 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 d15eabe5d68..cf464c5ec83 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 @@ -27,6 +27,7 @@ import { } from '../generated/type/tagLabel'; import { digitFormatter, + formatTimeFromSeconds, getBase64EncodedString, getIngestionFrequency, getIsErrorMatch, @@ -137,6 +138,29 @@ describe('Tests for CommonUtils', () => { }); }); + // formatTimeFromSeconds test + it('formatTimeFromSeconds formatter should format mills to human readable value', () => { + const values = [ + { input: 1, expected: '1 second' }, + { input: 2, expected: '2 seconds' }, + { input: 30, expected: '30 seconds' }, + { input: 60, expected: '1 minute' }, + { input: 120, expected: '2 minutes' }, + { input: 3600, expected: '1 hour' }, + { input: 7200, expected: '2 hours' }, + { input: 86400, expected: '1 day' }, + { input: 172800, expected: '2 days' }, + { input: 2592000, expected: '1 month' }, + { input: 5184000, expected: '2 months' }, + { input: 31536000, expected: '1 year' }, + { input: 63072000, expected: '2 years' }, + ]; + + values.map(({ input, expected }) => { + expect(formatTimeFromSeconds(input)).toEqual(expected); + }); + }); + describe('Tests for sortTagsCaseInsensitive function', () => { it('GetErrorMessage match function should return true if match found', () => { const result = getIsErrorMatch( 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 c14863fab9c..413e4ce3e47 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CommonUtils.tsx @@ -26,6 +26,7 @@ import { toLower, toNumber, } from 'lodash'; +import { Duration } from 'luxon'; import { CurrentState, ExtraInfo, @@ -604,6 +605,45 @@ export const digitFormatter = (value: number) => { }).format(value); }; +/** + * Converts a duration in seconds to a human-readable format. + * The function returns the largest time unit (years, months, days, hours, minutes, or seconds) + * that is greater than or equal to one, rounded to the nearest whole number. + * + * @param {number} seconds - The duration in seconds to be converted. + * @returns {string} A string representing the duration in a human-readable format, + * e.g., "1 hour", "2 days", "3 months", etc. + * + * @example + * formatTimeFromSeconds(1); // returns "1 second" + * formatTimeFromSeconds(60); // returns "1 minute" + * formatTimeFromSeconds(3600); // returns "1 hour" + * formatTimeFromSeconds(86400); // returns "1 day" + */ +export const formatTimeFromSeconds = (seconds: number): string => { + const duration = Duration.fromObject({ seconds }); + let unit: keyof Duration; + + if (duration.as('years') >= 1) { + unit = 'years'; + } else if (duration.as('months') >= 1) { + unit = 'months'; + } else if (duration.as('days') >= 1) { + unit = 'days'; + } else if (duration.as('hours') >= 1) { + unit = 'hours'; + } else if (duration.as('minutes') >= 1) { + unit = 'minutes'; + } else { + unit = 'seconds'; + } + + const value = Math.round(duration.as(unit)); + const unitSingular = unit.slice(0, -1); + + return `${value} ${value === 1 ? unitSingular : unit}`; +}; + export const getTeamsUser = ( data: ExtraInfo, currentUser: User