mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-12-24 22:18:41 +00:00
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:
parent
cc0449e506
commit
3b67cc824d
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -306,7 +306,7 @@ class OMetaServiceTest(TestCase):
|
||||
),
|
||||
service=EntityReference(
|
||||
id=self.test_suite.id,
|
||||
type="TestSuite",
|
||||
type="testSuite",
|
||||
name=self.test_suite.name.__root__,
|
||||
),
|
||||
)
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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">
|
||||
"{successName}"
|
||||
</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;
|
||||
@ -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">
|
||||
"{ingestionData?.name ?? 'Test Suite'}"
|
||||
</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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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.',
|
||||
};
|
||||
@ -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>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -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}`,
|
||||
|
||||
@ -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 },
|
||||
];
|
||||
|
||||
@ -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;
|
||||
@ -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}
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user