Data qaulity add test's workflow (#6957)

* initial setup for data quality form

* added stepper and form component

* added select/add test suite step form

* added form for table test and cron step

* added data quality table test form flow

* bug fix for profiler

* added column test form

* render right panel information dynamically

* updated test as per new changes

* updated data test id

* Fixed ingestionPipeline

* Fixed pytest + python format

* miner fix, added sql editor

* removed filter for duplicate test check

* miner fix

Co-authored-by: Teddy Crepineau <teddy.crepineau@gmail.com>
This commit is contained in:
Shailesh Parmar 2022-08-29 17:26:59 +05:30 committed by GitHub
parent cc0449e506
commit 3b67cc824d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1435 additions and 56 deletions

View File

@ -333,7 +333,7 @@ class TestSuiteWorkflow:
or self.get_or_create_test_suite_entity_for_cli_workflow()
)
test_cases = self.get_test_cases_from_test_suite(test_suites)
if self.processor_config:
if self.processor_config.testSuites:
cli_config_test_cases_def = self.get_test_case_from_cli_config()
runtime_created_test_cases = self.compare_and_create_test_cases(
cli_config_test_cases_def, test_cases

View File

@ -91,7 +91,7 @@ def build_source(ingestion_pipeline: IngestionPipeline) -> WorkflowSource:
Union[DatabaseService, MessagingService, PipelineService, DashboardService]
] = None
if service_type == "TestSuite":
if service_type == "testSuite":
return WorkflowSource(
type=service_type,
serviceName=ingestion_pipeline.service.name,

View File

@ -16,6 +16,9 @@ Add a function for each type from PipelineType
from openmetadata_managed_apis.workflows.ingestion.lineage import build_lineage_dag
from openmetadata_managed_apis.workflows.ingestion.metadata import build_metadata_dag
from openmetadata_managed_apis.workflows.ingestion.profiler import build_profiler_dag
from openmetadata_managed_apis.workflows.ingestion.test_suite import (
build_test_suite_dag,
)
from openmetadata_managed_apis.workflows.ingestion.usage import build_usage_dag
from metadata.generated.schema.entity.services.ingestionPipelines.ingestionPipeline import (
@ -29,3 +32,4 @@ build_registry.add(PipelineType.metadata.value)(build_metadata_dag)
build_registry.add(PipelineType.usage.value)(build_usage_dag)
build_registry.add(PipelineType.lineage.value)(build_lineage_dag)
build_registry.add(PipelineType.profiler.value)(build_profiler_dag)
build_registry.add(PipelineType.TestSuite.value)(build_test_suite_dag)

View File

@ -306,7 +306,7 @@ class OMetaServiceTest(TestCase):
),
service=EntityReference(
id=self.test_suite.id,
type="TestSuite",
type="testSuite",
name=self.test_suite.name.__root__,
),
)

View File

@ -11,35 +11,49 @@
* limitations under the License.
*/
import { CreateTestCase } from '../generated/api/tests/createTestCase';
import { CreateTestSuite } from '../generated/api/tests/createTestSuite';
import { TestCase, TestCaseResult } from '../generated/tests/testCase';
import { EntityType, TestDefinition } from '../generated/tests/testDefinition';
import { TestSuite } from '../generated/tests/testSuite';
import { Include } from '../generated/type/include';
import { Paging } from '../generated/type/paging';
import APIClient from './index';
export type ListTestCaseParams = {
export type ListParams = {
fields?: string;
limit?: number;
before?: string;
after?: string;
entityLink?: string;
testSuiteId?: string;
includeAllTests?: boolean;
include?: Include;
};
export type ListTestCaseResultsParams = {
startTs?: number;
endTs?: number;
before?: string;
after?: string;
limit?: number;
export type ListTestCaseParams = ListParams & {
entityLink?: string;
testSuiteId?: string;
includeAllTests?: boolean;
};
const baseUrl = '/testCase';
export type ListTestDefinitionsParams = ListParams & {
entityType?: EntityType;
};
export type ListTestCaseResultsParams = Omit<
ListParams,
'fields' | 'include'
> & {
startTs?: number;
endTs?: number;
};
const testCaseUrl = '/testCase';
const testSuiteUrl = '/testSuite';
const testDefinitionUrl = '/testDefinition';
// testCase section
export const getListTestCase = async (params?: ListTestCaseParams) => {
const response = await APIClient.get<{ data: TestCase[]; paging: Paging }>(
baseUrl,
testCaseUrl,
{
params,
}
@ -52,7 +66,7 @@ export const getListTestCaseResults = async (
fqn: string,
params?: ListTestCaseResultsParams
) => {
const url = `${baseUrl}/${fqn}/testCaseResult`;
const url = `${testCaseUrl}/${fqn}/testCaseResult`;
const response = await APIClient.get<{
data: TestCaseResult[];
paging: Paging;
@ -62,3 +76,41 @@ export const getListTestCaseResults = async (
return response.data;
};
export const createTestCase = async (data: CreateTestCase) => {
const response = await APIClient.post<TestCase>(testCaseUrl, data);
return response.data;
};
// testSuite section
export const getListTestSuites = async (params?: ListParams) => {
const response = await APIClient.get<{
data: TestSuite[];
paging: Paging;
}>(testSuiteUrl, {
params,
});
return response.data;
};
export const createTestSuites = async (data: CreateTestSuite) => {
const response = await APIClient.post<TestSuite>(testSuiteUrl, data);
return response.data;
};
// testDefinition Section
export const getListTestDefinitions = async (
params?: ListTestDefinitionsParams
) => {
const response = await APIClient.get<{
data: TestDefinition[];
paging: Paging;
}>(testDefinitionUrl, {
params,
});
return response.data;
};

View File

@ -0,0 +1,61 @@
/*
* Copyright 2022 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 { ReactNode } from 'react';
import { Table } from '../../generated/entity/data/table';
import { TestCase } from '../../generated/tests/testCase';
import { TestDefinition } from '../../generated/tests/testDefinition';
import { TestSuite } from '../../generated/tests/testSuite';
export interface AddDataQualityTestProps {
table: Table;
}
export interface SelectTestSuiteProps {
initialValue?: SelectTestSuiteType;
onSubmit: (data: SelectTestSuiteType) => void;
}
export interface TestCaseFormProps {
initialValue?: TestCase;
onSubmit: (data: TestCase) => void;
onCancel: (data: TestCase) => void;
}
export interface TestSuiteIngestionProps {
testSuite: TestSuite;
onCancel: () => void;
}
export interface TestSuiteSchedulerProps {
onSubmit: (repeatFrequency: string) => void;
onCancel: () => void;
}
export interface RightPanelProps {
data: {
title: string;
body: string | ReactNode;
};
}
export type SelectTestSuiteType = {
name?: string;
description?: string;
data?: TestSuite;
isNewTestSuite: boolean;
};
export interface ParameterFormProps {
definition: TestDefinition;
}

View File

@ -0,0 +1,304 @@
/*
* Copyright 2022 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 { Col, Row } from 'antd';
import { AxiosError } from 'axios';
import { isUndefined } from 'lodash';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useHistory, useParams } from 'react-router-dom';
import { checkAirflowStatus } from '../../axiosAPIs/ingestionPipelineAPI';
import { createTestCase, createTestSuites } from '../../axiosAPIs/testAPI';
import {
getDatabaseDetailsPath,
getDatabaseSchemaDetailsPath,
getServiceDetailsPath,
getTableTabPath,
} from '../../constants/constants';
import { STEPS_FOR_ADD_TEST_CASE } from '../../constants/profiler.constant';
import { FqnPart } from '../../enums/entity.enum';
import { FormSubmitType } from '../../enums/form.enum';
import { PageLayoutType } from '../../enums/layout.enum';
import { ServiceCategory } from '../../enums/service.enum';
import { ProfilerDashboardType } from '../../enums/table.enum';
import { OwnerType } from '../../enums/user.enum';
import { CreateTestCase } from '../../generated/api/tests/createTestCase';
import { TestCase } from '../../generated/tests/testCase';
import { TestSuite } from '../../generated/tests/testSuite';
import {
getCurrentUserId,
getEntityName,
getNameFromFQN,
getPartialNameFromTableFQN,
} from '../../utils/CommonUtils';
import { getSettingPath } from '../../utils/RouterUtils';
import { serviceTypeLogo } from '../../utils/ServiceUtils';
import { showErrorToast } from '../../utils/ToastUtils';
import SuccessScreen from '../common/success-screen/SuccessScreen';
import TitleBreadcrumb from '../common/title-breadcrumb/title-breadcrumb.component';
import { TitleBreadcrumbProps } from '../common/title-breadcrumb/title-breadcrumb.interface';
import PageLayout from '../containers/PageLayout';
import IngestionStepper from '../IngestionStepper/IngestionStepper.component';
import {
AddDataQualityTestProps,
SelectTestSuiteType,
} from './AddDataQualityTest.interface';
import RightPanel from './components/RightPanel';
import SelectTestSuite from './components/SelectTestSuite';
import TestCaseForm from './components/TestCaseForm';
import { INGESTION_DATA, TEST_FORM_DATA } from './rightPanelData';
import TestSuiteIngestion from './TestSuiteIngestion';
const AddDataQualityTestV1: React.FC<AddDataQualityTestProps> = ({ table }) => {
const { entityTypeFQN, dashboardType } = useParams<Record<string, string>>();
const isColumnFqn = dashboardType === ProfilerDashboardType.COLUMN;
const history = useHistory();
const [activeServiceStep, setActiveServiceStep] = useState(1);
const [selectedTestSuite, setSelectedTestSuite] =
useState<SelectTestSuiteType>();
const [testCaseData, setTestCaseData] = useState<TestCase>();
const [testSuiteData, setTestSuiteData] = useState<TestSuite>();
const [testCaseRes, setTestCaseRes] = useState<TestCase>();
const [isAirflowRunning, setIsAirflowRunning] = useState(false);
const [addIngestion, setAddIngestion] = useState(false);
const breadcrumb = useMemo(() => {
const { service, serviceType, fullyQualifiedName = '' } = table;
const data: TitleBreadcrumbProps['titleLinks'] = [
{
name: service?.name || '',
url: service
? getServiceDetailsPath(
service.name || '',
ServiceCategory.DATABASE_SERVICES
)
: '',
imgSrc: serviceType ? serviceTypeLogo(serviceType) : undefined,
},
{
name: getPartialNameFromTableFQN(fullyQualifiedName, [
FqnPart.Database,
]),
url: getDatabaseDetailsPath(fullyQualifiedName),
},
{
name: getPartialNameFromTableFQN(fullyQualifiedName, [FqnPart.Schema]),
url: getDatabaseSchemaDetailsPath(fullyQualifiedName),
},
{
name: getEntityName(table),
url: getTableTabPath(fullyQualifiedName),
},
];
if (isColumnFqn) {
const colVal = [
{
name: getNameFromFQN(entityTypeFQN),
url: getTableTabPath(fullyQualifiedName),
},
{
name: 'Add Column Test',
url: '',
activeTitle: true,
},
];
data.push(...colVal);
} else {
data.push({
name: 'Add Table Test',
url: '',
activeTitle: true,
});
}
return data;
}, [table, entityTypeFQN, isColumnFqn]);
const handleViewTestSuiteClick = () => {
history.push(getSettingPath());
};
const handleAirflowStatusCheck = (): Promise<void> => {
return new Promise<void>((resolve, reject) => {
checkAirflowStatus()
.then((res) => {
if (res.status === 200) {
setIsAirflowRunning(true);
resolve();
} else {
setIsAirflowRunning(false);
reject();
}
})
.catch(() => {
setIsAirflowRunning(false);
reject();
});
});
};
const handleCancelClick = () => {
setActiveServiceStep((pre) => pre - 1);
};
const handleTestCaseBack = (testCase: TestCase) => {
setTestCaseData(testCase);
handleCancelClick();
};
const handleSelectTestSuite = (data: SelectTestSuiteType) => {
setSelectedTestSuite(data);
setActiveServiceStep(2);
};
const handleFormSubmit = async (data: TestCase) => {
setTestCaseData(data);
if (isUndefined(selectedTestSuite)) return;
try {
const { parameterValues, testDefinition, name, entityLink, description } =
data;
const { isNewTestSuite, data: selectedSuite } = selectedTestSuite;
const owner = {
id: getCurrentUserId(),
type: OwnerType.USER,
};
const testCasePayload: CreateTestCase = {
name,
description,
entityLink,
parameterValues,
owner,
testDefinition,
testSuite: {
id: selectedSuite?.id || '',
type: 'testSuite',
},
};
if (isNewTestSuite && isUndefined(testSuiteData)) {
const testSuitePayload = {
name: selectedTestSuite.name || '',
description: selectedTestSuite.description || '',
owner,
};
const testSuiteResponse = await createTestSuites(testSuitePayload);
testCasePayload.testSuite.id = testSuiteResponse.id || '';
setTestSuiteData(testSuiteResponse);
} else if (!isUndefined(testSuiteData)) {
testCasePayload.testSuite.id = testSuiteData.id || '';
}
const testCaseResponse = await createTestCase(testCasePayload);
setActiveServiceStep(3);
setTestCaseRes(testCaseResponse);
} catch (error) {
showErrorToast(error as AxiosError);
}
};
const RenderSelectedTab = useCallback(() => {
if (activeServiceStep === 2) {
return (
<TestCaseForm
initialValue={testCaseData}
onCancel={handleTestCaseBack}
onSubmit={handleFormSubmit}
/>
);
} else if (activeServiceStep > 2) {
const successName = selectedTestSuite?.isNewTestSuite
? `${testSuiteData?.name} & ${testCaseRes?.name}`
: testCaseRes?.name || 'Test case';
const successMessage = selectedTestSuite?.isNewTestSuite ? undefined : (
<span>
<span className="tw-mr-1 tw-font-semibold">
&quot;{successName}&quot;
</span>
<span>
has been created successfully. and will be pickup in next run.
</span>
</span>
);
return (
<SuccessScreen
handleIngestionClick={() => setAddIngestion(true)}
handleViewServiceClick={handleViewTestSuiteClick}
isAirflowSetup={isAirflowRunning}
name={successName}
showIngestionButton={selectedTestSuite?.isNewTestSuite || false}
state={FormSubmitType.ADD}
successMessage={successMessage}
viewServiceText="View Test Suite"
onCheckAirflowStatus={handleAirflowStatusCheck}
/>
);
}
return (
<SelectTestSuite
initialValue={selectedTestSuite}
onSubmit={handleSelectTestSuite}
/>
);
}, [activeServiceStep, isAirflowRunning, testCaseRes]);
useEffect(() => {
handleAirflowStatusCheck();
}, []);
return (
<PageLayout
classes="tw-max-w-full-hd tw-h-full tw-pt-4"
header={<TitleBreadcrumb titleLinks={breadcrumb} />}
layout={PageLayoutType['2ColRTL']}
rightPanel={
<RightPanel
data={
addIngestion
? INGESTION_DATA
: TEST_FORM_DATA[activeServiceStep - 1]
}
/>
}>
{addIngestion ? (
<TestSuiteIngestion
testSuite={
selectedTestSuite?.isNewTestSuite
? (testSuiteData as TestSuite)
: (selectedTestSuite?.data as TestSuite)
}
onCancel={() => setAddIngestion(false)}
/>
) : (
<Row className="tw-form-container" gutter={[16, 16]}>
<Col span={24}>
<h6 className="tw-heading tw-text-base" data-testid="header">
{`Add ${isColumnFqn ? 'Column' : 'Table'} Test`}
</h6>
</Col>
<Col span={24}>
<IngestionStepper
activeStep={activeServiceStep}
steps={STEPS_FOR_ADD_TEST_CASE}
/>
</Col>
<Col span={24}>{RenderSelectedTab()}</Col>
</Row>
)}
</PageLayout>
);
};
export default AddDataQualityTestV1;

View File

@ -0,0 +1,176 @@
/*
* Copyright 2022 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 { Col, Row } from 'antd';
import { AxiosError } from 'axios';
import { camelCase } from 'lodash';
import React, { useMemo, useState } from 'react';
import { useHistory } from 'react-router-dom';
import {
addIngestionPipeline,
deployIngestionPipelineById,
} from '../../axiosAPIs/ingestionPipelineAPI';
import {
DEPLOYED_PROGRESS_VAL,
INGESTION_PROGRESS_END_VAL,
} from '../../constants/constants';
import { FormSubmitType } from '../../enums/form.enum';
import { IngestionActionMessage } from '../../enums/ingestion.enum';
import {
ConfigType,
CreateIngestionPipeline,
PipelineType,
} from '../../generated/api/services/ingestionPipelines/createIngestionPipeline';
import { IngestionPipeline } from '../../generated/entity/services/ingestionPipelines/ingestionPipeline';
import jsonData from '../../jsons/en';
import { getSettingPath } from '../../utils/RouterUtils';
import { showErrorToast } from '../../utils/ToastUtils';
import SuccessScreen from '../common/success-screen/SuccessScreen';
import DeployIngestionLoaderModal from '../Modals/DeployIngestionLoaderModal/DeployIngestionLoaderModal';
import { TestSuiteIngestionProps } from './AddDataQualityTest.interface';
import TestSuiteScheduler from './components/TestSuiteScheduler';
const TestSuiteIngestion: React.FC<TestSuiteIngestionProps> = ({
testSuite,
onCancel,
}) => {
const history = useHistory();
const [ingestionData, setIngestionData] = useState<IngestionPipeline>();
const [showDeployModal, setShowDeployModal] = useState(false);
const [showDeployButton, setShowDeployButton] = useState(false);
const [ingestionAction, setIngestionAction] = useState(
IngestionActionMessage.CREATING
);
const [isIngestionDeployed, setIsIngestionDeployed] = useState(false);
const [isIngestionCreated, setIsIngestionCreated] = useState(false);
const [ingestionProgress, setIngestionProgress] = useState(0);
const getSuccessMessage = useMemo(() => {
const createMessage = showDeployButton
? 'has been created, but failed to deploy'
: 'has been created and deployed successfully';
return (
<span>
<span className="tw-mr-1 tw-font-semibold">
&quot;{ingestionData?.name ?? 'Test Suite'}&quot;
</span>
<span>{createMessage}</span>
</span>
);
}, [ingestionData, showDeployButton]);
const handleIngestionDeploy = (id?: string) => {
setShowDeployModal(true);
return new Promise<void>((resolve) => {
setIsIngestionCreated(true);
setIngestionProgress(INGESTION_PROGRESS_END_VAL);
setIngestionAction(IngestionActionMessage.DEPLOYING);
deployIngestionPipelineById(`${id || ingestionData?.id}`)
.then(() => {
setIsIngestionDeployed(true);
setShowDeployButton(false);
setIngestionProgress(DEPLOYED_PROGRESS_VAL);
setIngestionAction(IngestionActionMessage.DEPLOYED);
})
.catch((err: AxiosError) => {
setShowDeployButton(true);
setIngestionAction(IngestionActionMessage.DEPLOYING_ERROR);
showErrorToast(
err || jsonData['api-error-messages']['deploy-ingestion-error']
);
})
.finally(() => resolve());
});
};
const createIngestionPipeline = async (repeatFrequency: string) => {
const ingestionPayload: CreateIngestionPipeline = {
airflowConfig: {
scheduleInterval: repeatFrequency,
},
name: `${testSuite.name}_${PipelineType.TestSuite}`,
pipelineType: PipelineType.TestSuite,
service: {
id: testSuite.id || '',
type: camelCase(PipelineType.TestSuite),
},
sourceConfig: {
config: {
type: ConfigType.TestSuite,
},
},
};
const ingestion = await addIngestionPipeline(ingestionPayload);
setIngestionData(ingestion);
handleIngestionDeploy(ingestion.id).finally(() =>
setShowDeployModal(false)
);
};
const handleViewTestSuiteClick = () => {
history.push(getSettingPath());
};
const handleDeployClick = () => {
setShowDeployModal(true);
handleIngestionDeploy?.().finally(() => {
setTimeout(() => setShowDeployModal(false), 500);
});
};
return (
<Row className="tw-form-container" gutter={[16, 16]}>
<Col span={24}>
<h6 className="tw-heading tw-text-base" data-testid="header">
Schedule Ingestion
</h6>
</Col>
<Col span={24}>
{isIngestionCreated ? (
<SuccessScreen
isAirflowSetup
handleDeployClick={handleDeployClick}
handleViewServiceClick={handleViewTestSuiteClick}
name={`${testSuite?.name}_${PipelineType.TestSuite}`}
showDeployButton={showDeployButton}
showIngestionButton={false}
state={FormSubmitType.ADD}
successMessage={getSuccessMessage}
viewServiceText="View Test Suite"
/>
) : (
<TestSuiteScheduler
onCancel={onCancel}
onSubmit={createIngestionPipeline}
/>
)}
</Col>
{showDeployModal && (
<DeployIngestionLoaderModal
action={ingestionAction}
ingestionName={ingestionData?.name || ''}
isDeployed={isIngestionDeployed}
isIngestionCreated={isIngestionCreated}
progress={ingestionProgress}
/>
)}
</Row>
);
};
export default TestSuiteIngestion;

View File

@ -0,0 +1,135 @@
/*
* Copyright 2022 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 { PlusOutlined } from '@ant-design/icons';
import { Button, Form, Input, InputNumber, Switch } from 'antd';
import 'codemirror/addon/fold/foldgutter.css';
import React from 'react';
import {
TestCaseParameterDefinition,
TestDataType,
} from '../../../generated/tests/testDefinition';
import SVGIcons, { Icons } from '../../../utils/SvgUtils';
import '../../TableProfiler/tableProfiler.less';
import { ParameterFormProps } from '../AddDataQualityTest.interface';
const ParameterForm: React.FC<ParameterFormProps> = ({ definition }) => {
const prepareForm = (data: TestCaseParameterDefinition) => {
let Field = <Input placeholder={`Enter ${data.displayName}`} />;
switch (data.dataType) {
case TestDataType.String:
Field = <Input placeholder={`Enter ${data.displayName}`} />;
break;
case TestDataType.Number:
case TestDataType.Int:
case TestDataType.Decimal:
case TestDataType.Double:
case TestDataType.Float:
Field = (
<InputNumber
className="tw-w-full"
placeholder={`Enter ${data.displayName}`}
/>
);
break;
case TestDataType.Boolean:
Field = <Switch size="small" />;
break;
case TestDataType.Array:
case TestDataType.Set:
Field = (
<Input placeholder={`Enter comma(,) separated ${data.displayName}`} />
);
return (
<Form.List
initialValue={[{ value: '' }]}
key={data.name}
name={data.name || ''}>
{(fields, { add, remove }) => (
<Form.Item
key={data.name}
label={
<span>
<span className="tw-mr-3">{data.displayName}:</span>
<Button
icon={<PlusOutlined />}
size="small"
type="primary"
onClick={() => add()}
/>
</span>
}
name={data.name}
tooltip={data.description}>
{fields.map(({ key, name, ...restField }) => (
<div className="tw-flex tw-gap-2 tw-w-full" key={key}>
<Form.Item
className="tw-w-11/12 tw-mb-4"
{...restField}
name={[name, 'value']}
rules={[
{
required: data.required,
message: `${data.displayName} is required!`,
},
]}>
<Input placeholder={`Enter ${data.displayName}`} />
</Form.Item>
<Button
icon={
<SVGIcons
alt="delete"
className="tw-w-4"
icon={Icons.DELETE}
/>
}
type="text"
onClick={() => remove(name)}
/>
</div>
))}
</Form.Item>
)}
</Form.List>
);
}
return (
<Form.Item
key={data.name}
label={`${data.displayName}:`}
name={data.name}
rules={[
{
required: data.required,
message: `${data.displayName} is required!`,
},
]}
tooltip={data.description}>
{Field}
</Form.Item>
);
};
return (
<Form.List name="params">
{() => definition.parameterDefinition?.map(prepareForm)}
</Form.List>
);
};
export default ParameterForm;

View File

@ -0,0 +1,29 @@
/*
* Copyright 2022 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 { Row, Typography } from 'antd';
import React from 'react';
import { RightPanelProps } from '../AddDataQualityTest.interface';
const RightPanel: React.FC<RightPanelProps> = ({ data }) => {
return (
<Row>
<h6 className="tw-heading tw-text-base" data-testid="right-panel-header">
{data.title}
</h6>
<Typography.Paragraph>{data.body}</Typography.Paragraph>
</Row>
);
};
export default RightPanel;

View File

@ -0,0 +1,196 @@
/*
* Copyright 2022 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 {
Button,
Divider,
Form,
FormProps,
Input,
Row,
Select,
Space,
Typography,
} from 'antd';
import { AxiosError } from 'axios';
import { isEmpty } from 'lodash';
import { EditorContentRef } from 'Models';
import React, { useEffect, useRef, useState } from 'react';
import { useHistory, useParams } from 'react-router-dom';
import { getListTestSuites } from '../../../axiosAPIs/testAPI';
import {
API_RES_MAX_SIZE,
getTableTabPath,
} from '../../../constants/constants';
import { TestSuite } from '../../../generated/tests/testSuite';
import SVGIcons, { Icons } from '../../../utils/SvgUtils';
import { showErrorToast } from '../../../utils/ToastUtils';
import RichTextEditor from '../../common/rich-text-editor/RichTextEditor';
import {
SelectTestSuiteProps,
SelectTestSuiteType,
} from '../AddDataQualityTest.interface';
const SelectTestSuite: React.FC<SelectTestSuiteProps> = ({
onSubmit,
initialValue,
}) => {
const { entityTypeFQN } = useParams<Record<string, string>>();
const history = useHistory();
const [isNewTestSuite, setIsNewTestSuite] = useState(
initialValue?.isNewTestSuite ?? false
);
const [testSuites, setTestSuites] = useState<TestSuite[]>([]);
const markdownRef = useRef<EditorContentRef>();
const fetchAllTestSuite = async () => {
try {
const { data } = await getListTestSuites({
limit: API_RES_MAX_SIZE,
});
setTestSuites(data);
} catch (error) {
showErrorToast(error as AxiosError);
}
};
const getDescription = () => {
return markdownRef.current?.getEditorContent() || '';
};
const handleCancelClick = () => {
history.push(getTableTabPath(entityTypeFQN));
};
const handleFormSubmit: FormProps['onFinish'] = (value) => {
const data: SelectTestSuiteType = {
name: value.testSuiteName,
description: getDescription(),
data: testSuites.find((suite) => suite.id === value.testSuiteId),
isNewTestSuite,
};
onSubmit(data);
};
useEffect(() => {
if (testSuites.length === 0) {
fetchAllTestSuite();
}
}, []);
return (
<Form
initialValues={{
testSuiteId: initialValue?.data?.id,
testSuiteName: initialValue?.name,
}}
layout="vertical"
name="selectTestSuite"
onFinish={handleFormSubmit}>
<Form.Item
label="Test Suite:"
name="testSuiteId"
rules={[
{ required: !isNewTestSuite, message: 'Test suite is required' },
]}>
<Select
disabled={isNewTestSuite}
options={testSuites.map((suite) => ({
label: suite.name,
value: suite.id,
}))}
placeholder="Select test suite"
/>
</Form.Item>
<Divider plain>OR</Divider>
{isNewTestSuite ? (
<>
<Typography.Paragraph className="tw-text-base tw-mt-5">
New Test Suite
</Typography.Paragraph>
<Form.Item
label="Name:"
name="testSuiteName"
rules={[
{
required: isNewTestSuite,
message: 'Name is required!',
},
{
validator: (_, value) => {
if (testSuites.some((suite) => suite.name === value)) {
return Promise.reject('Name already exist!');
}
return Promise.resolve();
},
},
]}>
<Input placeholder="Enter test suite name" />
</Form.Item>
<Form.Item
label="Description:"
name="description"
rules={[
{
required: true,
validator: () => {
if (isEmpty(getDescription())) {
return Promise.reject('Description is required!');
}
return Promise.resolve();
},
},
]}>
<RichTextEditor
initialValue={initialValue?.description || ''}
ref={markdownRef}
style={{
margin: 0,
}}
/>
</Form.Item>
</>
) : (
<Row className="tw-mb-10" justify="center">
<Button
icon={
<SVGIcons
alt="plus"
className="tw-w-4 tw-mr-1"
icon={Icons.ICON_PLUS_PRIMERY}
/>
}
onClick={() => setIsNewTestSuite(true)}>
<span className="tw-text-primary">Create new test suite</span>
</Button>
</Row>
)}
<Form.Item noStyle>
<Space className="tw-w-full tw-justify-end" size={16}>
<Button onClick={handleCancelClick}>Cancel</Button>
<Button htmlType="submit" type="primary">
Next
</Button>
</Space>
</Form.Item>
</Form>
);
};
export default SelectTestSuite;

View File

@ -0,0 +1,265 @@
/*
* Copyright 2022 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 { Button, Col, Form, FormProps, Input, Row, Select, Space } from 'antd';
import { AxiosError } from 'axios';
import 'codemirror/addon/fold/foldgutter.css';
import { isEmpty } from 'lodash';
import { EditorContentRef } from 'Models';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { Controlled as CodeMirror } from 'react-codemirror2';
import { useParams } from 'react-router-dom';
import {
getListTestCase,
getListTestDefinitions,
} from '../../../axiosAPIs/testAPI';
import { API_RES_MAX_SIZE } from '../../../constants/constants';
import { codeMirrorOption } from '../../../constants/profiler.constant';
import { ProfilerDashboardType } from '../../../enums/table.enum';
import { EntityReference } from '../../../generated/entity/services/ingestionPipelines/ingestionPipeline';
import {
TestCase,
TestCaseParameterValue,
} from '../../../generated/tests/testCase';
import {
EntityType,
TestDataType,
TestDefinition,
} from '../../../generated/tests/testDefinition';
import { generateEntityLink } from '../../../utils/TableUtils';
import { showErrorToast } from '../../../utils/ToastUtils';
import RichTextEditor from '../../common/rich-text-editor/RichTextEditor';
import { TestCaseFormProps } from '../AddDataQualityTest.interface';
import ParameterForm from './ParameterForm';
const TestCaseForm: React.FC<TestCaseFormProps> = ({
initialValue,
onSubmit,
onCancel,
}) => {
const { entityTypeFQN, dashboardType } = useParams<Record<string, string>>();
const isColumnFqn = dashboardType === ProfilerDashboardType.COLUMN;
const [form] = Form.useForm();
const markdownRef = useRef<EditorContentRef>();
const [testDefinitions, setTestDefinitions] = useState<TestDefinition[]>([]);
const [selectedTestType, setSelectedTestType] = useState<string | undefined>(
initialValue?.testDefinition?.id
);
const [testCases, setTestCases] = useState<{ [key: string]: TestCase }>({});
const [sqlQuery, setSqlQuery] = useState({
name: 'sqlExpression',
value: initialValue?.parameterValues?.[0].value || '',
});
const fetchAllTestDefinitions = async () => {
try {
const { data } = await getListTestDefinitions({
limit: API_RES_MAX_SIZE,
entityType: isColumnFqn ? EntityType.Column : EntityType.Table,
});
setTestDefinitions(data);
} catch (error) {
showErrorToast(error as AxiosError);
}
};
const fetchAllTestCases = async () => {
try {
const { data } = await getListTestCase({
fields: 'testDefinition',
limit: API_RES_MAX_SIZE,
entityLink: generateEntityLink(entityTypeFQN, isColumnFqn),
});
const modifiedData = data.reduce((acc, curr) => {
return { ...acc, [curr.testDefinition.fullyQualifiedName || '']: curr };
}, {});
setTestCases(modifiedData);
} catch (error) {
showErrorToast(error as AxiosError);
}
};
const GenerateParamsField = useCallback(() => {
const testType = initialValue?.testDefinition?.id ?? selectedTestType;
const selectedDefinition = testDefinitions.find(
(definition) => definition.id === testType
);
if (selectedDefinition && selectedDefinition.parameterDefinition) {
const name = selectedDefinition.parameterDefinition[0].name;
if (name === 'sqlExpression') {
return (
<Row>
<Col data-testid="sql-editor-container" span={24}>
<p className="tw-mb-1.5">Profile Sample Query</p>
<CodeMirror
className="profiler-setting-sql-editor"
data-testid="profiler-setting-sql-editor"
options={codeMirrorOption}
value={sqlQuery.value}
onBeforeChange={(_Editor, _EditorChange, value) => {
setSqlQuery((pre) => ({ ...pre, value }));
}}
onChange={(_Editor, _EditorChange, value) => {
setSqlQuery((pre) => ({ ...pre, value }));
}}
/>
</Col>
</Row>
);
}
return <ParameterForm definition={selectedDefinition} />;
}
return;
}, [selectedTestType, initialValue, testDefinitions, sqlQuery]);
const createTestCaseObj = (value: {
testName: string;
params: Record<string, string | { [key: string]: string }[]>;
testTypeId: string;
}) => {
const testType = initialValue?.testDefinition?.id ?? selectedTestType;
const selectedDefinition = testDefinitions.find(
(definition) => definition.id === testType
);
const paramsValue = selectedDefinition?.parameterDefinition?.[0];
const parameterValues =
paramsValue?.name === 'sqlExpression'
? [sqlQuery]
: Object.entries(value.params || {}).map(([key, value]) => ({
name: key,
value:
paramsValue?.dataType === TestDataType.Array
? // need to send array as string formate
JSON.stringify(
(value as { value: string }[]).map((data) => data.value)
)
: value,
}));
return {
name: value.testName,
entityLink: generateEntityLink(entityTypeFQN, isColumnFqn),
parameterValues: parameterValues as TestCaseParameterValue[],
testDefinition: {
id: value.testTypeId,
type: 'testDefinition',
},
description: markdownRef.current?.getEditorContent(),
testSuite: {} as EntityReference,
};
};
const handleFormSubmit: FormProps['onFinish'] = (value) => {
onSubmit(createTestCaseObj(value));
};
const onBack = () => {
const data = form.getFieldsValue();
onCancel(createTestCaseObj(data));
};
useEffect(() => {
if (testDefinitions.length === 0) {
fetchAllTestDefinitions();
}
if (isEmpty(testCases)) {
fetchAllTestCases();
}
}, []);
return (
<Form
form={form}
initialValues={{
testName: initialValue?.name,
testTypeId: initialValue?.testDefinition?.id,
params: initialValue?.parameterValues?.reduce(
(acc, curr) => ({
...acc,
[curr.name || '']: curr.value,
}),
{}
),
}}
layout="vertical"
name="tableTestForm"
onFinish={handleFormSubmit}
onValuesChange={(value) => {
if (value.testTypeId) {
setSelectedTestType(value.testTypeId);
}
}}>
<Form.Item
label="Name:"
name="testName"
rules={[
{
required: true,
message: 'Name is required!',
},
{
validator: (_, value) => {
if (
Object.values(testCases).some((suite) => suite.name === value)
) {
return Promise.reject('Name already exist!');
}
return Promise.resolve();
},
},
]}>
<Input placeholder="Enter test case name" />
</Form.Item>
<Form.Item
label="Test Type:"
name="testTypeId"
rules={[{ required: true, message: 'Test type is required' }]}>
<Select
options={testDefinitions.map((suite) => ({
label: suite.name,
value: suite.id,
}))}
placeholder="Select test type"
/>
</Form.Item>
{GenerateParamsField()}
<Form.Item label="Description:" name="description">
<RichTextEditor
initialValue={initialValue?.description || ''}
ref={markdownRef}
style={{
margin: 0,
}}
/>
</Form.Item>
<Form.Item noStyle>
<Space className="tw-w-full tw-justify-end" size={16}>
<Button onClick={onBack}>Back</Button>
<Button htmlType="submit" type="primary">
Submit
</Button>
</Space>
</Form.Item>
</Form>
);
};
export default TestCaseForm;

View File

@ -0,0 +1,45 @@
/*
* Copyright 2022 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 { Button, Col, Row, Space } from 'antd';
import React, { useState } from 'react';
import CronEditor from '../../common/CronEditor/CronEditor';
import { TestSuiteSchedulerProps } from '../AddDataQualityTest.interface';
const TestSuiteScheduler: React.FC<TestSuiteSchedulerProps> = ({
onCancel,
onSubmit,
}) => {
const [repeatFrequency, setRepeatFrequency] = useState('');
return (
<Row gutter={[16, 32]}>
<Col span={24}>
<CronEditor
value={repeatFrequency}
onChange={(value: string) => setRepeatFrequency(value)}
/>
</Col>
<Col span={24}>
<Space className="tw-w-full tw-justify-end" size={16}>
<Button onClick={onCancel}>Back</Button>
<Button type="primary" onClick={() => onSubmit(repeatFrequency)}>
Submit
</Button>
</Space>
</Col>
</Row>
);
};
export default TestSuiteScheduler;

View File

@ -0,0 +1,32 @@
/*
* Copyright 2022 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.
*/
export const TEST_FORM_DATA = [
{
title: 'Select/Create Test Suite',
body: 'Select existing test suite or Create new Test Suite',
},
{
title: 'Test Case',
body: 'Fill up the relevant details and create test case',
},
{
title: 'Test case created successfully',
body: 'Visit the newly created test case to take a look at the details. Ensure that you have Airflow set up correctly before heading to ingest metadata.',
},
];
export const INGESTION_DATA = {
title: 'Schedule for Ingestion',
body: 'Scheduling can be set up at an hourly, daily, or weekly cadence. The timezone is in UTC.',
};

View File

@ -29,8 +29,10 @@ import { ProfilerDashboardType } from '../../../enums/table.enum';
import { Column, ColumnProfile } from '../../../generated/entity/data/table';
import { formatNumberWithComma } from '../../../utils/CommonUtils';
import { updateTestResults } from '../../../utils/DataQualityAndProfilerUtils';
import { getCurrentDatasetTab } from '../../../utils/DatasetDetailsUtils';
import { getProfilerDashboardWithFqnPath } from '../../../utils/RouterUtils';
import {
getAddDataQualityTableTestPath,
getProfilerDashboardWithFqnPath,
} from '../../../utils/RouterUtils';
import Ellipses from '../../common/Ellipses/Ellipses';
import Searchbar from '../../common/searchbar/Searchbar';
import TestIndicator from '../../common/TestIndicator/TestIndicator';
@ -42,7 +44,6 @@ import ProfilerProgressWidget from './ProfilerProgressWidget';
const ColumnProfileTable: FC<ColumnProfileTableProps> = ({
columnTests,
onAddTestClick,
columns = [],
}) => {
const [searchText, setSearchText] = useState<string>('');
@ -89,7 +90,7 @@ const ColumnProfileTable: FC<ColumnProfileTableProps> = ({
return (
<ProfilerProgressWidget
strokeColor={PRIMERY_COLOR}
value={profile.nullProportion || 0}
value={profile?.nullProportion || 0}
/>
);
},
@ -102,7 +103,7 @@ const ColumnProfileTable: FC<ColumnProfileTableProps> = ({
render: (profile: ColumnProfile) => (
<ProfilerProgressWidget
strokeColor={SECONDARY_COLOR}
value={profile.uniqueProportion || 0}
value={profile?.uniqueProportion || 0}
/>
),
},
@ -114,7 +115,7 @@ const ColumnProfileTable: FC<ColumnProfileTableProps> = ({
render: (profile: ColumnProfile) => (
<ProfilerProgressWidget
strokeColor={SUCCESS_COLOR}
value={profile.distinctProportion || 0}
value={profile?.distinctProportion || 0}
/>
),
},
@ -123,7 +124,7 @@ const ColumnProfileTable: FC<ColumnProfileTableProps> = ({
dataIndex: 'profile',
key: 'valuesCount',
render: (profile: ColumnProfile) =>
formatNumberWithComma(profile.valuesCount || 0),
formatNumberWithComma(profile?.valuesCount || 0),
},
{
title: 'Test',
@ -152,18 +153,17 @@ const ColumnProfileTable: FC<ColumnProfileTableProps> = ({
dataIndex: 'actions',
key: 'actions',
render: (_, record) => (
<Button
className="tw-border tw-border-primary tw-rounded tw-text-primary"
size="small"
onClick={() =>
onAddTestClick(
getCurrentDatasetTab('data-quality'),
'column',
record.name
)
}>
Add Test
</Button>
<Link
to={getAddDataQualityTableTestPath(
ProfilerDashboardType.COLUMN,
record.fullyQualifiedName || ''
)}>
<Button
className="tw-border tw-border-primary tw-rounded tw-text-primary"
size="small">
Add Test
</Button>
</Link>
),
},
];

View File

@ -21,7 +21,6 @@ import {
} from '@testing-library/react';
import React from 'react';
import { MOCK_TABLE, TEST_CASE } from '../../mocks/TableData.mock';
import { getCurrentDatasetTab } from '../../utils/DatasetDetailsUtils';
import { TableProfilerProps } from './TableProfiler.interface';
// internal imports
import TableProfilerV1 from './TableProfilerV1';
@ -70,7 +69,6 @@ jest.mock('../../utils/CommonUtils', () => ({
formatNumberWithComma: jest.fn(),
formTwoDigitNmber: jest.fn(),
}));
const mockGetCurrentDatasetTab = getCurrentDatasetTab as jest.Mock;
jest.mock('../../axiosAPIs/testAPI', () => ({
getListTestCase: jest
@ -127,13 +125,6 @@ describe('Test TableProfiler component', () => {
);
expect(addTableTest).toBeInTheDocument();
await act(async () => {
fireEvent.click(addTableTest);
});
expect(mockProps.onAddTestClick).toHaveBeenCalledTimes(1);
expect(mockGetCurrentDatasetTab).toHaveBeenCalledTimes(1);
});
it('CTA: Setting button should work properly', async () => {

View File

@ -27,8 +27,10 @@ import {
formTwoDigitNmber,
} from '../../utils/CommonUtils';
import { updateTestResults } from '../../utils/DataQualityAndProfilerUtils';
import { getCurrentDatasetTab } from '../../utils/DatasetDetailsUtils';
import { getProfilerDashboardWithFqnPath } from '../../utils/RouterUtils';
import {
getAddDataQualityTableTestPath,
getProfilerDashboardWithFqnPath,
} from '../../utils/RouterUtils';
import SVGIcons, { Icons } from '../../utils/SvgUtils';
import { generateEntityLink } from '../../utils/TableUtils';
import { showErrorToast } from '../../utils/ToastUtils';
@ -156,15 +158,18 @@ const TableProfilerV1: FC<TableProfilerProps> = ({ table, onAddTestClick }) => {
className="table-profiler-container"
data-testid="table-profiler-container">
<div className="tw-flex tw-justify-end tw-gap-4 tw-mb-4">
<Button
className="tw-rounded"
data-testid="profiler-add-table-test-btn"
type="primary"
onClick={() =>
onAddTestClick(getCurrentDatasetTab('data-quality'), 'table')
}>
Add Test
</Button>
<Link
to={getAddDataQualityTableTestPath(
ProfilerDashboardType.TABLE,
table.fullyQualifiedName || ''
)}>
<Button
className="tw-rounded"
data-testid="profiler-add-table-test-btn"
type="primary">
Add Test
</Button>
</Link>
<Button
className="profiler-setting-btn tw-border tw-border-primary tw-rounded tw-text-primary"
data-testid="profiler-setting-btn"

View File

@ -31,6 +31,7 @@ type SuccessScreenProps = {
showDeployButton?: boolean;
state: FormSubmitType;
isAirflowSetup: boolean;
viewServiceText?: string;
handleIngestionClick?: () => void;
handleViewServiceClick: () => void;
handleDeployClick?: () => void;
@ -47,6 +48,7 @@ const SuccessScreen = ({
handleViewServiceClick,
handleDeployClick,
successMessage,
viewServiceText,
onCheckAirflowStatus,
}: SuccessScreenProps) => {
const [airflowCheckState, setAirflowCheckState] =
@ -182,7 +184,7 @@ const SuccessScreen = ({
theme="primary"
variant="outlined"
onClick={handleViewServiceClick}>
<span>View Service</span>
<span>{viewServiceText ?? 'View Service'}</span>
</Button>
{showIngestionButton && (

View File

@ -222,6 +222,7 @@ export const ROUTES = {
CUSTOM_ENTITY_DETAIL: `/custom-properties/${PLACEHOLDER_ENTITY_TYPE_FQN}`,
ADD_CUSTOM_PROPERTY: `/custom-properties/${PLACEHOLDER_ENTITY_TYPE_FQN}/add-field`,
PROFILER_DASHBOARD: `/profiler-dashboard/${PLACEHOLDER_DASHBOARD_TYPE}/${PLACEHOLDER_ENTITY_TYPE_FQN}`,
ADD_DATA_QUALITY_TEST_CASE: `/data-quality-test/${PLACEHOLDER_DASHBOARD_TYPE}/${PLACEHOLDER_ENTITY_TYPE_FQN}`,
// Tasks Routes
REQUEST_DESCRIPTION: `/request-description/${PLACEHOLDER_ROUTE_ENTITY_TYPE}/${PLACEHOLDER_ROUTE_ENTITY_FQN}`,

View File

@ -11,6 +11,7 @@
* limitations under the License.
*/
import { StepperStepType } from 'Models';
import { CSMode } from '../enums/codemirror.enum';
import { ColumnProfilerConfig } from '../generated/entity/data/table';
import { TestCaseStatus } from '../generated/tests/tableTest';
@ -200,3 +201,9 @@ export const codeMirrorOption = {
name: CSMode.SQL,
},
};
export const STEPS_FOR_ADD_TEST_CASE: Array<StepperStepType> = [
{ name: 'Select/Add Test Suite', step: 1 },
{ name: 'Configure Test Case', step: 2 },
// { name: 'Schedule Interval', step: 3 },
];

View File

@ -0,0 +1,55 @@
/*
* Copyright 2022 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 { AxiosError } from 'axios';
import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { getTableDetailsByFQN } from '../../axiosAPIs/tableAPI';
import AddDataQualityTestV1 from '../../components/AddDataQualityTest/AddDataQualityTestV1';
import PageContainerV1 from '../../components/containers/PageContainerV1';
import { ProfilerDashboardType } from '../../enums/table.enum';
import { Table } from '../../generated/entity/data/table';
import { getTableFQNFromColumnFQN } from '../../utils/CommonUtils';
import { showErrorToast } from '../../utils/ToastUtils';
const AddDataQualityTestPage = () => {
const { entityTypeFQN, dashboardType } = useParams<Record<string, string>>();
const isColumnFqn = dashboardType === ProfilerDashboardType.COLUMN;
const [table, setTable] = useState({} as Table);
const fetchTableData = async () => {
try {
const fqn = isColumnFqn
? getTableFQNFromColumnFQN(entityTypeFQN)
: entityTypeFQN;
const table = await getTableDetailsByFQN(fqn, '');
setTable(table);
} catch (error) {
showErrorToast(error as AxiosError);
}
};
useEffect(() => {
fetchTableData();
}, [entityTypeFQN]);
return (
<PageContainerV1>
<div className="tw-self-center">
<AddDataQualityTestV1 table={table} />
</div>
</PageContainerV1>
);
};
export default AddDataQualityTestPage;

View File

@ -18,6 +18,7 @@ import AppState from '../AppState';
import { usePermissionProvider } from '../components/PermissionProvider/PermissionProvider';
import { ResourceEntity } from '../components/PermissionProvider/PermissionProvider.interface';
import { ROUTES } from '../constants/constants';
import AddDataQualityTestPage from '../pages/AddDataQualityTestPage/AddDataQualityTestPage';
import { Operation } from '../generated/entity/policies/policy';
import { checkPermission } from '../utils/PermissionsUtils';
import AdminProtectedRoute from './AdminProtectedRoute';
@ -291,6 +292,11 @@ const AuthenticatedAppRouter: FunctionComponent = () => {
<Route exact component={UserPage} path={ROUTES.USER_PROFILE} />
<Route exact component={UserPage} path={ROUTES.USER_PROFILE_WITH_TAB} />
<Route exact component={MlModelPage} path={ROUTES.MLMODEL_DETAILS} />
<Route
exact
component={AddDataQualityTestPage}
path={ROUTES.ADD_DATA_QUALITY_TEST_CASE}
/>
<Route
exact
component={ProfilerDashboardPage}

View File

@ -330,3 +330,16 @@ export const getTagPath = (fqn?: string) => {
return path;
};
export const getAddDataQualityTableTestPath = (
dashboardType: string,
fqn: string
) => {
let path = ROUTES.ADD_DATA_QUALITY_TEST_CASE;
path = path
.replace(PLACEHOLDER_DASHBOARD_TYPE, dashboardType)
.replace(PLACEHOLDER_ENTITY_TYPE_FQN, fqn);
return path;
};