mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-12-14 00:57:09 +00:00
Co-authored-by: Teddy <teddy.crepineau@gmail.com>
This commit is contained in:
parent
7db1612c13
commit
016f585e14
@ -29,7 +29,7 @@ import { DateRangeObject } from 'Models';
|
||||
import Qs from 'qs';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { Link, useHistory, useLocation } from 'react-router-dom';
|
||||
import { ReactComponent as DropDownIcon } from '../../../../../assets/svg/drop-down.svg';
|
||||
import { ReactComponent as SettingIcon } from '../../../../../assets/svg/ic-settings-primery.svg';
|
||||
import { PAGE_SIZE_LARGE } from '../../../../../constants/constants';
|
||||
@ -50,6 +50,7 @@ import {
|
||||
getEntityName,
|
||||
searchInColumns,
|
||||
} from '../../../../../utils/EntityUtils';
|
||||
import { getEntityColumnFQN } from '../../../../../utils/FeedUtils';
|
||||
import {
|
||||
getAddCustomMetricPath,
|
||||
getAddDataQualityTableTestPath,
|
||||
@ -65,6 +66,7 @@ import { SummaryCard } from '../../../../common/SummaryCard/SummaryCard.componen
|
||||
import { SummaryCardProps } from '../../../../common/SummaryCard/SummaryCard.interface';
|
||||
import Table from '../../../../common/Table/Table';
|
||||
import TabsLabel from '../../../../common/TabsLabel/TabsLabel.component';
|
||||
import TestCaseStatusSummaryIndicator from '../../../../common/TestCaseStatusSummaryIndicator/TestCaseStatusSummaryIndicator.component';
|
||||
import PageHeader from '../../../../PageHeader/PageHeader.component';
|
||||
import { TableProfilerTab } from '../../ProfilerDashboard/profilerDashboard.interface';
|
||||
import ColumnPickerMenu from '../ColumnPickerMenu';
|
||||
@ -91,7 +93,12 @@ const ColumnProfileTable = () => {
|
||||
tableProfiler,
|
||||
dateRangeObject,
|
||||
onDateRangeChange,
|
||||
table,
|
||||
} = useTableProfiler();
|
||||
const testCaseCounts = useMemo(
|
||||
() => table?.testSuite?.summary?.columnTestSummary ?? [],
|
||||
[table]
|
||||
);
|
||||
const isLoading = isTestsLoading || isProfilerDataLoading;
|
||||
const columns = tableProfiler?.columns ?? [];
|
||||
const [searchText, setSearchText] = useState<string>('');
|
||||
@ -211,8 +218,50 @@ const ColumnProfileTable = () => {
|
||||
sorter: (col1, col2) =>
|
||||
(col1.profile?.valuesCount || 0) - (col2.profile?.valuesCount || 0),
|
||||
},
|
||||
{
|
||||
title: t('label.test-plural'),
|
||||
dataIndex: 'testCount',
|
||||
key: 'Tests',
|
||||
render: (_, record) => {
|
||||
const testCounts = testCaseCounts.find((column) => {
|
||||
return isEqual(
|
||||
getEntityColumnFQN(column.entityLink ?? ''),
|
||||
record.fullyQualifiedName
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<Link
|
||||
data-testid={`${record.name}-test-count`}
|
||||
to={{
|
||||
search: Qs.stringify({
|
||||
activeTab: TableProfilerTab.DATA_QUALITY,
|
||||
}),
|
||||
}}>
|
||||
{testCounts?.total ?? 0}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('label.status'),
|
||||
dataIndex: 'dataQualityTest',
|
||||
key: 'dataQualityTest',
|
||||
render: (_, record) => {
|
||||
const testCounts = testCaseCounts.find((column) => {
|
||||
return isEqual(
|
||||
getEntityColumnFQN(column.entityLink ?? ''),
|
||||
record.fullyQualifiedName
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<TestCaseStatusSummaryIndicator testCaseStatusCounts={testCounts} />
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
}, [columns]);
|
||||
}, [columns, testCaseCounts]);
|
||||
|
||||
const selectedColumn = useMemo(() => {
|
||||
return find(
|
||||
|
||||
@ -12,9 +12,8 @@
|
||||
*/
|
||||
import { DownOutlined } from '@ant-design/icons';
|
||||
import { Button, Col, Dropdown, Form, Row, Select, Space, Tabs } from 'antd';
|
||||
import { AxiosError } from 'axios';
|
||||
import { isEmpty, isUndefined } from 'lodash';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { isEmpty } from 'lodash';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { getEntityDetailsPath } from '../../../../../constants/constants';
|
||||
@ -24,23 +23,16 @@ import {
|
||||
TEST_CASE_TYPE_OPTION,
|
||||
} from '../../../../../constants/profiler.constant';
|
||||
import { INITIAL_TEST_SUMMARY } from '../../../../../constants/TestSuite.constant';
|
||||
import {
|
||||
EntityTabs,
|
||||
EntityType,
|
||||
TabSpecificField,
|
||||
} from '../../../../../enums/entity.enum';
|
||||
import { EntityTabs, EntityType } from '../../../../../enums/entity.enum';
|
||||
import { ProfilerDashboardType } from '../../../../../enums/table.enum';
|
||||
import { Table } from '../../../../../generated/entity/data/table';
|
||||
import { TestCaseStatus } from '../../../../../generated/tests/testCase';
|
||||
import { useFqn } from '../../../../../hooks/useFqn';
|
||||
import { getTableDetailsByFQN } from '../../../../../rest/tableAPI';
|
||||
import { TestCaseType } from '../../../../../rest/testAPI';
|
||||
import {
|
||||
getBreadcrumbForTable,
|
||||
getEntityName,
|
||||
} from '../../../../../utils/EntityUtils';
|
||||
import { getAddDataQualityTableTestPath } from '../../../../../utils/RouterUtils';
|
||||
import { showErrorToast } from '../../../../../utils/ToastUtils';
|
||||
import NextPrevious from '../../../../common/NextPrevious/NextPrevious';
|
||||
import { NextPreviousProps } from '../../../../common/NextPrevious/NextPrevious.interface';
|
||||
import TabsLabel from '../../../../common/TabsLabel/TabsLabel.component';
|
||||
@ -60,6 +52,7 @@ export const QualityTab = () => {
|
||||
isTestsLoading,
|
||||
isTableDeleted,
|
||||
testCasePaging,
|
||||
table,
|
||||
} = useTableProfiler();
|
||||
|
||||
const {
|
||||
@ -79,8 +72,6 @@ export const QualityTab = () => {
|
||||
const [selectedTestCaseStatus, setSelectedTestCaseStatus] =
|
||||
useState<TestCaseStatus>('' as TestCaseStatus);
|
||||
const [selectedTestType, setSelectedTestType] = useState(TestCaseType.all);
|
||||
const [table, setTable] = useState<Table>();
|
||||
const [isTestSuiteLoading, setIsTestSuiteLoading] = useState(true);
|
||||
const testSuite = useMemo(() => table?.testSuite, [table]);
|
||||
|
||||
const handleTestCasePageChange: NextPreviousProps['pagingHandler'] = ({
|
||||
@ -207,28 +198,6 @@ export const QualityTab = () => {
|
||||
[]
|
||||
);
|
||||
|
||||
const fetchTestSuiteDetails = async () => {
|
||||
setIsTestSuiteLoading(true);
|
||||
try {
|
||||
const details = await getTableDetailsByFQN(datasetFQN, {
|
||||
fields: TabSpecificField.TESTSUITE,
|
||||
});
|
||||
setTable(details);
|
||||
} catch (error) {
|
||||
showErrorToast(error as AxiosError);
|
||||
} finally {
|
||||
setIsTestSuiteLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isUndefined(testSuite)) {
|
||||
fetchTestSuiteDetails();
|
||||
} else {
|
||||
setIsTestSuiteLoading(false);
|
||||
}
|
||||
}, [testSuite]);
|
||||
|
||||
return (
|
||||
<Row gutter={[0, 16]}>
|
||||
<Col span={24}>
|
||||
@ -280,7 +249,6 @@ export const QualityTab = () => {
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<SummaryPanel
|
||||
isLoading={isTestSuiteLoading}
|
||||
testSummary={testSuite?.summary ?? INITIAL_TEST_SUMMARY}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
@ -29,8 +29,8 @@ import { UsePagingInterface } from '../../../../hooks/paging/usePaging';
|
||||
import { ListTestCaseParams } from '../../../../rest/testAPI';
|
||||
|
||||
export interface TableProfilerProps {
|
||||
isTableDeleted?: boolean;
|
||||
permissions: OperationPermission;
|
||||
table?: Table;
|
||||
}
|
||||
|
||||
export interface TableProfilerProviderProps extends TableProfilerProps {
|
||||
@ -54,6 +54,7 @@ export interface TableProfilerContextInterface {
|
||||
dateRangeObject: DateRangeObject;
|
||||
onDateRangeChange: (dateRange: DateRangeObject) => void;
|
||||
testCasePaging: UsePagingInterface;
|
||||
table?: Table;
|
||||
}
|
||||
|
||||
export type TableTestsType = {
|
||||
|
||||
@ -14,6 +14,7 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { OperationPermission } from '../../../../context/PermissionProvider/PermissionProvider.interface';
|
||||
import { MOCK_TABLE } from '../../../../mocks/TableData.mock';
|
||||
import { getListTestCase } from '../../../../rest/testAPI';
|
||||
import { TableProfilerProvider } from './TableProfilerProvider';
|
||||
|
||||
@ -69,27 +70,20 @@ const mockPermissions = {
|
||||
} as OperationPermission;
|
||||
|
||||
describe('TableProfilerProvider', () => {
|
||||
it('renders children without crashing', async () => {
|
||||
beforeEach(() => {
|
||||
render(
|
||||
<TableProfilerProvider
|
||||
isTableDeleted={false}
|
||||
permissions={mockPermissions}>
|
||||
<TableProfilerProvider permissions={mockPermissions} table={MOCK_TABLE}>
|
||||
<div>Test Children</div>
|
||||
</TableProfilerProvider>
|
||||
);
|
||||
});
|
||||
|
||||
it('renders children without crashing', async () => {
|
||||
expect(await screen.findByText('Test Children')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('test cases should be fetch on data quality tab', async () => {
|
||||
const mockGetListTestCase = getListTestCase as jest.Mock;
|
||||
render(
|
||||
<TableProfilerProvider
|
||||
isTableDeleted={false}
|
||||
permissions={mockPermissions}>
|
||||
<div>Test Children</div>
|
||||
</TableProfilerProvider>
|
||||
);
|
||||
|
||||
expect(mockGetListTestCase).toHaveBeenCalledTimes(1);
|
||||
expect(mockGetListTestCase).toHaveBeenCalledWith({
|
||||
|
||||
@ -58,7 +58,7 @@ export const TableProfilerContext =
|
||||
export const TableProfilerProvider = ({
|
||||
children,
|
||||
permissions,
|
||||
isTableDeleted,
|
||||
table,
|
||||
}: TableProfilerProviderProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { fqn: datasetFQN } = useFqn();
|
||||
@ -76,6 +76,8 @@ export const TableProfilerProvider = ({
|
||||
const [dateRangeObject, setDateRangeObject] =
|
||||
useState<DateRangeObject>(DEFAULT_RANGE_DATA);
|
||||
|
||||
const isTableDeleted = useMemo(() => table?.deleted, [table]);
|
||||
|
||||
const {
|
||||
activeTab = isTourOpen
|
||||
? TableProfilerTab.COLUMN_PROFILE
|
||||
@ -269,6 +271,7 @@ export const TableProfilerProvider = ({
|
||||
onDateRangeChange: handleDateRangeChange,
|
||||
dateRangeObject,
|
||||
testCasePaging,
|
||||
table,
|
||||
};
|
||||
}, [
|
||||
isTestsLoading,
|
||||
@ -282,6 +285,7 @@ export const TableProfilerProvider = ({
|
||||
customMetric,
|
||||
dateRangeObject,
|
||||
testCasePaging,
|
||||
table,
|
||||
]);
|
||||
|
||||
return (
|
||||
|
||||
@ -19,16 +19,12 @@ import SchemaTable from '../SchemaTable/SchemaTable.component';
|
||||
import { Props } from './SchemaTab.interfaces';
|
||||
|
||||
const SchemaTab: FunctionComponent<Props> = ({
|
||||
columns,
|
||||
joins,
|
||||
table,
|
||||
onUpdate,
|
||||
columnName,
|
||||
hasDescriptionEditAccess,
|
||||
hasTagEditAccess,
|
||||
onThreadLinkSelect,
|
||||
isReadOnly = false,
|
||||
entityFqn,
|
||||
tableConstraints,
|
||||
}: Props) => {
|
||||
const [searchText, setSearchText] = useState('');
|
||||
|
||||
@ -42,7 +38,7 @@ const SchemaTab: FunctionComponent<Props> = ({
|
||||
<div className="w-1/2">
|
||||
<Searchbar
|
||||
removeMargin
|
||||
placeholder={`${t('message.find-in-table')}`}
|
||||
placeholder={t('message.find-in-table')}
|
||||
searchValue={searchText}
|
||||
typingInterval={500}
|
||||
onSearch={handleSearchAction}
|
||||
@ -50,15 +46,11 @@ const SchemaTab: FunctionComponent<Props> = ({
|
||||
</div>
|
||||
</div>
|
||||
<SchemaTable
|
||||
columnName={columnName}
|
||||
entityFqn={entityFqn}
|
||||
hasDescriptionEditAccess={hasDescriptionEditAccess}
|
||||
hasTagEditAccess={hasTagEditAccess}
|
||||
isReadOnly={isReadOnly}
|
||||
joins={joins}
|
||||
searchText={lowerCase(searchText)}
|
||||
tableColumns={columns}
|
||||
tableConstraints={tableConstraints}
|
||||
table={table}
|
||||
onThreadLinkSelect={onThreadLinkSelect}
|
||||
onUpdate={onUpdate}
|
||||
/>
|
||||
|
||||
@ -12,22 +12,13 @@
|
||||
*/
|
||||
|
||||
import { ThreadType } from '../../../generated/api/feed/createThread';
|
||||
import {
|
||||
ColumnJoins,
|
||||
Table,
|
||||
TableData,
|
||||
} from '../../../generated/entity/data/table';
|
||||
import { Table } from '../../../generated/entity/data/table';
|
||||
|
||||
export type Props = {
|
||||
columns: Table['columns'];
|
||||
joins: Array<ColumnJoins>;
|
||||
columnName: string;
|
||||
tableConstraints: Table['tableConstraints'];
|
||||
sampleData?: TableData;
|
||||
table?: Table;
|
||||
hasDescriptionEditAccess: boolean;
|
||||
hasTagEditAccess: boolean;
|
||||
isReadOnly?: boolean;
|
||||
entityFqn: string;
|
||||
onThreadLinkSelect: (value: string, threadType?: ThreadType) => void;
|
||||
onUpdate: (columns: Table['columns']) => Promise<void>;
|
||||
};
|
||||
|
||||
@ -14,57 +14,11 @@
|
||||
import { getByTestId, getByText, render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import {
|
||||
Column,
|
||||
DataType,
|
||||
LabelType,
|
||||
State,
|
||||
Table,
|
||||
TagSource,
|
||||
} from '../../../generated/entity/data/table';
|
||||
import { MOCK_TABLE } from '../../../mocks/TableData.mock';
|
||||
import SchemaTab from './SchemaTab.component';
|
||||
const mockColumns: Column[] = [
|
||||
{
|
||||
name: 'testId',
|
||||
dataType: DataType.String,
|
||||
description: 'string',
|
||||
fullyQualifiedName: 'string',
|
||||
tags: [
|
||||
{
|
||||
tagFQN: 'string',
|
||||
labelType: LabelType.Manual,
|
||||
source: TagSource.Classification,
|
||||
state: State.Confirmed,
|
||||
},
|
||||
{
|
||||
tagFQN: 'string2',
|
||||
labelType: LabelType.Derived,
|
||||
source: TagSource.Classification,
|
||||
state: State.Confirmed,
|
||||
},
|
||||
],
|
||||
ordinalPosition: 2,
|
||||
},
|
||||
];
|
||||
|
||||
const mockjoins = [
|
||||
{
|
||||
columnName: 'testId',
|
||||
joinedWith: [{ fullyQualifiedName: 'joinedTable', joinCount: 1 }],
|
||||
},
|
||||
];
|
||||
|
||||
const mockUpdate = jest.fn();
|
||||
|
||||
const mockSampleData = {
|
||||
columns: ['column1', 'column2', 'column3'],
|
||||
rows: [
|
||||
['row1', 'row2', 'row3'],
|
||||
['row1', 'row2', 'row3'],
|
||||
['row1', 'row2', 'row3'],
|
||||
],
|
||||
};
|
||||
|
||||
jest.mock('../SampleDataTable/SampleDataTable.component', () => {
|
||||
return jest.fn().mockReturnValue(<p>SampleDataTable</p>);
|
||||
});
|
||||
@ -73,26 +27,14 @@ jest.mock('../SchemaTable/SchemaTable.component', () => {
|
||||
return jest.fn().mockReturnValue(<p>SchemaTable</p>);
|
||||
});
|
||||
|
||||
const mockTableConstraints = [
|
||||
{
|
||||
constraintType: 'PRIMARY_KEY',
|
||||
columns: ['address_id', 'shop_id'],
|
||||
},
|
||||
] as Table['tableConstraints'];
|
||||
|
||||
describe('Test SchemaTab Component', () => {
|
||||
it('Renders all the parts of the schema tab', () => {
|
||||
const { queryByTestId, container } = render(
|
||||
<SchemaTab
|
||||
hasDescriptionEditAccess
|
||||
hasTagEditAccess
|
||||
columnName="columnName"
|
||||
columns={mockColumns}
|
||||
entityFqn="mlflow_svc.eta_predictions"
|
||||
isReadOnly={false}
|
||||
joins={mockjoins}
|
||||
sampleData={mockSampleData}
|
||||
tableConstraints={mockTableConstraints}
|
||||
table={MOCK_TABLE}
|
||||
onThreadLinkSelect={jest.fn()}
|
||||
onUpdate={mockUpdate}
|
||||
/>,
|
||||
|
||||
@ -19,6 +19,7 @@ import {
|
||||
cloneDeep,
|
||||
groupBy,
|
||||
isEmpty,
|
||||
isEqual,
|
||||
isUndefined,
|
||||
set,
|
||||
sortBy,
|
||||
@ -52,6 +53,7 @@ import {
|
||||
getFrequentlyJoinedColumns,
|
||||
searchInColumns,
|
||||
} from '../../../utils/EntityUtils';
|
||||
import { getEntityColumnFQN } from '../../../utils/FeedUtils';
|
||||
import {
|
||||
getAllTags,
|
||||
searchTagInData,
|
||||
@ -66,6 +68,7 @@ import {
|
||||
import { showErrorToast } from '../../../utils/ToastUtils';
|
||||
import FilterTablePlaceHolder from '../../common/ErrorWithPlaceholder/FilterTablePlaceHolder';
|
||||
import Table from '../../common/Table/Table';
|
||||
import TestCaseStatusSummaryIndicator from '../../common/TestCaseStatusSummaryIndicator/TestCaseStatusSummaryIndicator.component';
|
||||
import EntityNameModal from '../../Modals/EntityNameModal/EntityNameModal.component';
|
||||
import { EntityName } from '../../Modals/EntityNameModal/EntityNameModal.interface';
|
||||
import { ModalWithMarkdownEditor } from '../../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor';
|
||||
@ -75,18 +78,25 @@ import TableTags from '../TableTags/TableTags.component';
|
||||
import { SchemaTableProps, TableCellRendered } from './SchemaTable.interface';
|
||||
|
||||
const SchemaTable = ({
|
||||
tableColumns,
|
||||
searchText,
|
||||
onUpdate,
|
||||
hasDescriptionEditAccess,
|
||||
hasTagEditAccess,
|
||||
joins,
|
||||
isReadOnly = false,
|
||||
onThreadLinkSelect,
|
||||
tableConstraints,
|
||||
table,
|
||||
}: SchemaTableProps) => {
|
||||
const { theme } = useApplicationStore();
|
||||
const { t } = useTranslation();
|
||||
const { testCaseCounts, tableColumns, joins, tableConstraints } = useMemo(
|
||||
() => ({
|
||||
testCaseCounts: table?.testSuite?.summary?.columnTestSummary ?? [],
|
||||
tableColumns: table?.columns ?? [],
|
||||
joins: table?.joins?.columnJoins ?? [],
|
||||
tableConstraints: table?.tableConstraints,
|
||||
}),
|
||||
[table]
|
||||
);
|
||||
|
||||
const [searchedColumns, setSearchedColumns] = useState<Column[]>([]);
|
||||
const [expandedRowKeys, setExpandedRowKeys] = useState<string[]>([]);
|
||||
@ -455,6 +465,24 @@ const SchemaTable = ({
|
||||
filterDropdown: ColumnFilter,
|
||||
onFilter: searchTagInData,
|
||||
},
|
||||
{
|
||||
title: t('label.data-quality-test-plural'),
|
||||
dataIndex: 'dataQualityTest',
|
||||
key: 'dataQualityTest',
|
||||
width: 170,
|
||||
render: (_, record) => {
|
||||
const testCounts = testCaseCounts.find((column) => {
|
||||
return isEqual(
|
||||
getEntityColumnFQN(column.entityLink ?? ''),
|
||||
record.fullyQualifiedName
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<TestCaseStatusSummaryIndicator testCaseStatusCounts={testCounts} />
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
decodedEntityFqn,
|
||||
@ -468,6 +496,7 @@ const SchemaTable = ({
|
||||
handleTagSelection,
|
||||
onThreadLinkSelect,
|
||||
tagFilter,
|
||||
testCaseCounts,
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@ -13,24 +13,16 @@
|
||||
|
||||
import { ReactNode } from 'react';
|
||||
import { ThreadType } from '../../../generated/api/feed/createThread';
|
||||
import {
|
||||
Column,
|
||||
ColumnJoins,
|
||||
Table,
|
||||
} from '../../../generated/entity/data/table';
|
||||
import { Column, Table } from '../../../generated/entity/data/table';
|
||||
|
||||
export interface SchemaTableProps {
|
||||
tableColumns: Column[];
|
||||
joins: Array<ColumnJoins>;
|
||||
columnName: string;
|
||||
hasDescriptionEditAccess: boolean;
|
||||
hasTagEditAccess: boolean;
|
||||
tableConstraints: Table['tableConstraints'];
|
||||
searchText?: string;
|
||||
isReadOnly?: boolean;
|
||||
entityFqn: string;
|
||||
onUpdate: (columns: Column[]) => Promise<void>;
|
||||
onThreadLinkSelect: (value: string, threadType?: ThreadType) => void;
|
||||
table?: Table;
|
||||
}
|
||||
|
||||
export type TableCellRendered<T, K extends keyof T> = (
|
||||
|
||||
@ -15,10 +15,11 @@ import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { Column } from '../../../generated/entity/data/container';
|
||||
import { Table, TablePartition } from '../../../generated/entity/data/table';
|
||||
import { Table } from '../../../generated/entity/data/table';
|
||||
import { MOCK_TABLE } from '../../../mocks/TableData.mock';
|
||||
import EntityTableV1 from './SchemaTable.component';
|
||||
import { SchemaTableProps } from './SchemaTable.interface';
|
||||
|
||||
const onEntityFieldSelect = jest.fn();
|
||||
const onThreadLinkSelect = jest.fn();
|
||||
const onUpdate = jest.fn();
|
||||
|
||||
@ -28,60 +29,52 @@ const mockTableConstraints = [
|
||||
columns: ['address_id', 'shop_id'],
|
||||
},
|
||||
] as Table['tableConstraints'];
|
||||
const columns = [
|
||||
{
|
||||
name: 'comments',
|
||||
dataType: 'STRING',
|
||||
dataLength: 1,
|
||||
dataTypeDisplay: 'string',
|
||||
fullyQualifiedName:
|
||||
'bigquery_gcp.ecommerce.shopify.raw_product_catalog.comments',
|
||||
tags: [],
|
||||
constraint: 'NULL',
|
||||
ordinalPosition: 1,
|
||||
},
|
||||
{
|
||||
name: 'products',
|
||||
dataType: 'ARRAY',
|
||||
arrayDataType: 'STRUCT',
|
||||
dataLength: 1,
|
||||
dataTypeDisplay:
|
||||
'array<struct<product_id:character varying(24),price:int,onsale:boolean,tax:int,weight:int,others:int,vendor:character varying(64), stock:int>>',
|
||||
fullyQualifiedName:
|
||||
'bigquery_gcp.ecommerce.shopify.raw_product_catalog.products',
|
||||
tags: [],
|
||||
constraint: 'NULL',
|
||||
ordinalPosition: 2,
|
||||
},
|
||||
{
|
||||
name: 'platform',
|
||||
dataType: 'STRING',
|
||||
dataLength: 1,
|
||||
dataTypeDisplay: 'string',
|
||||
fullyQualifiedName:
|
||||
'bigquery_gcp.ecommerce.shopify.raw_product_catalog.platform',
|
||||
tags: [],
|
||||
constraint: 'NULL',
|
||||
ordinalPosition: 3,
|
||||
},
|
||||
] as Column[];
|
||||
|
||||
const mockEntityTableProp = {
|
||||
tableColumns: [
|
||||
{
|
||||
name: 'comments',
|
||||
dataType: 'STRING',
|
||||
dataLength: 1,
|
||||
dataTypeDisplay: 'string',
|
||||
fullyQualifiedName:
|
||||
'bigquery_gcp.ecommerce.shopify.raw_product_catalog.comments',
|
||||
tags: [],
|
||||
constraint: 'NULL',
|
||||
ordinalPosition: 1,
|
||||
},
|
||||
{
|
||||
name: 'products',
|
||||
dataType: 'ARRAY',
|
||||
arrayDataType: 'STRUCT',
|
||||
dataLength: 1,
|
||||
dataTypeDisplay:
|
||||
'array<struct<product_id:character varying(24),price:int,onsale:boolean,tax:int,weight:int,others:int,vendor:character varying(64), stock:int>>',
|
||||
fullyQualifiedName:
|
||||
'bigquery_gcp.ecommerce.shopify.raw_product_catalog.products',
|
||||
tags: [],
|
||||
constraint: 'NULL',
|
||||
ordinalPosition: 2,
|
||||
},
|
||||
{
|
||||
name: 'platform',
|
||||
dataType: 'STRING',
|
||||
dataLength: 1,
|
||||
dataTypeDisplay: 'string',
|
||||
fullyQualifiedName:
|
||||
'bigquery_gcp.ecommerce.shopify.raw_product_catalog.platform',
|
||||
tags: [],
|
||||
constraint: 'NULL',
|
||||
ordinalPosition: 3,
|
||||
},
|
||||
] as Column[],
|
||||
const mockEntityTableProp: SchemaTableProps = {
|
||||
searchText: '',
|
||||
hasEditAccess: false,
|
||||
joins: [],
|
||||
entityFieldThreads: [],
|
||||
hasDescriptionEditAccess: true,
|
||||
isReadOnly: false,
|
||||
entityFqn: 'bigquery_gcp.ecommerce.shopify.raw_product_catalog',
|
||||
owner: {} as Table['owner'],
|
||||
columnName: '',
|
||||
hasTagEditAccess: true,
|
||||
tableConstraints: mockTableConstraints,
|
||||
tablePartitioned: {} as TablePartition,
|
||||
onEntityFieldSelect,
|
||||
onThreadLinkSelect,
|
||||
onUpdate,
|
||||
table: { ...MOCK_TABLE, columns, tableConstraints: mockTableConstraints },
|
||||
};
|
||||
|
||||
const columnsWithDisplayName = [
|
||||
@ -201,9 +194,15 @@ describe('Test EntityTable Component', () => {
|
||||
});
|
||||
|
||||
it('Table should load empty when no data present', async () => {
|
||||
render(<EntityTableV1 {...mockEntityTableProp} tableColumns={[]} />, {
|
||||
wrapper: MemoryRouter,
|
||||
});
|
||||
render(
|
||||
<EntityTableV1
|
||||
{...mockEntityTableProp}
|
||||
table={{ ...MOCK_TABLE, columns: [] }}
|
||||
/>,
|
||||
{
|
||||
wrapper: MemoryRouter,
|
||||
}
|
||||
);
|
||||
|
||||
const entityTable = await screen.findByTestId('entity-table');
|
||||
|
||||
@ -232,7 +231,7 @@ describe('Test EntityTable Component', () => {
|
||||
render(
|
||||
<EntityTableV1
|
||||
{...mockEntityTableProp}
|
||||
tableColumns={[...columnsWithDisplayName]}
|
||||
table={{ ...MOCK_TABLE, columns: columnsWithDisplayName }}
|
||||
/>,
|
||||
{
|
||||
wrapper: MemoryRouter,
|
||||
@ -256,7 +255,7 @@ describe('Test EntityTable Component', () => {
|
||||
<EntityTableV1
|
||||
{...mockEntityTableProp}
|
||||
isReadOnly
|
||||
tableColumns={[...columnsWithDisplayName]}
|
||||
table={{ ...MOCK_TABLE, columns: columnsWithDisplayName }}
|
||||
/>,
|
||||
{
|
||||
wrapper: MemoryRouter,
|
||||
|
||||
@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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 { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import TestCaseStatusSummaryIndicator from './TestCaseStatusSummaryIndicator.component';
|
||||
|
||||
jest.mock('../TestIndicator/TestIndicator', () => {
|
||||
return jest
|
||||
.fn()
|
||||
.mockImplementation(({ type }) => <div>{`test-indicator-${type}`}</div>);
|
||||
});
|
||||
|
||||
describe('TestCaseStatusSummaryIndicator', () => {
|
||||
it('should render test indicators for each test status', () => {
|
||||
const testCaseStatusCounts = {
|
||||
success: 5,
|
||||
failed: 2,
|
||||
aborted: 3,
|
||||
total: 10,
|
||||
queued: 0,
|
||||
entityLink: 'test',
|
||||
};
|
||||
|
||||
render(
|
||||
<TestCaseStatusSummaryIndicator
|
||||
testCaseStatusCounts={testCaseStatusCounts}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('test-indicator-success')).toBeInTheDocument();
|
||||
expect(screen.getByText('test-indicator-failed')).toBeInTheDocument();
|
||||
expect(screen.getByText('test-indicator-aborted')).toBeInTheDocument();
|
||||
expect(screen.getByText('test-indicator-queued')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render no data placeholder when testCaseStatusCounts is null', () => {
|
||||
render(<TestCaseStatusSummaryIndicator />);
|
||||
|
||||
expect(screen.getByTestId('no-data-placeholder')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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 { Space, Typography } from 'antd';
|
||||
import { omit } from 'lodash';
|
||||
import React from 'react';
|
||||
import { NO_DATA_PLACEHOLDER } from '../../../constants/constants';
|
||||
import TestIndicator from '../TestIndicator/TestIndicator';
|
||||
import { TestCaseStatusSummaryIndicatorProps } from './TestCaseStatusSummaryIndicator.interface';
|
||||
|
||||
const TestCaseStatusSummaryIndicator = ({
|
||||
testCaseStatusCounts,
|
||||
}: TestCaseStatusSummaryIndicatorProps) => {
|
||||
return testCaseStatusCounts ? (
|
||||
<Space size={16}>
|
||||
{Object.entries(omit(testCaseStatusCounts, ['entityLink', 'total'])).map(
|
||||
(test) => (
|
||||
<TestIndicator key={test[0]} type={test[0]} value={test[1]} />
|
||||
)
|
||||
)}
|
||||
</Space>
|
||||
) : (
|
||||
<Typography.Text data-testid="no-data-placeholder">
|
||||
{NO_DATA_PLACEHOLDER}
|
||||
</Typography.Text>
|
||||
);
|
||||
};
|
||||
|
||||
export default TestCaseStatusSummaryIndicator;
|
||||
@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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 { ColumnTestSummaryDefinition } from '../../../generated/tests/testCase';
|
||||
|
||||
export interface TestCaseStatusSummaryIndicatorProps {
|
||||
testCaseStatusCounts?: ColumnTestSummaryDefinition;
|
||||
}
|
||||
@ -41,4 +41,8 @@
|
||||
background: @abortedColor;
|
||||
border: 1px solid @abort-border;
|
||||
}
|
||||
&.queued {
|
||||
background: @grey-1;
|
||||
border: 1px solid @grey-3;
|
||||
}
|
||||
}
|
||||
|
||||
@ -280,6 +280,7 @@
|
||||
"data-proportion-plural": "Datenverhältnisse",
|
||||
"data-quality": "Datenqualität",
|
||||
"data-quality-test": "Datenqualitätstest",
|
||||
"data-quality-test-plural": "Data Quality Tests",
|
||||
"data-quartile-plural": "Datenquartile",
|
||||
"data-range": "Datenbereich",
|
||||
"data-type": "Datentyp",
|
||||
|
||||
@ -280,6 +280,7 @@
|
||||
"data-proportion-plural": "Data Proportions",
|
||||
"data-quality": "Data Quality",
|
||||
"data-quality-test": "Data Quality Test",
|
||||
"data-quality-test-plural": "Data Quality Tests",
|
||||
"data-quartile-plural": "Data Quartiles",
|
||||
"data-range": "Data Range",
|
||||
"data-type": "Data Type",
|
||||
|
||||
@ -280,6 +280,7 @@
|
||||
"data-proportion-plural": "Proporciones de datos",
|
||||
"data-quality": "Tests de calidad",
|
||||
"data-quality-test": "Test de calidad de datos",
|
||||
"data-quality-test-plural": "Data Quality Tests",
|
||||
"data-quartile-plural": "cuartiles",
|
||||
"data-range": "Rango de datos",
|
||||
"data-type": "Tipo de datos",
|
||||
|
||||
@ -280,6 +280,7 @@
|
||||
"data-proportion-plural": "Proportions des Données",
|
||||
"data-quality": "Qualité des Données",
|
||||
"data-quality-test": "Test de Qualité des Données",
|
||||
"data-quality-test-plural": "Data Quality Tests",
|
||||
"data-quartile-plural": "Quartiles des Données",
|
||||
"data-range": "Plage de Données",
|
||||
"data-type": "Type de Données",
|
||||
|
||||
@ -280,6 +280,7 @@
|
||||
"data-proportion-plural": "יחסי נתונים",
|
||||
"data-quality": "איכות נתונים",
|
||||
"data-quality-test": "בקרת איכות נתונים",
|
||||
"data-quality-test-plural": "Data Quality Tests",
|
||||
"data-quartile-plural": "רביעונים (Quaertiles)",
|
||||
"data-range": "טווח נתונים",
|
||||
"data-type": "סוג עמודה",
|
||||
|
||||
@ -280,6 +280,7 @@
|
||||
"data-proportion-plural": "Data Proportions",
|
||||
"data-quality": "Data Quality",
|
||||
"data-quality-test": "データ品質テスト",
|
||||
"data-quality-test-plural": "Data Quality Tests",
|
||||
"data-quartile-plural": "Data Quartiles",
|
||||
"data-range": "Data Range",
|
||||
"data-type": "データ型",
|
||||
|
||||
@ -280,6 +280,7 @@
|
||||
"data-proportion-plural": "Dataproporties",
|
||||
"data-quality": "Datakwaliteit",
|
||||
"data-quality-test": "Datakwaliteitstest",
|
||||
"data-quality-test-plural": "Data Quality Tests",
|
||||
"data-quartile-plural": "Datakwartielen",
|
||||
"data-range": "Datasbereik",
|
||||
"data-type": "Datatype",
|
||||
|
||||
@ -280,6 +280,7 @@
|
||||
"data-proportion-plural": "Proporções de Dados",
|
||||
"data-quality": "Qualidade de Dados",
|
||||
"data-quality-test": "Teste de Qualidade de Dados",
|
||||
"data-quality-test-plural": "Data Quality Tests",
|
||||
"data-quartile-plural": "Quartis de Dados",
|
||||
"data-range": "Intervalo de Dados",
|
||||
"data-type": "Tipo de Dados",
|
||||
|
||||
@ -280,6 +280,7 @@
|
||||
"data-proportion-plural": "Распределение данных",
|
||||
"data-quality": "Качество данных",
|
||||
"data-quality-test": "Тест качества данных",
|
||||
"data-quality-test-plural": "Data Quality Tests",
|
||||
"data-quartile-plural": "Качество данных",
|
||||
"data-range": "Временной интервал",
|
||||
"data-type": "Тип данных",
|
||||
|
||||
@ -280,6 +280,7 @@
|
||||
"data-proportion-plural": "数据比例",
|
||||
"data-quality": "数据质控",
|
||||
"data-quality-test": "数据质控测试",
|
||||
"data-quality-test-plural": "Data Quality Tests",
|
||||
"data-quartile-plural": "数据四分位数",
|
||||
"data-range": "数据范围",
|
||||
"data-type": "数据类型",
|
||||
|
||||
@ -244,7 +244,7 @@ describe('TestDetailsPageV1 component', () => {
|
||||
});
|
||||
|
||||
expect(getTableDetailsByFQN).toHaveBeenCalledWith('fqn', {
|
||||
fields: `${COMMON_API_FIELDS},usageSummary`,
|
||||
fields: `${COMMON_API_FIELDS},usageSummary,testSuite`,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -132,8 +132,13 @@ const TableDetailsPageV1: React.FC = () => {
|
||||
datasetFQN
|
||||
);
|
||||
|
||||
const viewUsagePermission = useMemo(
|
||||
() => tablePermissions.ViewAll || tablePermissions.ViewUsage,
|
||||
const { viewUsagePermission, viewTestCasePermission } = useMemo(
|
||||
() => ({
|
||||
viewUsagePermission:
|
||||
tablePermissions.ViewAll || tablePermissions.ViewUsage,
|
||||
viewTestCasePermission:
|
||||
tablePermissions.ViewAll || tablePermissions.ViewTests,
|
||||
}),
|
||||
[tablePermissions]
|
||||
);
|
||||
|
||||
@ -154,6 +159,9 @@ const TableDetailsPageV1: React.FC = () => {
|
||||
if (viewUsagePermission) {
|
||||
fields += `,${TabSpecificField.USAGE_SUMMARY}`;
|
||||
}
|
||||
if (viewTestCasePermission) {
|
||||
fields += `,${TabSpecificField.TESTSUITE}`;
|
||||
}
|
||||
|
||||
const details = await getTableDetailsByFQN(tableFqn, { fields });
|
||||
|
||||
@ -528,18 +536,10 @@ const TableDetailsPageV1: React.FC = () => {
|
||||
onThreadLinkSelect={onThreadLinkSelect}
|
||||
/>
|
||||
<SchemaTab
|
||||
columnName={getPartialNameFromTableFQN(
|
||||
tableFqn,
|
||||
[FqnPart['Column']],
|
||||
FQN_SEPARATOR_CHAR
|
||||
)}
|
||||
columns={tableDetails?.columns ?? []}
|
||||
entityFqn={datasetFQN}
|
||||
hasDescriptionEditAccess={editDescriptionPermission}
|
||||
hasTagEditAccess={editTagsPermission}
|
||||
isReadOnly={deleted}
|
||||
joins={tableDetails?.joins?.columnJoins ?? []}
|
||||
tableConstraints={tableDetails?.tableConstraints}
|
||||
table={tableDetails}
|
||||
onThreadLinkSelect={onThreadLinkSelect}
|
||||
onUpdate={onColumnsUpdate}
|
||||
/>
|
||||
@ -679,8 +679,8 @@ const TableDetailsPageV1: React.FC = () => {
|
||||
<ErrorPlaceHolder type={ERROR_PLACEHOLDER_TYPE.PERMISSION} />
|
||||
) : (
|
||||
<TableProfiler
|
||||
isTableDeleted={deleted}
|
||||
permissions={tablePermissions}
|
||||
table={tableDetails}
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
||||
@ -16,6 +16,8 @@ const entityLink =
|
||||
'<#E::table::sample_data.ecommerce_db.shopify.dim_address::description>';
|
||||
const entityLinkWithColumn =
|
||||
'<#E::table::sample_data.ecommerce_db.shopify.dim_address::columns::address_id::tags>';
|
||||
const entityLinkWithNestedColumn =
|
||||
'<#E::table::sample_data.ecommerce_db.shopify.dim_address::columns::"address_id.city"::tags>';
|
||||
|
||||
describe('Test EntityLink', () => {
|
||||
it('Should split the entityLink into parts', () => {
|
||||
@ -81,4 +83,24 @@ describe('Test EntityLink', () => {
|
||||
'<#E::table::sample_data.ecommerce_db.shopify.dim_address>'
|
||||
);
|
||||
});
|
||||
|
||||
it('Should return entityFqn from entityLink', () => {
|
||||
expect(EntityLink.getEntityColumnFqn(entityLink)).toStrictEqual(
|
||||
'sample_data.ecommerce_db.shopify.dim_address'
|
||||
);
|
||||
});
|
||||
|
||||
it('Should return entityColumnFqn from entityLink for column', () => {
|
||||
expect(EntityLink.getEntityColumnFqn(entityLinkWithColumn)).toStrictEqual(
|
||||
'sample_data.ecommerce_db.shopify.dim_address.address_id'
|
||||
);
|
||||
});
|
||||
|
||||
it('Should return entityColumnFqn from entityLink for nested column', () => {
|
||||
expect(
|
||||
EntityLink.getEntityColumnFqn(entityLinkWithNestedColumn)
|
||||
).toStrictEqual(
|
||||
'sample_data.ecommerce_db.shopify.dim_address."address_id.city"'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -57,6 +57,17 @@ export default class EntityLink {
|
||||
return this.split(entityLink)[1];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param string entityLink column
|
||||
* @returns entityColumn fqn
|
||||
*/
|
||||
static getEntityColumnFqn(entityLink: string) {
|
||||
const parts = this.split(entityLink);
|
||||
|
||||
return `${parts[1]}${parts[3] ? '.' + parts[3] : ''}`;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param string entityLink
|
||||
|
||||
@ -74,6 +74,9 @@ export const getEntityType = (entityLink: string) => {
|
||||
export const getEntityFQN = (entityLink: string) => {
|
||||
return EntityLink.getEntityFqn(entityLink);
|
||||
};
|
||||
export const getEntityColumnFQN = (entityLink: string) => {
|
||||
return EntityLink.getEntityColumnFqn(entityLink);
|
||||
};
|
||||
export const getEntityField = (entityLink: string) => {
|
||||
const match = EntityRegEx.exec(entityLink);
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user