fix(ui)#9846: row count not populating on table details page (#9896)

* fixed rows not populating in table info
added skeleton loaders for rowCount and columnCount
styling and localization fixes

* added unit tests for checking rowCount and columnCount
This commit is contained in:
Aniket Katkar 2023-01-25 11:35:40 +05:30 committed by GitHub
parent 74d2132883
commit d776224bcf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 186 additions and 44 deletions

View File

@ -11,7 +11,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { Col, Row } from 'antd'; import { Col, Row, Skeleton, Space, Typography } from 'antd';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import classNames from 'classnames'; import classNames from 'classnames';
import { isEqual, isNil, isUndefined } from 'lodash'; import { isEqual, isNil, isUndefined } from 'lodash';
@ -31,6 +31,7 @@ import {
JoinedWith, JoinedWith,
Table, Table,
TableJoins, TableJoins,
TableProfile,
TypeUsedToReturnUsageDetailsOfAnEntity, TypeUsedToReturnUsageDetailsOfAnEntity,
} from '../../generated/entity/data/table'; } from '../../generated/entity/data/table';
import { ThreadType } from '../../generated/entity/feed/thread'; import { ThreadType } from '../../generated/entity/feed/thread';
@ -127,6 +128,7 @@ const DatasetDetails: React.FC<DatasetDetailsProps> = ({
handleExtensionUpdate, handleExtensionUpdate,
updateThreadHandler, updateThreadHandler,
entityFieldTaskCount, entityFieldTaskCount,
isTableProfileLoading,
}: DatasetDetailsProps) => { }: DatasetDetailsProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const history = useHistory(); const history = useHistory();
@ -338,37 +340,62 @@ const DatasetDetails: React.FC<DatasetDetailsProps> = ({
.sort((a, b) => b.joinCount - a.joinCount); .sort((a, b) => b.joinCount - a.joinCount);
}; };
const prepareTableRowInfo = () => { const prepareExtraInfoValues = (
const rowData = key: EntityInfo,
([ isTableProfileLoading?: boolean,
{ tableProfile?: TableProfile,
date: new Date(tableProfile?.timestamp || 0), numberOfColumns?: number
value: tableProfile?.rowCount ?? 0, ) => {
}, if (isTableProfileLoading) {
] as Array<{
date: Date;
value: number;
}>) ?? [];
if (!isUndefined(tableProfile)) {
return ( return (
<div className="tw-flex"> <Skeleton active paragraph={{ rows: 1, width: 50 }} title={false} />
{rowData.length > 1 && (
<TableProfilerGraph
className="tw--mt-4"
data={rowData}
height={38}
toolTipPos={{ x: 20, y: -30 }}
/>
)}
<span
className={classNames({
'tw--ml-6': rowData.length > 1,
})}>{`${tableProfile?.rowCount?.toLocaleString() || 0} rows`}</span>
</div>
); );
} else { }
return ''; switch (key) {
case EntityInfo.COLUMNS: {
const columnCount =
tableProfile && tableProfile?.columnCount
? tableProfile?.columnCount
: numberOfColumns
? numberOfColumns
: undefined;
return columnCount
? `${columns.length} ${t('label.column-plural')}`
: null;
}
case EntityInfo.ROWS: {
const rowData =
([
{
date: new Date(tableProfile?.timestamp || 0),
value: tableProfile?.rowCount ?? 0,
},
] as Array<{
date: Date;
value: number;
}>) ?? [];
return isUndefined(tableProfile) ? null : (
<Space align="center">
{rowData.length > 1 && (
<TableProfilerGraph
data={rowData}
height={32}
margin={{ top: 0, right: 0, left: 0, bottom: 0 }}
toolTipPos={{ x: 20, y: 30 }}
width={120}
/>
)}
<Typography.Paragraph className="m-0">{`${
tableProfile?.rowCount?.toLocaleString() || 0
} rows`}</Typography.Paragraph>
</Space>
);
}
default:
return null;
} }
}; };
@ -395,16 +422,21 @@ const DatasetDetails: React.FC<DatasetDetailsProps> = ({
{ value: `${weeklyUsageCount} ${t('label.query-plural')}` }, { value: `${weeklyUsageCount} ${t('label.query-plural')}` },
{ {
key: EntityInfo.COLUMNS, key: EntityInfo.COLUMNS,
value: localizationKey: 'column-plural',
tableProfile && tableProfile?.columnCount value: prepareExtraInfoValues(
? `${tableProfile.columnCount} ${t('label.columns-plural')}` EntityInfo.COLUMNS,
: columns.length isTableProfileLoading,
? `${columns.length} ${t('label.columns-plural')}` tableProfile,
: '', columns.length
),
}, },
{ {
key: EntityInfo.ROWS, key: EntityInfo.ROWS,
value: prepareTableRowInfo(), value: prepareExtraInfoValues(
EntityInfo.ROWS,
isTableProfileLoading,
tableProfile
),
}, },
]; ];

View File

@ -66,6 +66,7 @@ export interface DatasetDetailsProps {
slashedTableName: TitleBreadcrumbProps['titleLinks']; slashedTableName: TitleBreadcrumbProps['titleLinks'];
entityThread: Thread[]; entityThread: Thread[];
deleted?: boolean; deleted?: boolean;
isTableProfileLoading?: boolean;
isLineageLoading?: boolean; isLineageLoading?: boolean;
isSampleDataLoading?: boolean; isSampleDataLoading?: boolean;
isQueriesLoading?: boolean; isQueriesLoading?: boolean;

View File

@ -203,7 +203,11 @@ const EntitySummaryDetails = ({
? `${t(`label.${toLower(data.key)}`)} - ` ? `${t(`label.${toLower(data.key)}`)} - `
: null : null
: `${t('label.no-entity', { : `${t('label.no-entity', {
entity: t(`label.${toLower(data.key)}`), entity: t(
`label.${toLower(
data.localizationKey ? data.localizationKey : data.key
)}`
),
})}` })}`
: null} : null}
</> </>

View File

@ -175,6 +175,7 @@ declare module 'Models' {
key?: string; key?: string;
value: string | number | React.ReactNode; value: string | number | React.ReactNode;
id?: string; id?: string;
localizationKey?: string;
isLink?: boolean; isLink?: boolean;
placeholderText?: string; placeholderText?: string;
openInNewTab?: boolean; openInNewTab?: boolean;

View File

@ -84,7 +84,7 @@
"collapse-all": "Collapse All", "collapse-all": "Collapse All",
"column": "Column", "column": "Column",
"column-entity": "Column {{entity}}", "column-entity": "Column {{entity}}",
"columns-plural": "Columns", "column-plural": "Columns",
"comment-lowercase": "comment", "comment-lowercase": "comment",
"completed": "Completed", "completed": "Completed",
"completed-entity": "Completed {{entity}}", "completed-entity": "Completed {{entity}}",

View File

@ -33,12 +33,14 @@ import { isEmpty, isUndefined } from 'lodash';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { EntityTags } from 'Models'; import { EntityTags } from 'Models';
import React, { FunctionComponent, useEffect, useState } from 'react'; import React, { FunctionComponent, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory, useParams } from 'react-router-dom'; import { useHistory, useParams } from 'react-router-dom';
import { getAllFeeds, postFeedById, postThread } from 'rest/feedsAPI'; import { getAllFeeds, postFeedById, postThread } from 'rest/feedsAPI';
import { getLineageByFQN } from 'rest/lineageAPI'; import { getLineageByFQN } from 'rest/lineageAPI';
import { addLineage, deleteLineageEdge } from 'rest/miscAPI'; import { addLineage, deleteLineageEdge } from 'rest/miscAPI';
import { import {
addFollower, addFollower,
getLatestTableProfileByFqn,
getTableDetailsByFQN, getTableDetailsByFQN,
patchTableDetails, patchTableDetails,
removeFollower, removeFollower,
@ -95,6 +97,7 @@ import { showErrorToast } from '../../utils/ToastUtils';
const DatasetDetailsPage: FunctionComponent = () => { const DatasetDetailsPage: FunctionComponent = () => {
const history = useHistory(); const history = useHistory();
const { t } = useTranslation();
const { getEntityPermissionByFqn } = usePermissionProvider(); const { getEntityPermissionByFqn } = usePermissionProvider();
const [isLoading, setIsLoading] = useState<boolean>(true); const [isLoading, setIsLoading] = useState<boolean>(true);
const [isLineageLoading, setIsLineageLoading] = useState<boolean>(false); const [isLineageLoading, setIsLineageLoading] = useState<boolean>(false);
@ -104,6 +107,8 @@ const DatasetDetailsPage: FunctionComponent = () => {
useState<boolean>(false); useState<boolean>(false);
const [isentityThreadLoading, setIsentityThreadLoading] = const [isentityThreadLoading, setIsentityThreadLoading] =
useState<boolean>(false); useState<boolean>(false);
const [isTableProfileLoading, setIsTableProfileLoading] =
useState<boolean>(false);
const USERId = getCurrentUserId(); const USERId = getCurrentUserId();
const [tableId, setTableId] = useState(''); const [tableId, setTableId] = useState('');
const [tier, setTier] = useState<TagLabel>(); const [tier, setTier] = useState<TagLabel>();
@ -289,7 +294,6 @@ const DatasetDetailsPage: FunctionComponent = () => {
joins, joins,
tags, tags,
sampleData, sampleData,
profile,
tableType, tableType,
version, version,
service, service,
@ -354,7 +358,6 @@ const DatasetDetailsPage: FunctionComponent = () => {
setDescription(description ?? ''); setDescription(description ?? '');
setColumns(columns || []); setColumns(columns || []);
setSampleData(sampleData as TableData); setSampleData(sampleData as TableData);
setTableProfile(profile);
setTableTags(getTagsWithoutTier(tags || [])); setTableTags(getTagsWithoutTier(tags || []));
setUsageSummary( setUsageSummary(
usageSummary as TypeUsedToReturnUsageDetailsOfAnEntity usageSummary as TypeUsedToReturnUsageDetailsOfAnEntity
@ -382,6 +385,29 @@ const DatasetDetailsPage: FunctionComponent = () => {
}); });
}; };
const fetchTableProfileDetails = async () => {
if (!isEmpty(tableDetails)) {
setIsTableProfileLoading(true);
try {
const { profile } = await getLatestTableProfileByFqn(
tableDetails.fullyQualifiedName ?? ''
);
setTableProfile(profile);
} catch (err) {
showErrorToast(
err as AxiosError,
t('server.entity-details-fetch-error', {
entityType: t('label.table'),
entityName: tableDetails.displayName ?? tableDetails.name,
})
);
} finally {
setIsTableProfileLoading(false);
}
}
};
const fetchTabSpecificData = (tabField = '') => { const fetchTabSpecificData = (tabField = '') => {
switch (tabField) { switch (tabField) {
case TabSpecificField.SAMPLE_DATA: { case TabSpecificField.SAMPLE_DATA: {
@ -798,6 +824,10 @@ const DatasetDetailsPage: FunctionComponent = () => {
} }
}, [tablePermissions]); }, [tablePermissions]);
useEffect(() => {
fetchTableProfileDetails();
}, [tableDetails]);
useEffect(() => { useEffect(() => {
fetchResourcePermission(tableFQN); fetchResourcePermission(tableFQN);
}, [tableFQN]); }, [tableFQN]);
@ -851,6 +881,7 @@ const DatasetDetailsPage: FunctionComponent = () => {
isNodeLoading={isNodeLoading} isNodeLoading={isNodeLoading}
isQueriesLoading={isTableQueriesLoading} isQueriesLoading={isTableQueriesLoading}
isSampleDataLoading={isSampleDataLoading} isSampleDataLoading={isSampleDataLoading}
isTableProfileLoading={isTableProfileLoading}
isentityThreadLoading={isentityThreadLoading} isentityThreadLoading={isentityThreadLoading}
joins={joins} joins={joins}
lineageLeafNodes={leafNodes} lineageLeafNodes={leafNodes}

View File

@ -17,6 +17,7 @@ import {
findByText, findByText,
fireEvent, fireEvent,
render, render,
screen,
} from '@testing-library/react'; } from '@testing-library/react';
import React from 'react'; import React from 'react';
import { MemoryRouter } from 'react-router'; import { MemoryRouter } from 'react-router';
@ -30,6 +31,7 @@ import { getLineageByFQN } from 'rest/lineageAPI';
import { addLineage, deleteLineageEdge } from 'rest/miscAPI'; import { addLineage, deleteLineageEdge } from 'rest/miscAPI';
import { import {
addFollower, addFollower,
getLatestTableProfileByFqn,
getTableDetailsByFQN, getTableDetailsByFQN,
patchTableDetails, patchTableDetails,
removeFollower, removeFollower,
@ -40,10 +42,13 @@ import {
createPostRes, createPostRes,
mockFollowRes, mockFollowRes,
mockLineageRes, mockLineageRes,
mockTableProfileResponse,
mockUnfollowRes, mockUnfollowRes,
updateTagRes, updateTagRes,
} from './datasetDetailsPage.mock'; } from './datasetDetailsPage.mock';
const mockShowErrorToast = jest.fn();
const mockUseParams = { const mockUseParams = {
datasetFQN: 'bigquery_gcp:shopify:dim_address', datasetFQN: 'bigquery_gcp:shopify:dim_address',
tab: 'schema', tab: 'schema',
@ -68,8 +73,7 @@ jest.mock('../../AppState', () => ({
jest.mock('components/PermissionProvider/PermissionProvider', () => ({ jest.mock('components/PermissionProvider/PermissionProvider', () => ({
usePermissionProvider: jest.fn().mockImplementation(() => ({ usePermissionProvider: jest.fn().mockImplementation(() => ({
permissions: {}, getEntityPermissionByFqn: jest.fn().mockResolvedValue({
getEntityPermission: jest.fn().mockResolvedValue({
Create: true, Create: true,
Delete: true, Delete: true,
EditAll: true, EditAll: true,
@ -146,6 +150,7 @@ jest.mock('components/DatasetDetails/DatasetDetails.component', () => {
handleRemoveColumnTest, handleRemoveColumnTest,
deletePostHandler, deletePostHandler,
entityLineageHandler, entityLineageHandler,
tableProfile,
}) => ( }) => (
<div data-testid="datasetdetails-component"> <div data-testid="datasetdetails-component">
<button data-testid="version-button" onClick={versionHandler}> <button data-testid="version-button" onClick={versionHandler}>
@ -231,6 +236,12 @@ jest.mock('components/DatasetDetails/DatasetDetails.component', () => {
onClick={entityLineageHandler}> onClick={entityLineageHandler}>
entityLineageHandler entityLineageHandler
</button> </button>
{tableProfile && (
<>
<div data-testid="rowCount">{tableProfile.rowCount}</div>
<div data-testid="columnCount">{tableProfile.columnCount}</div>
</>
)}
</div> </div>
) )
); );
@ -265,6 +276,19 @@ jest.mock('rest/tableAPI', () => ({
removeFollower: jest removeFollower: jest
.fn() .fn()
.mockImplementation(() => Promise.resolve(mockUnfollowRes)), .mockImplementation(() => Promise.resolve(mockUnfollowRes)),
getLatestTableProfileByFqn: jest
.fn()
.mockImplementation(() => Promise.resolve(mockTableProfileResponse)),
}));
jest.mock('react-i18next', () => ({
useTranslation: jest.fn().mockImplementation(() => ({
t: jest.fn().mockImplementation((str) => str),
})),
}));
jest.mock('../../utils/ToastUtils', () => ({
showErrorToast: jest.fn().mockImplementation(() => mockShowErrorToast()),
})); }));
jest.mock('../../utils/FeedUtils', () => ({ jest.mock('../../utils/FeedUtils', () => ({
@ -1159,5 +1183,45 @@ describe('Test DatasetDetails page', () => {
fireEvent.click(deletePostHandler); fireEvent.click(deletePostHandler);
}); });
it('Table profile details should be passed correctly after successful API response', async () => {
await act(async () => {
render(<DatasetDetailsPage />, {
wrapper: MemoryRouter,
});
});
const rowCount = screen.getByTestId('rowCount');
const columnCount = screen.getByTestId('columnCount');
expect(rowCount).toBeInTheDocument();
expect(columnCount).toBeInTheDocument();
expect(rowCount).toContainHTML(
`${mockTableProfileResponse.profile.rowCount}`
);
expect(columnCount).toContainHTML(
`${mockTableProfileResponse.profile.columnCount}`
);
});
it('An error should be thrown if table profile API throws error', async () => {
(getLatestTableProfileByFqn as jest.Mock).mockImplementationOnce(() =>
Promise.reject()
);
await act(async () => {
render(<DatasetDetailsPage />, {
wrapper: MemoryRouter,
});
});
const rowCount = screen.queryByTestId('rowCount');
const columnCount = screen.queryByTestId('columnCount');
expect(rowCount).toBeNull();
expect(columnCount).toBeNull();
expect(mockShowErrorToast).toHaveBeenCalledTimes(1);
});
}); });
}); });

View File

@ -489,3 +489,12 @@ export const mockLineageRes = {
upstreamEdges: [], upstreamEdges: [],
downstreamEdges: [], downstreamEdges: [],
}; };
export const mockTableProfileResponse = {
profile: {
timestamp: 1674466560,
profileSampleType: 'PERCENTAGE',
columnCount: 12,
rowCount: 14567,
},
};

View File

@ -172,7 +172,7 @@ export const getEntityOverview = (
isLink: false, isLink: false,
}, },
{ {
name: i18next.t('label.columns-plural'), name: i18next.t('label.column-plural'),
value: columns ? columns.length : '--', value: columns ? columns.length : '--',
isLink: false, isLink: false,
}, },