#14440 Enhance table details, and restore the test case states in column profiler tab (#16024)

Co-authored-by: Teddy <teddy.crepineau@gmail.com>
This commit is contained in:
Shailesh Parmar 2024-04-25 22:05:58 +05:30 committed by GitHub
parent 7db1612c13
commit 016f585e14
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 330 additions and 213 deletions

View File

@ -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(

View File

@ -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>

View File

@ -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 = {

View File

@ -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({

View File

@ -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 (

View File

@ -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}
/>

View File

@ -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>;
};

View File

@ -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}
/>,

View File

@ -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,
]
);

View File

@ -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> = (

View File

@ -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,

View File

@ -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();
});
});

View File

@ -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;

View File

@ -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;
}

View File

@ -41,4 +41,8 @@
background: @abortedColor;
border: 1px solid @abort-border;
}
&.queued {
background: @grey-1;
border: 1px solid @grey-3;
}
}

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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": "סוג עמודה",

View File

@ -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": "データ型",

View File

@ -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",

View File

@ -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",

View File

@ -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": "Тип данных",

View File

@ -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": "数据类型",

View File

@ -244,7 +244,7 @@ describe('TestDetailsPageV1 component', () => {
});
expect(getTableDetailsByFQN).toHaveBeenCalledWith('fqn', {
fields: `${COMMON_API_FIELDS},usageSummary`,
fields: `${COMMON_API_FIELDS},usageSummary,testSuite`,
});
});

View File

@ -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}
/>
),
},

View File

@ -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"'
);
});
});

View File

@ -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

View File

@ -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);