Fixed issue-3325 Data Quality - columnValuesMissingCountToBeEqual UI, 3338: Add Ingestion Modal are missing Snowflake (#3351)

* added support to accept multiple value for missingValueMatch in columnValuesMissingCountToBeEqual test
This commit is contained in:
Shailesh Parmar 2022-03-10 20:24:59 +05:30 committed by GitHub
parent 020613a6da
commit f03fb1cafa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 167 additions and 127 deletions

View File

@ -95,8 +95,8 @@ const ColumnTestForm = ({
}); });
const [columnName, setColumnName] = useState(data?.columnName); const [columnName, setColumnName] = useState(data?.columnName);
const [missingValueMatch, setMissingValueMatch] = useState<string>( const [missingValueMatch, setMissingValueMatch] = useState<string[]>(
data?.testCase?.config?.missingValueMatch || '' data?.testCase?.config?.missingValueMatch || ['']
); );
const [missingCountValue, setMissingCountValue] = useState< const [missingCountValue, setMissingCountValue] = useState<
number | undefined number | undefined
@ -123,6 +123,22 @@ const ColumnTestForm = ({
setIsShowError({ ...isShowError, values: false }); setIsShowError({ ...isShowError, values: false });
}; };
const addMatchFields = () => {
setMissingValueMatch([...missingValueMatch, '']);
};
const removeMatchFields = (i: number) => {
const newFormValues = [...missingValueMatch];
newFormValues.splice(i, 1);
setMissingValueMatch(newFormValues);
};
const handlMatchFieldsChange = (i: number, value: string) => {
const newFormValues = [...missingValueMatch];
newFormValues[i] = value;
setMissingValueMatch(newFormValues);
};
const setAllTestOption = (datatype: string) => { const setAllTestOption = (datatype: string) => {
const newTest = filteredColumnTestOption(datatype); const newTest = filteredColumnTestOption(datatype);
setTestTypeOptions(newTest); setTestTypeOptions(newTest);
@ -225,14 +241,18 @@ const ColumnTestForm = ({
maxValue: !isEmpty(maxValue) ? maxValue : undefined, maxValue: !isEmpty(maxValue) ? maxValue : undefined,
}; };
case ColumnTestType.columnValuesMissingCountToBeEqual: case ColumnTestType.columnValuesMissingCountToBeEqual: {
const filterMatchValue = missingValueMatch.filter(
(value) => !isEmpty(value)
);
return { return {
missingCountValue: missingCountValue, missingCountValue: missingCountValue,
missingValueMatch: !isEmpty(missingValueMatch) missingValueMatch: filterMatchValue.length
? missingValueMatch ? missingValueMatch
: undefined, : undefined,
}; };
}
case ColumnTestType.columnValuesToBeNotInSet: case ColumnTestType.columnValuesToBeNotInSet:
return { return {
forbiddenValues: forbiddenValues.filter((v) => !isEmpty(v)), forbiddenValues: forbiddenValues.filter((v) => !isEmpty(v)),
@ -344,11 +364,6 @@ const ColumnTestForm = ({
break; break;
} }
case 'missingValueMatch':
setMissingValueMatch(value);
break;
case 'missingCountValue': case 'missingCountValue':
setMissingCountValue(value as unknown as number); setMissingCountValue(value as unknown as number);
errorMsg.missingCountValue = false; errorMsg.missingCountValue = false;
@ -413,42 +428,67 @@ const ColumnTestForm = ({
const getMissingCountToBeEqualFields = () => { const getMissingCountToBeEqualFields = () => {
return ( return (
<Fragment> <Fragment>
<div className="tw-flex tw-gap-4 tw-w-full"> <Field>
<div className="tw-flex-1"> <label className="tw-block tw-form-label" htmlFor="missingCountValue">
<label {requiredField('Count:')}
className="tw-block tw-form-label" </label>
htmlFor="missingCountValue"> <input
{requiredField('Count:')} className="tw-form-inputs tw-px-3 tw-py-1"
</label> data-testid="missingCountValue"
<input id="missingCountValue"
className="tw-form-inputs tw-px-3 tw-py-1" name="missingCountValue"
data-testid="missingCountValue" placeholder="Missing count value"
id="missingCountValue" type="number"
name="missingCountValue" value={missingCountValue}
placeholder="Missing count value" onChange={handleValidation}
type="number" />
value={missingCountValue} {isShowError.missingCountValue &&
onChange={handleValidation} errorMsg('Count value is required.')}
/> </Field>
{isShowError.missingCountValue &&
errorMsg('Count value is required.')} <div data-testid="missing-count-to-be-equal">
</div> <div className="tw-flex tw-items-center tw-mt-6">
<div className="tw-flex-1"> <p className="w-form-label tw-mr-3">Match:</p>
<label <Button
className="tw-block tw-form-label" className="tw-h-5 tw-px-2"
htmlFor="missingValueMatch"> size="x-small"
Match: theme="primary"
</label> variant="contained"
<input onClick={addMatchFields}>
className="tw-form-inputs tw-px-3 tw-py-1" <i aria-hidden="true" className="fa fa-plus" />
data-testid="missingValueMatch" </Button>
id="missingValueMatch"
name="missingValueMatch"
placeholder="Missing value match"
value={missingValueMatch}
onChange={handleValidation}
/>
</div> </div>
{missingValueMatch.map((value, i) => (
<div className="tw-flex tw-items-center" key={i}>
<div className="tw-w-11/12">
<Field>
<input
className="tw-form-inputs tw-px-3 tw-py-1"
id={`value-key-${i}`}
name="key"
placeholder="Missing value to be match"
type="text"
value={value}
onChange={(e) => handlMatchFieldsChange(i, e.target.value)}
/>
</Field>
</div>
<button
className="focus:tw-outline-none tw-mt-3 tw-w-1/12"
onClick={(e) => {
e.preventDefault();
removeMatchFields(i);
}}>
<SVGIcons
alt="delete"
icon="icon-delete"
title="Delete"
width="12px"
/>
</button>
</div>
))}
</div> </div>
</Fragment> </Fragment>
); );
@ -552,10 +592,7 @@ const ColumnTestForm = ({
<p className="tw-font-medium tw-px-4"> <p className="tw-font-medium tw-px-4">
{isUndefined(data) ? 'Add' : 'Edit'} Column Test {isUndefined(data) ? 'Add' : 'Edit'} Column Test
</p> </p>
<form <div className="tw-w-screen-sm">
className="tw-w-screen-sm"
data-testid="form"
onSubmit={(e) => e.preventDefault()}>
<div className="tw-px-4 tw-mx-auto"> <div className="tw-px-4 tw-mx-auto">
<Field> <Field>
<label className="tw-block tw-form-label" htmlFor="columnName"> <label className="tw-block tw-form-label" htmlFor="columnName">
@ -663,7 +700,7 @@ const ColumnTestForm = ({
</Button> </Button>
</Field> </Field>
</Field> </Field>
</form> </div>
</div> </div>
); );
}; };

View File

@ -282,10 +282,7 @@ const TableTestForm = ({
{isUndefined(data) ? 'Add' : 'Edit'} Table Test {isUndefined(data) ? 'Add' : 'Edit'} Table Test
</p> </p>
<form <div className="tw-w-screen-sm">
className="tw-w-screen-sm"
data-testid="form"
onSubmit={(e) => e.preventDefault()}>
<div className="tw-px-4 tw-mx-auto"> <div className="tw-px-4 tw-mx-auto">
<Field> <Field>
<label className="tw-block tw-form-label" htmlFor="tableTestType"> <label className="tw-block tw-form-label" htmlFor="tableTestType">
@ -369,7 +366,7 @@ const TableTestForm = ({
Save Save
</Button> </Button>
</Field> </Field>
</form> </div>
</Fragment> </Fragment>
); );
}; };

View File

@ -36,6 +36,7 @@ type Props = {
tableTestCase: TableTest[]; tableTestCase: TableTest[];
handleRemoveTableTest: (testType: TableTestType) => void; handleRemoveTableTest: (testType: TableTestType) => void;
selectedColumn: string; selectedColumn: string;
handleSelectedColumn: (value: string | undefined) => void;
handleRemoveColumnTest: ( handleRemoveColumnTest: (
columnName: string, columnName: string,
testType: ColumnTestType testType: ColumnTestType
@ -51,6 +52,7 @@ const DataQualityTab = ({
handleAddColumnTestCase, handleAddColumnTestCase,
handleRemoveTableTest, handleRemoveTableTest,
handleRemoveColumnTest, handleRemoveColumnTest,
handleSelectedColumn,
testMode, testMode,
tableTestCase, tableTestCase,
selectedColumn, selectedColumn,
@ -61,6 +63,7 @@ const DataQualityTab = ({
const onFormCancel = () => { const onFormCancel = () => {
handleShowTestForm(false); handleShowTestForm(false);
setActiveData(undefined); setActiveData(undefined);
handleSelectedColumn(undefined);
}; };
const handleShowDropDown = (value: boolean) => { const handleShowDropDown = (value: boolean) => {

View File

@ -116,6 +116,7 @@ const DatasetDetails: React.FC<DatasetDetailsProps> = ({
handleRemoveTableTest, handleRemoveTableTest,
handleRemoveColumnTest, handleRemoveColumnTest,
qualityTestFormHandler, qualityTestFormHandler,
handleSelectedColumn,
selectedColumn, selectedColumn,
}: DatasetDetailsProps) => { }: DatasetDetailsProps) => {
const { isAuthDisabled } = useAuthContext(); const { isAuthDisabled } = useAuthContext();
@ -643,6 +644,7 @@ const DatasetDetails: React.FC<DatasetDetailsProps> = ({
handleAddTableTestCase={handleAddTableTestCase} handleAddTableTestCase={handleAddTableTestCase}
handleRemoveColumnTest={handleRemoveColumnTest} handleRemoveColumnTest={handleRemoveColumnTest}
handleRemoveTableTest={handleRemoveTableTest} handleRemoveTableTest={handleRemoveTableTest}
handleSelectedColumn={handleSelectedColumn}
handleShowTestForm={handleShowTestForm} handleShowTestForm={handleShowTestForm}
handleTestModeChange={handleTestModeChange} handleTestModeChange={handleTestModeChange}
selectedColumn={selectedColumn} selectedColumn={selectedColumn}

View File

@ -95,6 +95,7 @@ export interface DatasetDetailsProps {
columnsUpdateHandler: (updatedTable: Table) => void; columnsUpdateHandler: (updatedTable: Table) => void;
descriptionUpdateHandler: (updatedTable: Table) => void; descriptionUpdateHandler: (updatedTable: Table) => void;
versionHandler: () => void; versionHandler: () => void;
handleSelectedColumn: (value: string | undefined) => void;
loadNodeHandler: (node: EntityReference, pos: LineagePos) => void; loadNodeHandler: (node: EntityReference, pos: LineagePos) => void;
addLineageHandler: (edge: Edge) => Promise<void>; addLineageHandler: (edge: Edge) => Promise<void>;
removeLineageHandler: (data: EdgeData) => void; removeLineageHandler: (data: EdgeData) => void;

View File

@ -102,6 +102,7 @@ const DatasetDetailsProps = {
tableTestCase: [], tableTestCase: [],
selectedColumn: '', selectedColumn: '',
handleAddColumnTestCase: jest.fn(), handleAddColumnTestCase: jest.fn(),
handleSelectedColumn: jest.fn(),
createThread: jest.fn(), createThread: jest.fn(),
handleShowTestForm: jest.fn(), handleShowTestForm: jest.fn(),
handleRemoveTableTest: jest.fn(), handleRemoveTableTest: jest.fn(),

View File

@ -441,6 +441,7 @@ const Ingestion: React.FC<Props> = ({
name: s.name, name: s.name,
serviceType: s.serviceType, serviceType: s.serviceType,
}))} }))}
serviceType={serviceType}
type="" type=""
onCancel={() => setIsAdding(false)} onCancel={() => setIsAdding(false)}
/> />
@ -457,6 +458,7 @@ const Ingestion: React.FC<Props> = ({
name: s.name, name: s.name,
serviceType: s.serviceType, serviceType: s.serviceType,
}))} }))}
serviceType={serviceType}
updateIngestion={(data, triggerIngestion) => { updateIngestion={(data, triggerIngestion) => {
setIsUpdating(false); setIsUpdating(false);
handleUpdateIngestion(data, triggerIngestion); handleUpdateIngestion(data, triggerIngestion);

View File

@ -17,6 +17,7 @@ import { isEmpty } from 'lodash';
import { StepperStepType } from 'Models'; import { StepperStepType } from 'Models';
import { utc } from 'moment'; import { utc } from 'moment';
import React, { Fragment, ReactNode, useEffect, useState } from 'react'; import React, { Fragment, ReactNode, useEffect, useState } from 'react';
import { DatabaseServiceType } from '../../generated/entity/services/databaseService';
import { import {
AirflowPipeline, AirflowPipeline,
ConfigObject, ConfigObject,
@ -94,6 +95,7 @@ const getIngestionName = (name: string) => {
const IngestionModal: React.FC<IngestionModalProps> = ({ const IngestionModal: React.FC<IngestionModalProps> = ({
isUpdating, isUpdating,
header, header,
serviceType,
service = '', service = '',
ingestionTypes, ingestionTypes,
ingestionList, ingestionList,
@ -150,6 +152,9 @@ const IngestionModal: React.FC<IngestionModalProps> = ({
selectedIngestion?.scheduleInterval || '5 * * * *' selectedIngestion?.scheduleInterval || '5 * * * *'
); );
const [warehouse, setWarehouse] = useState(pipelineConfig.warehouse);
const [account, setAccount] = useState(pipelineConfig.account);
const [showErrorMsg, setShowErrorMsg] = useState<ValidationErrorMsg>({ const [showErrorMsg, setShowErrorMsg] = useState<ValidationErrorMsg>({
selectService: false, selectService: false,
name: false, name: false,
@ -284,6 +289,45 @@ const IngestionModal: React.FC<IngestionModalProps> = ({
errorMsg('Ingestion Type is required')} errorMsg('Ingestion Type is required')}
</Field> </Field>
{serviceType === DatabaseServiceType.Snowflake && (
<div className="tw-grid tw-grid-cols-2 tw-gap-x-4 tw-mt-6">
<div>
<label className="tw-block" htmlFor="warehouse">
Warehouse:
</label>
<input
className="tw-form-inputs tw-px-3 tw-py-1"
data-testid="warehouse"
id="warehouse"
name="warehouse"
placeholder="Warehouse"
type="text"
value={warehouse}
onChange={(e) => {
setWarehouse(e.target.value);
}}
/>
</div>
<div>
<label className="tw-block" htmlFor="account">
Account:
</label>
<input
className="tw-form-inputs tw-px-3 tw-py-1"
data-testid="account"
id="account"
name="account"
placeholder="Account"
type="text"
value={account}
onChange={(e) => {
setAccount(e.target.value);
}}
/>
</div>
</div>
)}
<Field> <Field>
{getSeparator('Table Filter Pattern')} {getSeparator('Table Filter Pattern')}
<div className="tw-grid tw-grid-cols-2 tw-gap-x-4 tw-mt-1"> <div className="tw-grid tw-grid-cols-2 tw-gap-x-4 tw-mt-1">

View File

@ -35,6 +35,7 @@ export interface IngestionModalProps {
schedule?: string; schedule?: string;
connectorConfig?: ConnectorConfig; connectorConfig?: ConnectorConfig;
selectedIngestion?: AirflowPipeline; selectedIngestion?: AirflowPipeline;
serviceType: string;
addIngestion?: (data: AirflowPipeline, triggerIngestion?: boolean) => void; addIngestion?: (data: AirflowPipeline, triggerIngestion?: boolean) => void;
updateIngestion?: (data: AirflowPipeline, triggerIngestion?: boolean) => void; updateIngestion?: (data: AirflowPipeline, triggerIngestion?: boolean) => void;
onCancel: () => void; onCancel: () => void;

View File

@ -29,6 +29,7 @@ const mockServiceList = [
serviceType: 'Redshift', serviceType: 'Redshift',
}, },
]; ];
const mockServiceType = 'Redshift';
jest.mock('../IngestionStepper/IngestionStepper.component', () => { jest.mock('../IngestionStepper/IngestionStepper.component', () => {
return jest.fn().mockReturnValue(<p>IngestionStepper</p>); return jest.fn().mockReturnValue(<p>IngestionStepper</p>);
@ -42,6 +43,7 @@ describe('Test Ingestion modal component', () => {
ingestionList={[]} ingestionList={[]}
ingestionTypes={[]} ingestionTypes={[]}
serviceList={mockServiceList} serviceList={mockServiceList}
serviceType={mockServiceType}
onCancel={mockFunction} onCancel={mockFunction}
/>, />,
{ {
@ -81,6 +83,7 @@ describe('Test Ingestion modal component', () => {
ingestionTypes={['metadata', 'queryUsage'] as PipelineType[]} ingestionTypes={['metadata', 'queryUsage'] as PipelineType[]}
service="bigquery_gcp" service="bigquery_gcp"
serviceList={[]} serviceList={[]}
serviceType={mockServiceType}
onCancel={mockFunction} onCancel={mockFunction}
/>, />,
{ {

View File

@ -206,6 +206,7 @@ export enum DatabaseServiceType {
Postgres = 'Postgres', Postgres = 'Postgres',
Presto = 'Presto', Presto = 'Presto',
Redshift = 'Redshift', Redshift = 'Redshift',
SQLite = 'SQLite',
SingleStore = 'SingleStore', SingleStore = 'SingleStore',
Snowflake = 'Snowflake', Snowflake = 'Snowflake',
Trino = 'Trino', Trino = 'Trino',

View File

@ -27,7 +27,7 @@ export interface TestCaseConfigType {
regex?: string; regex?: string;
forbiddenValues?: Array<number | string>; forbiddenValues?: Array<number | string>;
missingCountValue?: number; missingCountValue?: number;
missingValueMatch?: string; missingValueMatch?: Array<string>;
columnValuesToBeUnique?: boolean; columnValuesToBeUnique?: boolean;
columnValuesToBeNotNull?: boolean; columnValuesToBeNotNull?: boolean;
} }

View File

@ -176,6 +176,10 @@ const DatasetDetailsPage: FunctionComponent = () => {
setShowTestForm(value); setShowTestForm(value);
}; };
const handleSelectedColumn = (value: string | undefined) => {
setSelectedColumn(value);
};
const activeTabHandler = (tabValue: number) => { const activeTabHandler = (tabValue: number) => {
const currentTabIndex = tabValue - 1; const currentTabIndex = tabValue - 1;
if (datasetTableTabs[currentTabIndex].path !== tab) { if (datasetTableTabs[currentTabIndex].path !== tab) {
@ -686,6 +690,7 @@ const DatasetDetailsPage: FunctionComponent = () => {
}); });
setColumns(updatedColumns); setColumns(updatedColumns);
handleShowTestForm(false); handleShowTestForm(false);
setSelectedColumn(undefined);
showToast({ showToast({
variant: 'success', variant: 'success',
body: `Test ${data.testCase.columnTestType} for ${data.columnName} has been added.`, body: `Test ${data.testCase.columnTestType} for ${data.columnName} has been added.`,
@ -794,6 +799,7 @@ const DatasetDetailsPage: FunctionComponent = () => {
handleAddTableTestCase={handleAddTableTestCase} handleAddTableTestCase={handleAddTableTestCase}
handleRemoveColumnTest={handleRemoveColumnTest} handleRemoveColumnTest={handleRemoveColumnTest}
handleRemoveTableTest={handleRemoveTableTest} handleRemoveTableTest={handleRemoveTableTest}
handleSelectedColumn={handleSelectedColumn}
handleShowTestForm={handleShowTestForm} handleShowTestForm={handleShowTestForm}
handleTestModeChange={handleTestModeChange} handleTestModeChange={handleTestModeChange}
isLineageLoading={isLineageLoading} isLineageLoading={isLineageLoading}

View File

@ -193,6 +193,7 @@ const TourPage = () => {
handleAddTableTestCase={handleCountChange} handleAddTableTestCase={handleCountChange}
handleRemoveColumnTest={handleCountChange} handleRemoveColumnTest={handleCountChange}
handleRemoveTableTest={handleCountChange} handleRemoveTableTest={handleCountChange}
handleSelectedColumn={handleCountChange}
handleShowTestForm={handleCountChange} handleShowTestForm={handleCountChange}
handleTestModeChange={handleCountChange} handleTestModeChange={handleCountChange}
isNodeLoading={{ isNodeLoading={{

View File

@ -62,7 +62,7 @@ import {
VERTICA, VERTICA,
} from '../constants/services.const'; } from '../constants/services.const';
import { TabSpecificField } from '../enums/entity.enum'; import { TabSpecificField } from '../enums/entity.enum';
import { IngestionType, ServiceCategory } from '../enums/service.enum'; import { ServiceCategory } from '../enums/service.enum';
import { DashboardServiceType } from '../generated/entity/services/dashboardService'; import { DashboardServiceType } from '../generated/entity/services/dashboardService';
import { DatabaseServiceType } from '../generated/entity/services/databaseService'; import { DatabaseServiceType } from '../generated/entity/services/databaseService';
import { MessagingServiceType } from '../generated/entity/services/messagingService'; import { MessagingServiceType } from '../generated/entity/services/messagingService';
@ -318,89 +318,30 @@ export const getTotalEntityCountByService = (buckets: Array<Bucket> = []) => {
return entityCounts; return entityCounts;
}; };
export const getIngestionTypeList = (
serviceType: string,
onlyMetaData = false
): Array<string> | undefined => {
let ingestionType: Array<string> | undefined;
switch (serviceType) {
case DatabaseServiceType.BigQuery:
ingestionType = onlyMetaData
? [IngestionType.BIGQUERY]
: [IngestionType.BIGQUERY, IngestionType.BIGQUERY_USAGE];
break;
case DatabaseServiceType.Hive:
ingestionType = [IngestionType.HIVE];
break;
case DatabaseServiceType.Mssql:
ingestionType = [IngestionType.MSSQL];
break;
case DatabaseServiceType.MySQL:
ingestionType = [IngestionType.MYSQL];
break;
case DatabaseServiceType.Postgres:
ingestionType = [IngestionType.POSTGRES];
break;
case DatabaseServiceType.Redshift:
ingestionType = onlyMetaData
? [IngestionType.REDSHIFT]
: [IngestionType.REDSHIFT, IngestionType.REDSHIFT_USAGE];
break;
case DatabaseServiceType.Trino:
ingestionType = [IngestionType.TRINO];
break;
case DatabaseServiceType.Snowflake:
ingestionType = onlyMetaData
? [IngestionType.SNOWFLAKE]
: [IngestionType.SNOWFLAKE, IngestionType.SNOWFLAKE_USAGE];
break;
case DatabaseServiceType.Vertica:
ingestionType = [IngestionType.VERTICA];
break;
default:
break;
}
return ingestionType;
};
export const getAirflowPipelineTypes = ( export const getAirflowPipelineTypes = (
serviceType: string, serviceType: string,
onlyMetaData = false onlyMetaData = false
): Array<PipelineType> | undefined => { ): Array<PipelineType> | undefined => {
if (onlyMetaData) {
return [PipelineType.Metadata];
}
switch (serviceType) { switch (serviceType) {
case DatabaseServiceType.Redshift: case DatabaseServiceType.Redshift:
case DatabaseServiceType.BigQuery: case DatabaseServiceType.BigQuery:
case DatabaseServiceType.Snowflake: case DatabaseServiceType.Snowflake:
return [PipelineType.Metadata, PipelineType.QueryUsage]; case DatabaseServiceType.ClickHouse:
case DatabaseServiceType.Hive:
case DatabaseServiceType.Mssql: case DatabaseServiceType.Mssql:
return onlyMetaData
? [PipelineType.Metadata]
: [PipelineType.Metadata, PipelineType.QueryUsage];
// need to add additional config feild to support trino
// case DatabaseServiceType.Trino:
case DatabaseServiceType.Hive:
case DatabaseServiceType.MySQL: case DatabaseServiceType.MySQL:
case DatabaseServiceType.Postgres: case DatabaseServiceType.Postgres:
case DatabaseServiceType.Trino:
case DatabaseServiceType.Vertica: case DatabaseServiceType.Vertica:
case DatabaseServiceType.MariaDB: case DatabaseServiceType.MariaDB:
case DatabaseServiceType.SingleStore:
case DatabaseServiceType.SQLite:
case DatabaseServiceType.AzureSQL:
return [PipelineType.Metadata]; return [PipelineType.Metadata];
default: default: