Skip to content

Commit 31cc75b

Browse files
authored
fix(ui): Column Profile shows 0% for zero unique, null, and distinct proportions (#27394)
* fix(ui): show 0% for zero column profile proportions (#27302) Replace truthy checks with isNil() for nullProportion, uniqueProportion, and distinctProportion so 0 renders as 0% instead of --. Add unit tests for proportion column renderers. Made-with: Cursor * fix lint error
1 parent 50c1750 commit 31cc75b

2 files changed

Lines changed: 121 additions & 21 deletions

File tree

openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TableProfiler/ColumnProfileTable/ColumnProfileTable.test.tsx

Lines changed: 115 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,31 @@
1111
* limitations under the License.
1212
*/
1313
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
14+
import { ColumnsType } from 'antd/lib/table';
1415
import { act } from 'react';
1516
import { MemoryRouter } from 'react-router-dom';
17+
import { ColumnProfile } from '../../../../../generated/entity/data/table';
1618
import { MOCK_TABLE } from '../../../../../mocks/TableData.mock';
1719
import { useTableProfiler } from '../TableProfilerProvider';
1820
import ColumnProfileTable from './ColumnProfileTable';
1921

22+
let capturedColumns: ColumnsType<{ profile?: ColumnProfile }> = [];
23+
2024
jest.mock('../../../../common/Table/Table', () =>
21-
jest.fn().mockImplementation(({ searchProps }) => (
22-
<div>
23-
<input
24-
data-testid="searchbar"
25-
value={searchProps?.value ?? ''}
26-
onChange={(e) => searchProps?.onSearch?.(e.target.value)}
27-
/>
28-
<div>Table</div>
29-
</div>
30-
))
25+
jest.fn().mockImplementation(({ columns, searchProps }) => {
26+
capturedColumns = columns ?? [];
27+
28+
return (
29+
<div>
30+
<input
31+
data-testid="searchbar"
32+
value={searchProps?.value ?? ''}
33+
onChange={(e) => searchProps?.onSearch?.(e.target.value)}
34+
/>
35+
<div>Table</div>
36+
</div>
37+
);
38+
})
3139
);
3240

3341
jest.mock('../../../../common/SummaryCard/SummaryCardV1', () =>
@@ -59,7 +67,25 @@ jest.mock(
5967
jest.mock('../../../../../utils/CommonUtils', () => ({
6068
formatNumberWithComma: jest.fn(),
6169
getTableFQNFromColumnFQN: jest.fn().mockImplementation((fqn) => fqn),
62-
calculatePercentage: jest.fn().mockReturnValue('50%'),
70+
calculatePercentage: jest
71+
.fn()
72+
.mockImplementation(
73+
(
74+
numerator: number,
75+
denominator: number,
76+
precision: number,
77+
format: boolean
78+
) => {
79+
if (denominator === 0) {
80+
return format ? '0%' : 0;
81+
}
82+
const value = parseFloat(
83+
((numerator / denominator) * 100).toFixed(precision)
84+
);
85+
86+
return format ? `${value}%` : value;
87+
}
88+
),
6389
}));
6490

6591
jest.mock('../../../../../utils/TableUtils', () => ({
@@ -268,3 +294,81 @@ describe('Test ColumnProfileTable component', () => {
268294
});
269295
});
270296
});
297+
298+
describe('ColumnProfileTable proportion column renders', () => {
299+
const proportionColumnKeys = [
300+
'nullProportion',
301+
'uniqueProportion',
302+
'distinctProportion',
303+
] as const;
304+
305+
beforeEach(async () => {
306+
cleanup();
307+
await act(async () => {
308+
render(<ColumnProfileTable />, { wrapper: MemoryRouter });
309+
});
310+
});
311+
312+
it.each(proportionColumnKeys)(
313+
'should show "0%" instead of "--" when %s is 0',
314+
(field) => {
315+
const col = capturedColumns.find((c) => c.key === field);
316+
const renderFn = col?.render as (
317+
profile: ColumnProfile | undefined
318+
) => string;
319+
320+
expect(renderFn({ [field]: 0 } as unknown as ColumnProfile)).toBe('0%');
321+
}
322+
);
323+
324+
it.each(proportionColumnKeys)('should show "--" when %s is null', (field) => {
325+
const col = capturedColumns.find((c) => c.key === field);
326+
const renderFn = col?.render as (
327+
profile: ColumnProfile | undefined
328+
) => string;
329+
330+
expect(renderFn({ [field]: null } as unknown as ColumnProfile)).toBe('--');
331+
});
332+
333+
it.each(proportionColumnKeys)(
334+
'should show "--" when %s is undefined',
335+
(field) => {
336+
const col = capturedColumns.find((c) => c.key === field);
337+
const renderFn = col?.render as (
338+
profile: ColumnProfile | undefined
339+
) => string;
340+
341+
expect(renderFn({} as ColumnProfile)).toBe('--');
342+
expect(renderFn(undefined)).toBe('--');
343+
}
344+
);
345+
346+
it.each(proportionColumnKeys)(
347+
'should show correct percentage for a normal value when %s is 0.5',
348+
(field) => {
349+
const col = capturedColumns.find((c) => c.key === field);
350+
const renderFn = col?.render as (
351+
profile: ColumnProfile | undefined
352+
) => string;
353+
354+
expect(renderFn({ [field]: 0.5 } as unknown as ColumnProfile)).toBe(
355+
'50%'
356+
);
357+
}
358+
);
359+
360+
it.each(proportionColumnKeys)(
361+
'should not round small values (%s = 0.001) to 0%',
362+
(field) => {
363+
const col = capturedColumns.find((c) => c.key === field);
364+
const renderFn = col?.render as (
365+
profile: ColumnProfile | undefined
366+
) => string;
367+
368+
// 0.001 * 100 = 0.1 → rounds to 0.1%, not 0%
369+
expect(renderFn({ [field]: 0.001 } as unknown as ColumnProfile)).toBe(
370+
'0.1%'
371+
);
372+
}
373+
);
374+
});

openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TableProfiler/ColumnProfileTable/ColumnProfileTable.tsx

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
import { Typography } from '@openmetadata/ui-core-components';
1515
import { ColumnsType } from 'antd/lib/table';
16-
import { isEmpty, isUndefined } from 'lodash';
16+
import { isEmpty, isNil, isUndefined } from 'lodash';
1717
import Qs from 'qs';
1818
import { useCallback, useEffect, useMemo, useState } from 'react';
1919
import { useTranslation } from 'react-i18next';
@@ -164,12 +164,10 @@ const ColumnProfileTable = () => {
164164
dataIndex: 'profile',
165165
key: 'nullProportion',
166166
width: 200,
167-
render: (profile: ColumnProfile) => {
168-
return profile?.nullProportion !== undefined &&
169-
profile?.nullProportion !== null
167+
render: (profile: ColumnProfile) =>
168+
!isNil(profile?.nullProportion)
170169
? calculatePercentage(profile.nullProportion, 1, 2, true)
171-
: '--';
172-
},
170+
: '--',
173171
sorter: (col1, col2) =>
174172
(col1.profile?.nullProportion || 0) -
175173
(col2.profile?.nullProportion || 0),
@@ -180,8 +178,7 @@ const ColumnProfileTable = () => {
180178
key: 'uniqueProportion',
181179
width: 200,
182180
render: (profile: ColumnProfile) =>
183-
profile?.uniqueProportion !== undefined &&
184-
profile?.uniqueProportion !== null
181+
!isNil(profile?.uniqueProportion)
185182
? calculatePercentage(profile.uniqueProportion, 1, 2, true)
186183
: '--',
187184
sorter: (col1, col2) =>
@@ -194,8 +191,7 @@ const ColumnProfileTable = () => {
194191
key: 'distinctProportion',
195192
width: 200,
196193
render: (profile: ColumnProfile) =>
197-
profile?.distinctProportion !== undefined &&
198-
profile?.distinctProportion !== null
194+
!isNil(profile?.distinctProportion)
199195
? calculatePercentage(profile.distinctProportion, 1, 2, true)
200196
: '--',
201197
sorter: (col1, col2) =>

0 commit comments

Comments
 (0)