mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-10-08 07:16:29 +00:00
Feat: Data Quality Tab (#3183)
This commit is contained in:
parent
a9290bf1a0
commit
34dc6cb3e3
@ -13,6 +13,10 @@
|
|||||||
|
|
||||||
import { AxiosResponse } from 'axios';
|
import { AxiosResponse } from 'axios';
|
||||||
import { Table } from 'Models';
|
import { Table } from 'Models';
|
||||||
|
import { ColumnTestType } from '../enums/columnTest.enum';
|
||||||
|
import { CreateTableTest } from '../generated/api/tests/createTableTest';
|
||||||
|
import { TableTestType } from '../generated/tests/tableTest';
|
||||||
|
import { CreateColumnTest } from '../interface/dataQuality.interface';
|
||||||
import { getURLWithQueryFields } from '../utils/APIUtils';
|
import { getURLWithQueryFields } from '../utils/APIUtils';
|
||||||
import APIClient from './index';
|
import APIClient from './index';
|
||||||
|
|
||||||
@ -111,3 +115,48 @@ export const removeFollower: Function = (
|
|||||||
configOptions
|
configOptions
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const addTableTestCase = (tableId: string, data: CreateTableTest) => {
|
||||||
|
const configOptions = {
|
||||||
|
headers: { 'Content-type': 'application/json' },
|
||||||
|
};
|
||||||
|
|
||||||
|
return APIClient.put(`/tables/${tableId}/tableTest`, data, configOptions);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteTableTestCase = (
|
||||||
|
tableId: string,
|
||||||
|
tableTestType: TableTestType
|
||||||
|
): Promise<AxiosResponse> => {
|
||||||
|
const configOptions = {
|
||||||
|
headers: { 'Content-type': 'application/json' },
|
||||||
|
};
|
||||||
|
|
||||||
|
return APIClient.delete(
|
||||||
|
`/tables/${tableId}/tableTest/${tableTestType}`,
|
||||||
|
configOptions
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addColumnTestCase = (tableId: string, data: CreateColumnTest) => {
|
||||||
|
const configOptions = {
|
||||||
|
headers: { 'Content-type': 'application/json' },
|
||||||
|
};
|
||||||
|
|
||||||
|
return APIClient.put(`/tables/${tableId}/columnTest`, data, configOptions);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteColumnTestCase = (
|
||||||
|
tableId: string,
|
||||||
|
columnName: string,
|
||||||
|
columnTestType: ColumnTestType
|
||||||
|
): Promise<AxiosResponse> => {
|
||||||
|
const configOptions = {
|
||||||
|
headers: { 'Content-type': 'application/json' },
|
||||||
|
};
|
||||||
|
|
||||||
|
return APIClient.delete(
|
||||||
|
`/tables/${tableId}/columnTest/${columnName}/${columnTestType}`,
|
||||||
|
configOptions
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 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 React from 'react';
|
||||||
|
import { CreateTableTest } from '../../generated/api/tests/createTableTest';
|
||||||
|
import { Table } from '../../generated/entity/data/table';
|
||||||
|
import { TableTest } from '../../generated/tests/tableTest';
|
||||||
|
import {
|
||||||
|
CreateColumnTest,
|
||||||
|
TableTestDataType,
|
||||||
|
} from '../../interface/dataQuality.interface';
|
||||||
|
import ColumnTestForm from './Forms/ColumnTestForm';
|
||||||
|
import TableTestForm from './Forms/TableTestForm';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
data?: TableTestDataType;
|
||||||
|
testMode: 'table' | 'column';
|
||||||
|
columnOptions: Table['columns'];
|
||||||
|
tableTestCase: TableTest[];
|
||||||
|
handleAddTableTestCase: (data: CreateTableTest) => void;
|
||||||
|
handleAddColumnTestCase: (data: CreateColumnTest) => void;
|
||||||
|
onFormCancel: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AddDataQualityTest = ({
|
||||||
|
tableTestCase,
|
||||||
|
data,
|
||||||
|
testMode,
|
||||||
|
columnOptions = [],
|
||||||
|
handleAddTableTestCase,
|
||||||
|
handleAddColumnTestCase,
|
||||||
|
onFormCancel,
|
||||||
|
}: Props) => {
|
||||||
|
return (
|
||||||
|
<div className="tw-max-w-xl tw-mx-auto tw-pb-6">
|
||||||
|
{testMode === 'table' ? (
|
||||||
|
<TableTestForm
|
||||||
|
data={data as TableTest}
|
||||||
|
handleAddTableTestCase={handleAddTableTestCase}
|
||||||
|
tableTestCase={tableTestCase}
|
||||||
|
onFormCancel={onFormCancel}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ColumnTestForm
|
||||||
|
column={columnOptions}
|
||||||
|
data={data as CreateColumnTest}
|
||||||
|
handleAddColumnTestCase={handleAddColumnTestCase}
|
||||||
|
onFormCancel={onFormCancel}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddDataQualityTest;
|
@ -0,0 +1,618 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 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 classNames from 'classnames';
|
||||||
|
import { cloneDeep, isEmpty, isUndefined } from 'lodash';
|
||||||
|
import { EditorContentRef } from 'Models';
|
||||||
|
import React, { Fragment, useEffect, useRef, useState } from 'react';
|
||||||
|
import { ColumnTestType } from '../../../enums/columnTest.enum';
|
||||||
|
import { TestCaseExecutionFrequency } from '../../../generated/api/tests/createTableTest';
|
||||||
|
import { Table } from '../../../generated/entity/data/table';
|
||||||
|
import {
|
||||||
|
CreateColumnTest,
|
||||||
|
ModifiedTableColumn,
|
||||||
|
} from '../../../interface/dataQuality.interface';
|
||||||
|
import {
|
||||||
|
errorMsg,
|
||||||
|
getCurrentUserId,
|
||||||
|
requiredField,
|
||||||
|
} from '../../../utils/CommonUtils';
|
||||||
|
import SVGIcons from '../../../utils/SvgUtils';
|
||||||
|
import { getDataTypeString } from '../../../utils/TableUtils';
|
||||||
|
import { Button } from '../../buttons/Button/Button';
|
||||||
|
import MarkdownWithPreview from '../../common/editor/MarkdownWithPreview';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
data: CreateColumnTest;
|
||||||
|
column: ModifiedTableColumn[];
|
||||||
|
handleAddColumnTestCase: (data: CreateColumnTest) => void;
|
||||||
|
onFormCancel: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Field = ({
|
||||||
|
children,
|
||||||
|
className = '',
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}) => {
|
||||||
|
return <div className={classNames('tw-mt-4', className)}>{children}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ColumnTestForm = ({
|
||||||
|
data,
|
||||||
|
column,
|
||||||
|
handleAddColumnTestCase,
|
||||||
|
onFormCancel,
|
||||||
|
}: Props) => {
|
||||||
|
const markdownRef = useRef<EditorContentRef>();
|
||||||
|
const [description] = useState(data?.description || '');
|
||||||
|
const isAcceptedTypeIsString = useRef<boolean>(true);
|
||||||
|
const [columnTest, setColumnTest] = useState<ColumnTestType>(
|
||||||
|
data?.testCase?.columnTestType
|
||||||
|
);
|
||||||
|
const [columnOptions, setColumnOptions] = useState<Table['columns']>([]);
|
||||||
|
const [testTypeOptions, setTestTypeOptions] = useState<string[]>([]);
|
||||||
|
const [minValue, setMinValue] = useState<number | undefined>(
|
||||||
|
data?.testCase?.config?.minValue
|
||||||
|
);
|
||||||
|
const [maxValue, setMaxValue] = useState<number | undefined>(
|
||||||
|
data?.testCase?.config?.maxValue
|
||||||
|
);
|
||||||
|
|
||||||
|
const [frequency, setFrequency] = useState<TestCaseExecutionFrequency>(
|
||||||
|
data?.executionFrequency || TestCaseExecutionFrequency.Daily
|
||||||
|
);
|
||||||
|
const [forbiddenValues, setForbiddenValues] = useState<(string | number)[]>(
|
||||||
|
data?.testCase?.config?.forbiddenValues || ['']
|
||||||
|
);
|
||||||
|
const [isShowError, setIsShowError] = useState({
|
||||||
|
columnName: false,
|
||||||
|
regex: false,
|
||||||
|
minOrMax: false,
|
||||||
|
missingCountValue: false,
|
||||||
|
values: false,
|
||||||
|
minMaxValue: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [columnName, setColumnName] = useState(data?.columnName);
|
||||||
|
const [missingValueMatch, setMissingValueMatch] = useState<string>(
|
||||||
|
data?.testCase?.config?.missingValueMatch || ''
|
||||||
|
);
|
||||||
|
const [missingCountValue, setMissingCountValue] = useState<
|
||||||
|
number | undefined
|
||||||
|
>(data?.testCase?.config?.missingCountValue);
|
||||||
|
|
||||||
|
const [regex, setRegex] = useState<string>(
|
||||||
|
data?.testCase?.config?.regex || ''
|
||||||
|
);
|
||||||
|
|
||||||
|
const addValueFields = () => {
|
||||||
|
setForbiddenValues([...forbiddenValues, '']);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeValueFields = (i: number) => {
|
||||||
|
const newFormValues = [...forbiddenValues];
|
||||||
|
newFormValues.splice(i, 1);
|
||||||
|
setForbiddenValues(newFormValues);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleValueFieldsChange = (i: number, value: string) => {
|
||||||
|
const newFormValues = [...forbiddenValues];
|
||||||
|
newFormValues[i] = value;
|
||||||
|
setForbiddenValues(newFormValues);
|
||||||
|
setIsShowError({ ...isShowError, values: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTestTypeOptionChange = (name: string) => {
|
||||||
|
const selectedColumn = column.find((d) => d.name === name);
|
||||||
|
const existingTests =
|
||||||
|
selectedColumn?.columnTests?.map(
|
||||||
|
(d: CreateColumnTest) => d.testCase.columnTestType
|
||||||
|
) || [];
|
||||||
|
if (existingTests.length) {
|
||||||
|
const newTest = Object.values(ColumnTestType).filter(
|
||||||
|
(d) => !existingTests.includes(d)
|
||||||
|
);
|
||||||
|
setTestTypeOptions(newTest);
|
||||||
|
setColumnTest(newTest[0]);
|
||||||
|
} else {
|
||||||
|
const newTest = Object.values(ColumnTestType);
|
||||||
|
setTestTypeOptions(newTest);
|
||||||
|
setColumnTest(newTest[0]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isUndefined(data)) {
|
||||||
|
const allOption = column.filter((value) => {
|
||||||
|
return (
|
||||||
|
value?.dataType !== 'STRUCT' &&
|
||||||
|
value.columnTests?.length !== Object.values(ColumnTestType).length
|
||||||
|
);
|
||||||
|
});
|
||||||
|
setColumnOptions(allOption);
|
||||||
|
setColumnName(allOption[0]?.name || '');
|
||||||
|
handleTestTypeOptionChange(allOption[0]?.name || '');
|
||||||
|
} else {
|
||||||
|
setColumnOptions(column);
|
||||||
|
setTestTypeOptions(Object.values(ColumnTestType));
|
||||||
|
setColumnName(data.columnName || '');
|
||||||
|
}
|
||||||
|
}, [column]);
|
||||||
|
|
||||||
|
const validateForm = () => {
|
||||||
|
const errMsg = cloneDeep(isShowError);
|
||||||
|
errMsg.columnName = isEmpty(columnName);
|
||||||
|
|
||||||
|
switch (columnTest) {
|
||||||
|
case ColumnTestType.columnValueLengthsToBeBetween:
|
||||||
|
case ColumnTestType.columnValuesToBeBetween:
|
||||||
|
errMsg.minOrMax = isEmpty(minValue) && isEmpty(maxValue);
|
||||||
|
if (!isEmpty(minValue) && !isEmpty(maxValue)) {
|
||||||
|
errMsg.minMaxValue = (minValue as number) > (maxValue as number);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ColumnTestType.columnValuesMissingCountToBeEqual:
|
||||||
|
errMsg.missingCountValue = isEmpty(missingCountValue);
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ColumnTestType.columnValuesToBeNotInSet: {
|
||||||
|
const actualValues = forbiddenValues.filter((v) => !isEmpty(v));
|
||||||
|
errMsg.values = actualValues.length < 1;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case ColumnTestType.columnValuesToMatchRegex:
|
||||||
|
errMsg.regex = isEmpty(regex);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsShowError(errMsg);
|
||||||
|
|
||||||
|
return !Object.values(errMsg).includes(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTestConfi = () => {
|
||||||
|
switch (columnTest) {
|
||||||
|
case ColumnTestType.columnValueLengthsToBeBetween:
|
||||||
|
case ColumnTestType.columnValuesToBeBetween:
|
||||||
|
return {
|
||||||
|
minValue: minValue,
|
||||||
|
maxValue: maxValue,
|
||||||
|
};
|
||||||
|
|
||||||
|
case ColumnTestType.columnValuesMissingCountToBeEqual:
|
||||||
|
return {
|
||||||
|
missingCountValue: missingCountValue,
|
||||||
|
missingValueMatch: missingValueMatch,
|
||||||
|
};
|
||||||
|
|
||||||
|
case ColumnTestType.columnValuesToBeNotInSet:
|
||||||
|
return {
|
||||||
|
forbiddenValues: forbiddenValues.filter((v) => !isEmpty(v)),
|
||||||
|
};
|
||||||
|
|
||||||
|
case ColumnTestType.columnValuesToMatchRegex:
|
||||||
|
return {
|
||||||
|
regex: regex,
|
||||||
|
};
|
||||||
|
|
||||||
|
case ColumnTestType.columnValuesToBeNotNull:
|
||||||
|
case ColumnTestType.columnValuesToBeUnique:
|
||||||
|
default:
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
if (validateForm()) {
|
||||||
|
const columnTestObj: CreateColumnTest = {
|
||||||
|
columnName: columnName,
|
||||||
|
description: markdownRef.current?.getEditorContent() || undefined,
|
||||||
|
executionFrequency: frequency,
|
||||||
|
testCase: {
|
||||||
|
config: getTestConfi(),
|
||||||
|
columnTestType: columnTest,
|
||||||
|
},
|
||||||
|
owner: {
|
||||||
|
type: 'user',
|
||||||
|
id: getCurrentUserId(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
handleAddColumnTestCase(columnTestObj);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleValidation = (
|
||||||
|
event: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
|
||||||
|
) => {
|
||||||
|
const value = event.target.value;
|
||||||
|
const eleName = event.target.name;
|
||||||
|
|
||||||
|
const errorMsg = cloneDeep(isShowError);
|
||||||
|
|
||||||
|
switch (eleName) {
|
||||||
|
case 'columTestType': {
|
||||||
|
const selectedColumn = column.find((d) => d.name === columnName);
|
||||||
|
const columnDataType = getDataTypeString(
|
||||||
|
selectedColumn?.dataType as string
|
||||||
|
);
|
||||||
|
isAcceptedTypeIsString.current =
|
||||||
|
columnDataType === 'varchar' || columnDataType === 'boolean';
|
||||||
|
setForbiddenValues(['']);
|
||||||
|
setColumnTest(value as ColumnTestType);
|
||||||
|
errorMsg.columnName = false;
|
||||||
|
errorMsg.regex = false;
|
||||||
|
errorMsg.minOrMax = false;
|
||||||
|
errorMsg.missingCountValue = false;
|
||||||
|
errorMsg.values = false;
|
||||||
|
errorMsg.minMaxValue = false;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'min': {
|
||||||
|
setMinValue(value as unknown as number);
|
||||||
|
errorMsg.minOrMax = false;
|
||||||
|
errorMsg.minMaxValue = false;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'max': {
|
||||||
|
setMaxValue(value as unknown as number);
|
||||||
|
errorMsg.minOrMax = false;
|
||||||
|
errorMsg.minMaxValue = false;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'frequency':
|
||||||
|
setFrequency(value as TestCaseExecutionFrequency);
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'columnName': {
|
||||||
|
const selectedColumn = column.find((d) => d.name === value);
|
||||||
|
const columnDataType = getDataTypeString(
|
||||||
|
selectedColumn?.dataType as string
|
||||||
|
);
|
||||||
|
isAcceptedTypeIsString.current =
|
||||||
|
columnDataType === 'varchar' || columnDataType === 'boolean';
|
||||||
|
setForbiddenValues(['']);
|
||||||
|
setColumnName(value);
|
||||||
|
handleTestTypeOptionChange(value);
|
||||||
|
errorMsg.columnName = false;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'missingValueMatch':
|
||||||
|
setMissingValueMatch(value);
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'missingCountValue':
|
||||||
|
setMissingCountValue(value as unknown as number);
|
||||||
|
errorMsg.missingCountValue = false;
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'regex':
|
||||||
|
setRegex(value);
|
||||||
|
errorMsg.regex = false;
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsShowError(errorMsg);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMinMaxField = () => {
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<div className="tw-flex tw-gap-4 tw-w-full">
|
||||||
|
<div className="tw-flex-1">
|
||||||
|
<label className="tw-block tw-form-label" htmlFor="min">
|
||||||
|
Min:
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className="tw-form-inputs tw-px-3 tw-py-1"
|
||||||
|
data-testid="min"
|
||||||
|
id="min"
|
||||||
|
name="min"
|
||||||
|
placeholder="10"
|
||||||
|
type="number"
|
||||||
|
value={minValue}
|
||||||
|
onChange={handleValidation}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="tw-flex-1">
|
||||||
|
<label className="tw-block tw-form-label" htmlFor="max">
|
||||||
|
Max:
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className="tw-form-inputs tw-px-3 tw-py-1"
|
||||||
|
data-testid="max"
|
||||||
|
id="max"
|
||||||
|
name="max"
|
||||||
|
placeholder="100"
|
||||||
|
type="number"
|
||||||
|
value={maxValue}
|
||||||
|
onChange={handleValidation}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{isShowError.minOrMax && errorMsg('Please enter atleast one value.')}
|
||||||
|
{isShowError.minMaxValue &&
|
||||||
|
errorMsg('Min value should be lower than Max value.')}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMissingCountToBeEqualFields = () => {
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<div className="tw-flex tw-gap-4 tw-w-full">
|
||||||
|
<div className="tw-flex-1">
|
||||||
|
<label
|
||||||
|
className="tw-block tw-form-label"
|
||||||
|
htmlFor="missingCountValue">
|
||||||
|
{requiredField('Count:')}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className="tw-form-inputs tw-px-3 tw-py-1"
|
||||||
|
data-testid="missingCountValue"
|
||||||
|
id="missingCountValue"
|
||||||
|
name="missingCountValue"
|
||||||
|
placeholder="Missing count value"
|
||||||
|
type="number"
|
||||||
|
value={missingCountValue}
|
||||||
|
onChange={handleValidation}
|
||||||
|
/>
|
||||||
|
{isShowError.missingCountValue &&
|
||||||
|
errorMsg('Count value is required.')}
|
||||||
|
</div>
|
||||||
|
<div className="tw-flex-1">
|
||||||
|
<label
|
||||||
|
className="tw-block tw-form-label"
|
||||||
|
htmlFor="missingValueMatch">
|
||||||
|
Match:
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className="tw-form-inputs tw-px-3 tw-py-1"
|
||||||
|
data-testid="missingValueMatch"
|
||||||
|
id="missingValueMatch"
|
||||||
|
name="missingValueMatch"
|
||||||
|
placeholder="Missing value match"
|
||||||
|
value={missingValueMatch}
|
||||||
|
onChange={handleValidation}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getColumnValuesToMatchRegexFields = () => {
|
||||||
|
return (
|
||||||
|
<Field>
|
||||||
|
<label className="tw-block tw-form-label" htmlFor="regex">
|
||||||
|
{requiredField('Regex:')}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className="tw-form-inputs tw-px-3 tw-py-1"
|
||||||
|
data-testid="regex"
|
||||||
|
id="regex"
|
||||||
|
name="regex"
|
||||||
|
placeholder="Regex column entries should match"
|
||||||
|
value={regex}
|
||||||
|
onChange={handleValidation}
|
||||||
|
/>
|
||||||
|
{isShowError.regex && errorMsg('Regex is required.')}
|
||||||
|
</Field>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getColumnValuesToBeNotInSetField = () => {
|
||||||
|
return (
|
||||||
|
<div data-testid="not-in-set-fiel">
|
||||||
|
<div className="tw-flex tw-items-center tw-mt-6">
|
||||||
|
<p className="w-form-label tw-mr-3">{requiredField('Values')}</p>
|
||||||
|
<Button
|
||||||
|
className="tw-h-5 tw-px-2"
|
||||||
|
size="x-small"
|
||||||
|
theme="primary"
|
||||||
|
variant="contained"
|
||||||
|
onClick={addValueFields}>
|
||||||
|
<i aria-hidden="true" className="fa fa-plus" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{forbiddenValues.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={`option-key-${i}`}
|
||||||
|
name="key"
|
||||||
|
placeholder="Values not to be in the set"
|
||||||
|
type={isAcceptedTypeIsString.current ? 'text' : 'number'}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => handleValueFieldsChange(i, e.target.value)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="focus:tw-outline-none tw-mt-3 tw-w-1/12"
|
||||||
|
onClick={(e) => {
|
||||||
|
removeValueFields(i);
|
||||||
|
e.preventDefault();
|
||||||
|
}}>
|
||||||
|
<SVGIcons
|
||||||
|
alt="delete"
|
||||||
|
icon="icon-delete"
|
||||||
|
title="Delete"
|
||||||
|
width="12px"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{isShowError.values && errorMsg('Value is required.')}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getColumnTestConfig = () => {
|
||||||
|
switch (columnTest) {
|
||||||
|
case ColumnTestType.columnValueLengthsToBeBetween:
|
||||||
|
case ColumnTestType.columnValuesToBeBetween:
|
||||||
|
return getMinMaxField();
|
||||||
|
|
||||||
|
case ColumnTestType.columnValuesMissingCountToBeEqual:
|
||||||
|
return getMissingCountToBeEqualFields();
|
||||||
|
|
||||||
|
case ColumnTestType.columnValuesToBeNotInSet:
|
||||||
|
return getColumnValuesToBeNotInSetField();
|
||||||
|
|
||||||
|
case ColumnTestType.columnValuesToMatchRegex:
|
||||||
|
return getColumnValuesToMatchRegexFields();
|
||||||
|
|
||||||
|
case ColumnTestType.columnValuesToBeNotNull:
|
||||||
|
case ColumnTestType.columnValuesToBeUnique:
|
||||||
|
default:
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p className="tw-font-medium tw-px-4">
|
||||||
|
{isUndefined(data) ? 'Add' : 'Edit'} Column Test
|
||||||
|
</p>
|
||||||
|
<form className="tw-w-screen-sm" data-testid="form">
|
||||||
|
<div className="tw-px-4 tw-mx-auto">
|
||||||
|
<Field>
|
||||||
|
<label className="tw-block tw-form-label" htmlFor="columnName">
|
||||||
|
{requiredField('Column Name:')}
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className={classNames('tw-form-inputs tw-px-3 tw-py-1', {
|
||||||
|
'tw-cursor-not-allowed': !isUndefined(data),
|
||||||
|
})}
|
||||||
|
disabled={!isUndefined(data)}
|
||||||
|
id="columnName"
|
||||||
|
name="columnName"
|
||||||
|
value={columnName}
|
||||||
|
onChange={handleValidation}>
|
||||||
|
{columnOptions.map((option) => (
|
||||||
|
<option key={option.name} value={option.name}>
|
||||||
|
{option.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{isShowError.columnName && errorMsg('Column name is required.')}
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field>
|
||||||
|
<label className="tw-block tw-form-label" htmlFor="columTestType">
|
||||||
|
{requiredField('Test Type:')}
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className={classNames('tw-form-inputs tw-px-3 tw-py-1', {
|
||||||
|
'tw-cursor-not-allowed': !isUndefined(data),
|
||||||
|
})}
|
||||||
|
disabled={!isUndefined(data)}
|
||||||
|
id="columTestType"
|
||||||
|
name="columTestType"
|
||||||
|
value={columnTest}
|
||||||
|
onChange={handleValidation}>
|
||||||
|
{testTypeOptions &&
|
||||||
|
testTypeOptions.length > 0 &&
|
||||||
|
testTypeOptions.map((option) => (
|
||||||
|
<option key={option} value={option}>
|
||||||
|
{option}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field>
|
||||||
|
<label
|
||||||
|
className="tw-block tw-form-label tw-mb-0"
|
||||||
|
htmlFor="description">
|
||||||
|
Description:
|
||||||
|
</label>
|
||||||
|
<MarkdownWithPreview
|
||||||
|
data-testid="description"
|
||||||
|
ref={markdownRef}
|
||||||
|
value={description}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
{getColumnTestConfig()}
|
||||||
|
|
||||||
|
<Field>
|
||||||
|
<label className="tw-block tw-form-label" htmlFor="frequency">
|
||||||
|
Frequency of Test Run:
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className="tw-form-inputs tw-px-3 tw-py-1"
|
||||||
|
id="frequency"
|
||||||
|
name="frequency"
|
||||||
|
value={frequency}
|
||||||
|
onChange={handleValidation}>
|
||||||
|
{Object.values(TestCaseExecutionFrequency).map((option) => (
|
||||||
|
<option key={option} value={option}>
|
||||||
|
{option}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
<Field>
|
||||||
|
<Field className="tw-flex tw-justify-end">
|
||||||
|
<Button
|
||||||
|
data-testid="cancel-test"
|
||||||
|
size="regular"
|
||||||
|
theme="primary"
|
||||||
|
variant="text"
|
||||||
|
onClick={onFormCancel}>
|
||||||
|
Discard
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="tw-w-16 tw-h-10"
|
||||||
|
size="regular"
|
||||||
|
theme="primary"
|
||||||
|
variant="contained"
|
||||||
|
onClick={handleSave}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</Field>
|
||||||
|
</Field>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ColumnTestForm;
|
@ -0,0 +1,345 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 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 classNames from 'classnames';
|
||||||
|
import { cloneDeep, isEmpty, isUndefined } from 'lodash';
|
||||||
|
import { EditorContentRef } from 'Models';
|
||||||
|
import React, { Fragment, useEffect, useRef, useState } from 'react';
|
||||||
|
import {
|
||||||
|
CreateTableTest,
|
||||||
|
TableTestType,
|
||||||
|
TestCaseExecutionFrequency,
|
||||||
|
} from '../../../generated/api/tests/createTableTest';
|
||||||
|
import { TableTest } from '../../../generated/tests/tableTest';
|
||||||
|
import {
|
||||||
|
errorMsg,
|
||||||
|
getCurrentUserId,
|
||||||
|
requiredField,
|
||||||
|
} from '../../../utils/CommonUtils';
|
||||||
|
import { Button } from '../../buttons/Button/Button';
|
||||||
|
import MarkdownWithPreview from '../../common/editor/MarkdownWithPreview';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
data: TableTest;
|
||||||
|
tableTestCase: TableTest[];
|
||||||
|
handleAddTableTestCase: (data: CreateTableTest) => void;
|
||||||
|
onFormCancel: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Field = ({
|
||||||
|
children,
|
||||||
|
className = '',
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}) => {
|
||||||
|
return <div className={classNames('tw-mt-4', className)}>{children}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TableTestForm = ({
|
||||||
|
data,
|
||||||
|
tableTestCase,
|
||||||
|
handleAddTableTestCase,
|
||||||
|
onFormCancel,
|
||||||
|
}: Props) => {
|
||||||
|
const markdownRef = useRef<EditorContentRef>();
|
||||||
|
const [tableTest, setTableTest] = useState<TableTestType | undefined>(
|
||||||
|
data?.testCase?.tableTestType
|
||||||
|
);
|
||||||
|
const [description] = useState(data?.description || '');
|
||||||
|
const [minValue, setMinValue] = useState<number | undefined>(
|
||||||
|
data?.testCase?.config?.minValue
|
||||||
|
);
|
||||||
|
const [maxValue, setMaxValue] = useState<number | undefined>(
|
||||||
|
data?.testCase?.config?.maxValue
|
||||||
|
);
|
||||||
|
const [value, setValue] = useState<number | undefined>(
|
||||||
|
data?.testCase.config?.value
|
||||||
|
);
|
||||||
|
const [frequency, setFrequency] = useState<TestCaseExecutionFrequency>(
|
||||||
|
data?.executionFrequency
|
||||||
|
? data.executionFrequency
|
||||||
|
: TestCaseExecutionFrequency.Daily
|
||||||
|
);
|
||||||
|
const [isShowError, setIsShowError] = useState({
|
||||||
|
minOrMax: false,
|
||||||
|
values: false,
|
||||||
|
minMaxValue: false,
|
||||||
|
});
|
||||||
|
const [testTypeOptions, setTestTypeOptions] = useState<string[]>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (tableTestCase.length && isUndefined(data)) {
|
||||||
|
const existingTest = tableTestCase?.map(
|
||||||
|
(d) => d.testCase.tableTestType as string
|
||||||
|
);
|
||||||
|
const newTest = Object.values(TableTestType).filter(
|
||||||
|
(d) => !existingTest.includes(d)
|
||||||
|
);
|
||||||
|
setTestTypeOptions(newTest);
|
||||||
|
setTableTest(newTest[0]);
|
||||||
|
} else {
|
||||||
|
const testValue = Object.values(TableTestType);
|
||||||
|
setTestTypeOptions(testValue);
|
||||||
|
setTableTest(data?.testCase?.tableTestType || testValue[0]);
|
||||||
|
}
|
||||||
|
}, [tableTestCase]);
|
||||||
|
|
||||||
|
const validateForm = () => {
|
||||||
|
const errMsg = cloneDeep(isShowError);
|
||||||
|
|
||||||
|
const isTableRowCountToBeBetweenTest =
|
||||||
|
tableTest === TableTestType.TableRowCountToBeBetween;
|
||||||
|
|
||||||
|
if (isTableRowCountToBeBetweenTest) {
|
||||||
|
errMsg.minOrMax = isEmpty(minValue) && isEmpty(maxValue);
|
||||||
|
if (!isEmpty(minValue) && !isEmpty(maxValue)) {
|
||||||
|
errMsg.minMaxValue = (minValue as number) > (maxValue as number);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
errMsg.values = isEmpty(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsShowError(errMsg);
|
||||||
|
|
||||||
|
return !Object.values(errMsg).includes(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
const isTableRowCountToBeBetweenTest =
|
||||||
|
tableTest === TableTestType.TableRowCountToBeBetween;
|
||||||
|
|
||||||
|
if (validateForm()) {
|
||||||
|
const createTest: CreateTableTest = {
|
||||||
|
description: markdownRef.current?.getEditorContent() || undefined,
|
||||||
|
executionFrequency: frequency,
|
||||||
|
testCase: {
|
||||||
|
config: {
|
||||||
|
maxValue: isTableRowCountToBeBetweenTest ? maxValue : undefined,
|
||||||
|
minValue: isTableRowCountToBeBetweenTest ? minValue : undefined,
|
||||||
|
value: isTableRowCountToBeBetweenTest ? undefined : value,
|
||||||
|
},
|
||||||
|
tableTestType: tableTest,
|
||||||
|
},
|
||||||
|
owner: {
|
||||||
|
type: 'user',
|
||||||
|
id: getCurrentUserId(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
handleAddTableTestCase(createTest);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleValidation = (
|
||||||
|
event: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
|
||||||
|
) => {
|
||||||
|
const value = event.target.value;
|
||||||
|
const eleName = event.target.name;
|
||||||
|
|
||||||
|
const errorMsg = cloneDeep(isShowError);
|
||||||
|
|
||||||
|
switch (eleName) {
|
||||||
|
case 'tableTestType':
|
||||||
|
setTableTest(value as TableTestType);
|
||||||
|
errorMsg.minMaxValue = false;
|
||||||
|
errorMsg.minOrMax = false;
|
||||||
|
errorMsg.values = false;
|
||||||
|
|
||||||
|
break;
|
||||||
|
case 'min':
|
||||||
|
setMinValue(value as unknown as number);
|
||||||
|
errorMsg.minMaxValue = false;
|
||||||
|
errorMsg.minOrMax = false;
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'max':
|
||||||
|
setMaxValue(value as unknown as number);
|
||||||
|
errorMsg.minMaxValue = false;
|
||||||
|
errorMsg.minOrMax = false;
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'value':
|
||||||
|
setValue(value as unknown as number);
|
||||||
|
errorMsg.values = false;
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'frequency':
|
||||||
|
setFrequency(value as TestCaseExecutionFrequency);
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsShowError(errorMsg);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getValueField = () => {
|
||||||
|
return (
|
||||||
|
<Field>
|
||||||
|
<label className="tw-block tw-form-label" htmlFor="value">
|
||||||
|
{requiredField('Value:')}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className="tw-form-inputs tw-px-3 tw-py-1"
|
||||||
|
data-testid="value"
|
||||||
|
id="value"
|
||||||
|
name="value"
|
||||||
|
placeholder="100"
|
||||||
|
type="number"
|
||||||
|
value={value}
|
||||||
|
onChange={handleValidation}
|
||||||
|
/>
|
||||||
|
{isShowError.values && errorMsg('Value is required.')}
|
||||||
|
</Field>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMinMaxField = () => {
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<div className="tw-flex tw-gap-4 tw-w-full">
|
||||||
|
<div className="tw-flex-1">
|
||||||
|
<label className="tw-block tw-form-label" htmlFor="min">
|
||||||
|
Min:
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className="tw-form-inputs tw-px-3 tw-py-1"
|
||||||
|
data-testid="min"
|
||||||
|
id="min"
|
||||||
|
name="min"
|
||||||
|
placeholder="10"
|
||||||
|
type="number"
|
||||||
|
value={minValue}
|
||||||
|
onChange={handleValidation}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="tw-flex-1">
|
||||||
|
<label className="tw-block tw-form-label" htmlFor="max">
|
||||||
|
Max:
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className="tw-form-inputs tw-px-3 tw-py-1"
|
||||||
|
data-testid="max"
|
||||||
|
id="max"
|
||||||
|
name="max"
|
||||||
|
placeholder="100"
|
||||||
|
type="number"
|
||||||
|
value={maxValue}
|
||||||
|
onChange={handleValidation}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{isShowError.minOrMax && errorMsg('Please enter atleast one value')}
|
||||||
|
{isShowError.minMaxValue &&
|
||||||
|
errorMsg('Min value should be lower than Max value.')}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<p className="tw-font-medium tw-px-4">
|
||||||
|
{isUndefined(data) ? 'Add' : 'Edit'} Table Test
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form className="tw-w-screen-sm" data-testid="form">
|
||||||
|
<div className="tw-px-4 tw-mx-auto">
|
||||||
|
<Field>
|
||||||
|
<label className="tw-block tw-form-label" htmlFor="tableTestType">
|
||||||
|
{requiredField('Test Type:')}
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className={classNames('tw-form-inputs tw-px-3 tw-py-1', {
|
||||||
|
'tw-cursor-not-allowed': !isUndefined(data),
|
||||||
|
})}
|
||||||
|
disabled={!isUndefined(data)}
|
||||||
|
id="tableTestType"
|
||||||
|
name="tableTestType"
|
||||||
|
value={tableTest}
|
||||||
|
onChange={handleValidation}>
|
||||||
|
{testTypeOptions &&
|
||||||
|
testTypeOptions.length > 0 &&
|
||||||
|
testTypeOptions.map((option) => (
|
||||||
|
<option key={option} value={option}>
|
||||||
|
{option}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field>
|
||||||
|
<label
|
||||||
|
className="tw-block tw-form-label tw-mb-0"
|
||||||
|
htmlFor="description">
|
||||||
|
Description:
|
||||||
|
</label>
|
||||||
|
<MarkdownWithPreview
|
||||||
|
data-testid="description"
|
||||||
|
ref={markdownRef}
|
||||||
|
value={description}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field>
|
||||||
|
{tableTest === TableTestType.TableRowCountToBeBetween
|
||||||
|
? getMinMaxField()
|
||||||
|
: getValueField()}
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field>
|
||||||
|
<label className="tw-block tw-form-label" htmlFor="frequency">
|
||||||
|
Frequency of Test Run:
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className="tw-form-inputs tw-px-3 tw-py-1"
|
||||||
|
id="frequency"
|
||||||
|
name="frequency"
|
||||||
|
value={frequency}
|
||||||
|
onChange={handleValidation}>
|
||||||
|
{Object.values(TestCaseExecutionFrequency).map((option) => (
|
||||||
|
<option key={option} value={option}>
|
||||||
|
{option}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
<Field className="tw-flex tw-justify-end">
|
||||||
|
<Button
|
||||||
|
data-testid="cancel-test"
|
||||||
|
size="regular"
|
||||||
|
theme="primary"
|
||||||
|
variant="text"
|
||||||
|
onClick={onFormCancel}>
|
||||||
|
Discard
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="tw-w-16 tw-h-10"
|
||||||
|
size="regular"
|
||||||
|
theme="primary"
|
||||||
|
variant="contained"
|
||||||
|
onClick={handleSave}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</Field>
|
||||||
|
</form>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TableTestForm;
|
@ -0,0 +1,129 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 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 React, { useState } from 'react';
|
||||||
|
import { ColumnTestType } from '../../enums/columnTest.enum';
|
||||||
|
import { CreateTableTest } from '../../generated/api/tests/createTableTest';
|
||||||
|
import { Table } from '../../generated/entity/data/table';
|
||||||
|
import { TableTest, TableTestType } from '../../generated/tests/tableTest';
|
||||||
|
import {
|
||||||
|
CreateColumnTest,
|
||||||
|
DatasetTestModeType,
|
||||||
|
ModifiedTableColumn,
|
||||||
|
TableTestDataType,
|
||||||
|
} from '../../interface/dataQuality.interface';
|
||||||
|
import AddDataQualityTest from '../AddDataQualityTest/AddDataQualityTest';
|
||||||
|
import DataQualityTest from '../DataQualityTest/DataQualityTest';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
handleAddTableTestCase: (data: CreateTableTest) => void;
|
||||||
|
handleAddColumnTestCase: (data: CreateColumnTest) => void;
|
||||||
|
columnOptions: Table['columns'];
|
||||||
|
testMode: DatasetTestModeType;
|
||||||
|
handleTestModeChange: (mode: DatasetTestModeType) => void;
|
||||||
|
showTestForm: boolean;
|
||||||
|
handleShowTestForm: (value: boolean) => void;
|
||||||
|
tableTestCase: TableTest[];
|
||||||
|
handleRemoveTableTest: (testType: TableTestType) => void;
|
||||||
|
handleRemoveColumnTest: (
|
||||||
|
columnName: string,
|
||||||
|
testType: ColumnTestType
|
||||||
|
) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DataQualityTab = ({
|
||||||
|
columnOptions,
|
||||||
|
showTestForm,
|
||||||
|
handleTestModeChange,
|
||||||
|
handleShowTestForm,
|
||||||
|
handleAddTableTestCase,
|
||||||
|
handleAddColumnTestCase,
|
||||||
|
handleRemoveTableTest,
|
||||||
|
handleRemoveColumnTest,
|
||||||
|
testMode,
|
||||||
|
tableTestCase,
|
||||||
|
}: Props) => {
|
||||||
|
const [showDropDown, setShowDropDown] = useState(false);
|
||||||
|
const [activeData, setActiveData] = useState<TableTestDataType>();
|
||||||
|
|
||||||
|
const onFormCancel = () => {
|
||||||
|
handleShowTestForm(false);
|
||||||
|
setActiveData(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleShowDropDown = (value: boolean) => {
|
||||||
|
setShowDropDown(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTestSelection = (mode: DatasetTestModeType) => {
|
||||||
|
handleTestModeChange(mode);
|
||||||
|
handleShowTestForm(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const haandleDropDownClick = (
|
||||||
|
_e: React.MouseEvent<HTMLElement, MouseEvent>,
|
||||||
|
value?: string
|
||||||
|
) => {
|
||||||
|
if (value) {
|
||||||
|
handleTestSelection(value as DatasetTestModeType);
|
||||||
|
}
|
||||||
|
setShowDropDown(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditTest = (
|
||||||
|
mode: DatasetTestModeType,
|
||||||
|
obj: TableTestDataType
|
||||||
|
) => {
|
||||||
|
setActiveData(obj);
|
||||||
|
handleTestSelection(mode);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTableTestSave = (data: CreateTableTest) => {
|
||||||
|
handleAddTableTestCase(data);
|
||||||
|
setActiveData(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onColumnTestSave = (data: CreateColumnTest) => {
|
||||||
|
handleAddColumnTestCase(data);
|
||||||
|
setActiveData(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{showTestForm ? (
|
||||||
|
<AddDataQualityTest
|
||||||
|
columnOptions={columnOptions}
|
||||||
|
data={activeData}
|
||||||
|
handleAddColumnTestCase={onColumnTestSave}
|
||||||
|
handleAddTableTestCase={onTableTestSave}
|
||||||
|
tableTestCase={tableTestCase}
|
||||||
|
testMode={testMode}
|
||||||
|
onFormCancel={onFormCancel}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<DataQualityTest
|
||||||
|
columns={columnOptions as ModifiedTableColumn[]}
|
||||||
|
haandleDropDownClick={haandleDropDownClick}
|
||||||
|
handleEditTest={handleEditTest}
|
||||||
|
handleRemoveColumnTest={handleRemoveColumnTest}
|
||||||
|
handleRemoveTableTest={handleRemoveTableTest}
|
||||||
|
handleShowDropDown={handleShowDropDown}
|
||||||
|
showDropDown={showDropDown}
|
||||||
|
tableTestCase={tableTestCase}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DataQualityTab;
|
@ -0,0 +1,158 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 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 React, { useEffect, useState } from 'react';
|
||||||
|
import { TITLE_FOR_NON_ADMIN_ACTION } from '../../constants/constants';
|
||||||
|
import { ColumnTestType } from '../../enums/columnTest.enum';
|
||||||
|
import { TableTest, TableTestType } from '../../generated/tests/tableTest';
|
||||||
|
import {
|
||||||
|
DatasetTestModeType,
|
||||||
|
ModifiedTableColumn,
|
||||||
|
TableTestDataType,
|
||||||
|
} from '../../interface/dataQuality.interface';
|
||||||
|
import { normalLink } from '../../utils/styleconstant';
|
||||||
|
import { dropdownIcon as DropdownIcon } from '../../utils/svgconstant';
|
||||||
|
import ErrorPlaceHolder from '../common/error-with-placeholder/ErrorPlaceHolder';
|
||||||
|
import NonAdminAction from '../common/non-admin-action/NonAdminAction';
|
||||||
|
import DropDownList from '../dropdown/DropDownList';
|
||||||
|
import { DropDownListItem } from '../dropdown/types';
|
||||||
|
import DataQualityTable from './Table/DataQualityTable';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
tableTestCase: TableTest[];
|
||||||
|
columns: ModifiedTableColumn[];
|
||||||
|
showDropDown: boolean;
|
||||||
|
handleEditTest: (mode: DatasetTestModeType, obj: TableTestDataType) => void;
|
||||||
|
handleRemoveTableTest: (testType: TableTestType) => void;
|
||||||
|
haandleDropDownClick: (
|
||||||
|
_e: React.MouseEvent<HTMLElement, MouseEvent>,
|
||||||
|
value?: string | undefined
|
||||||
|
) => void;
|
||||||
|
handleShowDropDown: (value: boolean) => void;
|
||||||
|
handleRemoveColumnTest: (
|
||||||
|
columnName: string,
|
||||||
|
testType: ColumnTestType
|
||||||
|
) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DataQualityTest = ({
|
||||||
|
showDropDown,
|
||||||
|
tableTestCase,
|
||||||
|
columns,
|
||||||
|
handleEditTest,
|
||||||
|
handleShowDropDown,
|
||||||
|
handleRemoveTableTest,
|
||||||
|
handleRemoveColumnTest,
|
||||||
|
haandleDropDownClick,
|
||||||
|
}: Props) => {
|
||||||
|
const [columnsData, setColumnsData] = useState<ModifiedTableColumn[]>([]);
|
||||||
|
|
||||||
|
const isColumnTestDisable = () => {
|
||||||
|
const remainingTest = columns?.filter((d) => {
|
||||||
|
return d?.columnTests?.length !== Object.values(ColumnTestType).length;
|
||||||
|
});
|
||||||
|
|
||||||
|
return !(remainingTest.length > 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const dropdownList: DropDownListItem[] = [
|
||||||
|
{
|
||||||
|
name: 'Table Test',
|
||||||
|
value: 'table',
|
||||||
|
disabled: tableTestCase.length >= Object.values(TableTestType).length,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Column Test',
|
||||||
|
value: 'column',
|
||||||
|
disabled: isColumnTestDisable(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (columns.length) {
|
||||||
|
setColumnsData(
|
||||||
|
columns.filter((d) => d?.columnTests && d?.columnTests.length > 0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [columns]);
|
||||||
|
|
||||||
|
const addTestButton = (horzPosRight: boolean) => {
|
||||||
|
return (
|
||||||
|
<div className="tw-flex tw-justify-end">
|
||||||
|
<NonAdminAction position="bottom" title={TITLE_FOR_NON_ADMIN_ACTION}>
|
||||||
|
<span className="tw-relative">
|
||||||
|
<button onClick={() => handleShowDropDown(true)}>
|
||||||
|
Add Test{' '}
|
||||||
|
<DropdownIcon style={{ marginTop: '1px', color: normalLink }} />
|
||||||
|
</button>
|
||||||
|
{showDropDown && (
|
||||||
|
<DropDownList
|
||||||
|
dropDownList={dropdownList}
|
||||||
|
horzPosRight={horzPosRight}
|
||||||
|
onSelect={haandleDropDownClick}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</NonAdminAction>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{tableTestCase.length > 0 || columnsData.length > 0 ? (
|
||||||
|
<div>
|
||||||
|
{addTestButton(true)}
|
||||||
|
{tableTestCase.length > 0 && (
|
||||||
|
<div className="tw-mb-5">
|
||||||
|
<p className="tw-form-label">Table Tests</p>
|
||||||
|
<DataQualityTable
|
||||||
|
isTableTest
|
||||||
|
handleEditTest={handleEditTest}
|
||||||
|
handleRemoveTableTest={handleRemoveTableTest}
|
||||||
|
testCase={tableTestCase}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{columnsData.map((data, index) => {
|
||||||
|
return (
|
||||||
|
<div className="tw-mb-5" key={index}>
|
||||||
|
<p className="tw-form-label">{`Column Tests - ${data?.name}`}</p>
|
||||||
|
<DataQualityTable
|
||||||
|
handleEditTest={handleEditTest}
|
||||||
|
handleRemoveColumnTest={handleRemoveColumnTest}
|
||||||
|
isTableTest={false}
|
||||||
|
testCase={
|
||||||
|
data.columnTests && data.columnTests?.length > 0
|
||||||
|
? (data.columnTests as TableTestDataType[])
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<ErrorPlaceHolder>
|
||||||
|
<p>No test available.</p>
|
||||||
|
{addTestButton(false)}
|
||||||
|
</ErrorPlaceHolder>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DataQualityTest;
|
@ -0,0 +1,223 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 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 classNames from 'classnames';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { TITLE_FOR_NON_ADMIN_ACTION } from '../../../constants/constants';
|
||||||
|
import { ColumnTestType } from '../../../enums/columnTest.enum';
|
||||||
|
import {
|
||||||
|
TableTestType,
|
||||||
|
TestCaseStatus,
|
||||||
|
} from '../../../generated/tests/tableTest';
|
||||||
|
import {
|
||||||
|
DatasetTestModeType,
|
||||||
|
TableTestDataType,
|
||||||
|
} from '../../../interface/dataQuality.interface';
|
||||||
|
import { isEven } from '../../../utils/CommonUtils';
|
||||||
|
import NonAdminAction from '../../common/non-admin-action/NonAdminAction';
|
||||||
|
import Loader from '../../Loader/Loader';
|
||||||
|
import ConfirmationModal from '../../Modals/ConfirmationModal/ConfirmationModal';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
testCase: TableTestDataType[];
|
||||||
|
isTableTest: boolean;
|
||||||
|
handleEditTest: (mode: DatasetTestModeType, obj: TableTestDataType) => void;
|
||||||
|
handleRemoveTableTest?: (testType: TableTestType) => void;
|
||||||
|
handleRemoveColumnTest?: (
|
||||||
|
columnName: string,
|
||||||
|
testType: ColumnTestType
|
||||||
|
) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DataQualityTable = ({
|
||||||
|
testCase,
|
||||||
|
isTableTest,
|
||||||
|
handleEditTest,
|
||||||
|
handleRemoveTableTest,
|
||||||
|
handleRemoveColumnTest,
|
||||||
|
}: Props) => {
|
||||||
|
const [isConfirmationModalOpen, setIsConfirmationModalOpen] = useState(false);
|
||||||
|
const [deleteSelection, setDeleteSelection] = useState<{
|
||||||
|
data?: TableTestDataType;
|
||||||
|
state: string;
|
||||||
|
}>({
|
||||||
|
data: undefined,
|
||||||
|
state: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleCancelConfirmationModal = () => {
|
||||||
|
setIsConfirmationModalOpen(false);
|
||||||
|
setDeleteSelection({ data: undefined, state: '' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmDelete = (data: TableTestDataType) => {
|
||||||
|
setDeleteSelection({ data, state: '' });
|
||||||
|
setIsConfirmationModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (data: TableTestDataType) => {
|
||||||
|
if (isTableTest) {
|
||||||
|
handleRemoveTableTest &&
|
||||||
|
handleRemoveTableTest(data.testCase.tableTestType as TableTestType);
|
||||||
|
} else {
|
||||||
|
handleRemoveColumnTest &&
|
||||||
|
handleRemoveColumnTest(
|
||||||
|
data?.columnName || '',
|
||||||
|
data.testCase?.columnTestType as ColumnTestType
|
||||||
|
);
|
||||||
|
}
|
||||||
|
handleCancelConfirmationModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="tw-table-responsive">
|
||||||
|
<table className="tw-w-full">
|
||||||
|
<thead>
|
||||||
|
<tr className="tableHead-row">
|
||||||
|
<th className="tableHead-cell">Test Case</th>
|
||||||
|
<th className="tableHead-cell">Config</th>
|
||||||
|
<th className="tableHead-cell">Last Run</th>
|
||||||
|
<th className="tableHead-cell">Value</th>
|
||||||
|
<th className="tableHead-cell">Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="tableBody">
|
||||||
|
{testCase.map((column, index) => {
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
className={classNames(
|
||||||
|
'tableBody-row',
|
||||||
|
!isEven(index + 1) ? 'odd-row' : null
|
||||||
|
)}
|
||||||
|
data-testid="column"
|
||||||
|
id={column.name}
|
||||||
|
key={index}>
|
||||||
|
<td className="tableBody-cell tw-w-3/12">
|
||||||
|
<span>
|
||||||
|
{isTableTest
|
||||||
|
? column.testCase.tableTestType
|
||||||
|
: column.testCase.columnTestType}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="tableBody-cell tw-w-2/12">
|
||||||
|
{!isEmpty(column.testCase?.config) && column.testCase?.config
|
||||||
|
? Object.entries(column.testCase?.config).map((d, i) => (
|
||||||
|
<p key={i}>{`${d[0]}: ${
|
||||||
|
!isEmpty(d[1]) ? d[1] : '--'
|
||||||
|
}`}</p>
|
||||||
|
))
|
||||||
|
: '--'}
|
||||||
|
</td>
|
||||||
|
<td className="tableBody-cell tw-w-1/12">
|
||||||
|
{column.results && column.results.length > 0 ? (
|
||||||
|
<span
|
||||||
|
className={classNames(
|
||||||
|
'tw-block tw-w-full tw-h-full tw-text-white tw-text-center tw-py-1',
|
||||||
|
{
|
||||||
|
'tw-bg-success':
|
||||||
|
column.results[0].testCaseStatus ===
|
||||||
|
TestCaseStatus.Success,
|
||||||
|
'tw-bg-failed':
|
||||||
|
column.results[0].testCaseStatus ===
|
||||||
|
TestCaseStatus.Failed,
|
||||||
|
'tw-bg-status-queued':
|
||||||
|
column.results[0].testCaseStatus ===
|
||||||
|
TestCaseStatus.Aborted,
|
||||||
|
}
|
||||||
|
)}>
|
||||||
|
{column.results[0].testCaseStatus}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
'--'
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="tableBody-cell tw-w-4/12">
|
||||||
|
<span>
|
||||||
|
{column.results && column.results.length > 0
|
||||||
|
? column.results[0].result || '--'
|
||||||
|
: '--'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="tableBody-cell tw-w-2/12">
|
||||||
|
<div className="tw-flex tw-items-center">
|
||||||
|
<NonAdminAction
|
||||||
|
position="left"
|
||||||
|
title={TITLE_FOR_NON_ADMIN_ACTION}>
|
||||||
|
<button
|
||||||
|
className="link-text tw-mr-2"
|
||||||
|
data-testid="edit"
|
||||||
|
onClick={() =>
|
||||||
|
handleEditTest(
|
||||||
|
isTableTest ? 'table' : 'column',
|
||||||
|
column
|
||||||
|
)
|
||||||
|
}>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
</NonAdminAction>
|
||||||
|
<NonAdminAction
|
||||||
|
position="left"
|
||||||
|
title={TITLE_FOR_NON_ADMIN_ACTION}>
|
||||||
|
<button
|
||||||
|
className="link-text tw-mr-2"
|
||||||
|
data-testid="delete"
|
||||||
|
onClick={() => confirmDelete(column)}>
|
||||||
|
{deleteSelection.data?.id === column.id ? (
|
||||||
|
deleteSelection.state === 'success' ? (
|
||||||
|
<i aria-hidden="true" className="fa fa-check" />
|
||||||
|
) : (
|
||||||
|
<Loader size="small" type="default" />
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
'Delete'
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</NonAdminAction>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{isConfirmationModalOpen && (
|
||||||
|
<ConfirmationModal
|
||||||
|
bodyText={`You want to delete test ${
|
||||||
|
deleteSelection.data?.testCase?.columnTestType ||
|
||||||
|
deleteSelection.data?.testCase?.tableTestType
|
||||||
|
} permanently? This action cannot be reverted.`}
|
||||||
|
cancelText="Discard"
|
||||||
|
confirmButtonCss="tw-bg-error hover:tw-bg-error focus:tw-bg-error"
|
||||||
|
confirmText={
|
||||||
|
deleteSelection.state === 'waiting' ? (
|
||||||
|
<Loader size="small" type="white" />
|
||||||
|
) : deleteSelection.state === 'success' ? (
|
||||||
|
<i aria-hidden="true" className="fa fa-check" />
|
||||||
|
) : (
|
||||||
|
'Delete'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
header="Are you sure?"
|
||||||
|
onCancel={handleCancelConfirmationModal}
|
||||||
|
onConfirm={() =>
|
||||||
|
handleDelete(deleteSelection.data as TableTestDataType)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DataQualityTable;
|
@ -43,6 +43,7 @@ import Description from '../common/description/Description';
|
|||||||
import EntityPageInfo from '../common/entityPageInfo/EntityPageInfo';
|
import EntityPageInfo from '../common/entityPageInfo/EntityPageInfo';
|
||||||
import TabsPane from '../common/TabsPane/TabsPane';
|
import TabsPane from '../common/TabsPane/TabsPane';
|
||||||
import PageContainer from '../containers/PageContainer';
|
import PageContainer from '../containers/PageContainer';
|
||||||
|
import DataQualityTab from '../DataQualityTab/DataQualityTab';
|
||||||
import Entitylineage from '../EntityLineage/EntityLineage.component';
|
import Entitylineage from '../EntityLineage/EntityLineage.component';
|
||||||
import FrequentlyJoinedTables from '../FrequentlyJoinedTables/FrequentlyJoinedTables.component';
|
import FrequentlyJoinedTables from '../FrequentlyJoinedTables/FrequentlyJoinedTables.component';
|
||||||
import ManageTab from '../ManageTab/ManageTab.component';
|
import ManageTab from '../ManageTab/ManageTab.component';
|
||||||
@ -100,7 +101,16 @@ const DatasetDetails: React.FC<DatasetDetailsProps> = ({
|
|||||||
postFeedHandler,
|
postFeedHandler,
|
||||||
feedCount,
|
feedCount,
|
||||||
entityFieldThreadCount,
|
entityFieldThreadCount,
|
||||||
|
testMode,
|
||||||
|
tableTestCase,
|
||||||
|
handleTestModeChange,
|
||||||
createThread,
|
createThread,
|
||||||
|
handleAddTableTestCase,
|
||||||
|
handleAddColumnTestCase,
|
||||||
|
showTestForm,
|
||||||
|
handleShowTestForm,
|
||||||
|
handleRemoveTableTest,
|
||||||
|
handleRemoveColumnTest,
|
||||||
}: DatasetDetailsProps) => {
|
}: DatasetDetailsProps) => {
|
||||||
const { isAuthDisabled } = useAuth();
|
const { isAuthDisabled } = useAuth();
|
||||||
const [isEdit, setIsEdit] = useState(false);
|
const [isEdit, setIsEdit] = useState(false);
|
||||||
@ -209,6 +219,17 @@ const DatasetDetails: React.FC<DatasetDetailsProps> = ({
|
|||||||
isProtected: false,
|
isProtected: false,
|
||||||
position: 5,
|
position: 5,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Data Quality',
|
||||||
|
icon: {
|
||||||
|
alt: 'data-quality',
|
||||||
|
name: 'icon-quality',
|
||||||
|
title: 'Data Quality',
|
||||||
|
selectedName: '',
|
||||||
|
},
|
||||||
|
isProtected: false,
|
||||||
|
position: 6,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Lineage',
|
name: 'Lineage',
|
||||||
icon: {
|
icon: {
|
||||||
@ -218,7 +239,7 @@ const DatasetDetails: React.FC<DatasetDetailsProps> = ({
|
|||||||
selectedName: 'icon-lineagecolor',
|
selectedName: 'icon-lineagecolor',
|
||||||
},
|
},
|
||||||
isProtected: false,
|
isProtected: false,
|
||||||
position: 6,
|
position: 7,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'DBT',
|
name: 'DBT',
|
||||||
@ -230,7 +251,7 @@ const DatasetDetails: React.FC<DatasetDetailsProps> = ({
|
|||||||
},
|
},
|
||||||
isProtected: false,
|
isProtected: false,
|
||||||
isHidden: !dataModel?.sql,
|
isHidden: !dataModel?.sql,
|
||||||
position: 7,
|
position: 8,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Manage',
|
name: 'Manage',
|
||||||
@ -243,7 +264,7 @@ const DatasetDetails: React.FC<DatasetDetailsProps> = ({
|
|||||||
isProtected: false,
|
isProtected: false,
|
||||||
isHidden: deleted,
|
isHidden: deleted,
|
||||||
protectedState: !owner || hasEditAccess(),
|
protectedState: !owner || hasEditAccess(),
|
||||||
position: 8,
|
position: 9,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -600,7 +621,23 @@ const DatasetDetails: React.FC<DatasetDetailsProps> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === 6 && (
|
{activeTab === 6 && (
|
||||||
|
<DataQualityTab
|
||||||
|
columnOptions={columns}
|
||||||
|
handleAddColumnTestCase={handleAddColumnTestCase}
|
||||||
|
handleAddTableTestCase={handleAddTableTestCase}
|
||||||
|
handleRemoveColumnTest={handleRemoveColumnTest}
|
||||||
|
handleRemoveTableTest={handleRemoveTableTest}
|
||||||
|
handleShowTestForm={handleShowTestForm}
|
||||||
|
handleTestModeChange={handleTestModeChange}
|
||||||
|
showTestForm={showTestForm}
|
||||||
|
tableTestCase={tableTestCase}
|
||||||
|
testMode={testMode}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 7 && (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
location.pathname.includes(ROUTES.TOUR)
|
location.pathname.includes(ROUTES.TOUR)
|
||||||
@ -622,7 +659,7 @@ const DatasetDetails: React.FC<DatasetDetailsProps> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{activeTab === 7 && Boolean(dataModel?.sql) && (
|
{activeTab === 8 && Boolean(dataModel?.sql) && (
|
||||||
<div className="tw-border tw-border-main tw-rounded-md tw-py-4 tw-h-full cm-h-full">
|
<div className="tw-border tw-border-main tw-rounded-md tw-py-4 tw-h-full cm-h-full">
|
||||||
<SchemaEditor
|
<SchemaEditor
|
||||||
className="tw-h-full"
|
className="tw-h-full"
|
||||||
@ -631,7 +668,7 @@ const DatasetDetails: React.FC<DatasetDetailsProps> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{activeTab === 8 && !deleted && (
|
{activeTab === 9 && !deleted && (
|
||||||
<div>
|
<div>
|
||||||
<ManageTab
|
<ManageTab
|
||||||
currentTier={tier?.tagFQN}
|
currentTier={tier?.tagFQN}
|
||||||
|
@ -19,7 +19,9 @@ import {
|
|||||||
LineagePos,
|
LineagePos,
|
||||||
LoadingNodeState,
|
LoadingNodeState,
|
||||||
} from 'Models';
|
} from 'Models';
|
||||||
|
import { ColumnTestType } from '../../enums/columnTest.enum';
|
||||||
import { CreateThread } from '../../generated/api/feed/createThread';
|
import { CreateThread } from '../../generated/api/feed/createThread';
|
||||||
|
import { CreateTableTest } from '../../generated/api/tests/createTableTest';
|
||||||
import {
|
import {
|
||||||
EntityReference,
|
EntityReference,
|
||||||
Table,
|
Table,
|
||||||
@ -28,8 +30,13 @@ import {
|
|||||||
TypeUsedToReturnUsageDetailsOfAnEntity,
|
TypeUsedToReturnUsageDetailsOfAnEntity,
|
||||||
} from '../../generated/entity/data/table';
|
} from '../../generated/entity/data/table';
|
||||||
import { User } from '../../generated/entity/teams/user';
|
import { User } from '../../generated/entity/teams/user';
|
||||||
|
import { TableTest, TableTestType } from '../../generated/tests/tableTest';
|
||||||
import { EntityLineage } from '../../generated/type/entityLineage';
|
import { EntityLineage } from '../../generated/type/entityLineage';
|
||||||
import { TagLabel } from '../../generated/type/tagLabel';
|
import { TagLabel } from '../../generated/type/tagLabel';
|
||||||
|
import {
|
||||||
|
CreateColumnTest,
|
||||||
|
DatasetTestModeType,
|
||||||
|
} from '../../interface/dataQuality.interface';
|
||||||
import { TitleBreadcrumbProps } from '../common/title-breadcrumb/title-breadcrumb.interface';
|
import { TitleBreadcrumbProps } from '../common/title-breadcrumb/title-breadcrumb.interface';
|
||||||
import { Edge, EdgeData } from '../EntityLineage/EntityLineage.interface';
|
import { Edge, EdgeData } from '../EntityLineage/EntityLineage.interface';
|
||||||
|
|
||||||
@ -68,6 +75,11 @@ export interface DatasetDetailsProps {
|
|||||||
isentityThreadLoading: boolean;
|
isentityThreadLoading: boolean;
|
||||||
feedCount: number;
|
feedCount: number;
|
||||||
entityFieldThreadCount: EntityFieldThreadCount[];
|
entityFieldThreadCount: EntityFieldThreadCount[];
|
||||||
|
testMode: DatasetTestModeType;
|
||||||
|
tableTestCase: TableTest[];
|
||||||
|
showTestForm: boolean;
|
||||||
|
handleShowTestForm: (value: boolean) => void;
|
||||||
|
handleTestModeChange: (mode: DatasetTestModeType) => void;
|
||||||
createThread: (data: CreateThread) => void;
|
createThread: (data: CreateThread) => void;
|
||||||
setActiveTabHandler: (value: number) => void;
|
setActiveTabHandler: (value: number) => void;
|
||||||
followTableHandler: () => void;
|
followTableHandler: () => void;
|
||||||
@ -81,4 +93,11 @@ export interface DatasetDetailsProps {
|
|||||||
removeLineageHandler: (data: EdgeData) => void;
|
removeLineageHandler: (data: EdgeData) => void;
|
||||||
entityLineageHandler: (lineage: EntityLineage) => void;
|
entityLineageHandler: (lineage: EntityLineage) => void;
|
||||||
postFeedHandler: (value: string, id: string) => void;
|
postFeedHandler: (value: string, id: string) => void;
|
||||||
|
handleAddTableTestCase: (data: CreateTableTest) => void;
|
||||||
|
handleAddColumnTestCase: (data: CreateColumnTest) => void;
|
||||||
|
handleRemoveTableTest: (testType: TableTestType) => void;
|
||||||
|
handleRemoveColumnTest: (
|
||||||
|
columnName: string,
|
||||||
|
testType: ColumnTestType
|
||||||
|
) => void;
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@ import {
|
|||||||
} from '../../generated/entity/data/table';
|
} from '../../generated/entity/data/table';
|
||||||
import { EntityLineage } from '../../generated/type/entityLineage';
|
import { EntityLineage } from '../../generated/type/entityLineage';
|
||||||
import { TagLabel } from '../../generated/type/tagLabel';
|
import { TagLabel } from '../../generated/type/tagLabel';
|
||||||
|
import { DatasetTestModeType } from '../../interface/dataQuality.interface';
|
||||||
import DatasetDetails from './DatasetDetails.component';
|
import DatasetDetails from './DatasetDetails.component';
|
||||||
import { DatasetOwner } from './DatasetDetails.interface';
|
import { DatasetOwner } from './DatasetDetails.interface';
|
||||||
|
|
||||||
@ -83,7 +84,16 @@ const DatasetDetailsProps = {
|
|||||||
postFeedHandler: jest.fn(),
|
postFeedHandler: jest.fn(),
|
||||||
feedCount: 0,
|
feedCount: 0,
|
||||||
entityFieldThreadCount: [],
|
entityFieldThreadCount: [],
|
||||||
|
showTestForm: false,
|
||||||
|
testMode: 'table' as DatasetTestModeType,
|
||||||
|
handleAddTableTestCase: jest.fn(),
|
||||||
|
tableTestCase: [],
|
||||||
|
handleAddColumnTestCase: jest.fn(),
|
||||||
createThread: jest.fn(),
|
createThread: jest.fn(),
|
||||||
|
handleShowTestForm: jest.fn(),
|
||||||
|
handleRemoveTableTest: jest.fn(),
|
||||||
|
handleRemoveColumnTest: jest.fn(),
|
||||||
|
handleTestModeChange: jest.fn(),
|
||||||
};
|
};
|
||||||
jest.mock('../ManageTab/ManageTab.component', () => {
|
jest.mock('../ManageTab/ManageTab.component', () => {
|
||||||
return jest.fn().mockReturnValue(<p>ManageTab</p>);
|
return jest.fn().mockReturnValue(<p>ManageTab</p>);
|
||||||
|
@ -73,13 +73,16 @@ const DropDownList: FunctionComponent<DropDownListProp> = ({
|
|||||||
aria-disabled={item.disabled as boolean}
|
aria-disabled={item.disabled as boolean}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'tw-text-gray-700 tw-block tw-px-4 tw-py-2 tw-text-sm hover:tw-bg-body-hover tw-cursor-pointer',
|
'tw-text-gray-700 tw-block tw-px-4 tw-py-2 tw-text-sm hover:tw-bg-body-hover tw-cursor-pointer',
|
||||||
!isNil(value) && item.value === value ? 'tw-bg-primary-lite' : null
|
!isNil(value) && item.value === value ? 'tw-bg-primary-lite' : null,
|
||||||
|
{
|
||||||
|
'tw-cursor-not-allowed': item.disabled,
|
||||||
|
}
|
||||||
)}
|
)}
|
||||||
data-testid="list-item"
|
data-testid="list-item"
|
||||||
id={`menu-item-${index}`}
|
id={`menu-item-${index}`}
|
||||||
key={index}
|
key={index}
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
onClick={(e) => onSelect && onSelect(e, item.value)}>
|
onClick={(e) => !item.disabled && onSelect?.(e, item.value)}>
|
||||||
<p className="tw-truncate tw-w-52" title={item.name as string}>
|
<p className="tw-truncate tw-w-52" title={item.name as string}>
|
||||||
{item.name}
|
{item.name}
|
||||||
</p>
|
</p>
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 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 enum ColumnTestType {
|
||||||
|
columnValuesToBeUnique = 'columnValuesToBeUnique',
|
||||||
|
columnValuesToBeNotNull = 'columnValuesToBeNotNull',
|
||||||
|
columnValuesToMatchRegex = 'columnValuesToMatchRegex',
|
||||||
|
columnValuesToBeNotInSet = 'columnValuesToBeNotInSet',
|
||||||
|
columnValuesToBeBetween = 'columnValuesToBeBetween',
|
||||||
|
columnValuesMissingCountToBeEqual = 'columnValuesMissingCountToBeEqual',
|
||||||
|
columnValueLengthsToBeBetween = 'columnValueLengthsToBeBetween',
|
||||||
|
}
|
@ -46,4 +46,5 @@ export enum TabSpecificField {
|
|||||||
CHARTS = 'charts',
|
CHARTS = 'charts',
|
||||||
TASKS = 'tasks',
|
TASKS = 'tasks',
|
||||||
TABLE_QUERIES = 'tableQueries',
|
TABLE_QUERIES = 'tableQueries',
|
||||||
|
TESTS = 'tests',
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,159 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
/*
|
||||||
|
* Copyright 2021 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TableTest is a test definition to capture data quality tests against tables and columns.
|
||||||
|
*/
|
||||||
|
export interface CreateTableTest {
|
||||||
|
/**
|
||||||
|
* Description of the testcase.
|
||||||
|
*/
|
||||||
|
description?: string;
|
||||||
|
executionFrequency?: TestCaseExecutionFrequency;
|
||||||
|
/**
|
||||||
|
* Owner of this Pipeline.
|
||||||
|
*/
|
||||||
|
owner?: EntityReference;
|
||||||
|
result?: TestCaseResult;
|
||||||
|
testCase: TableTestCase;
|
||||||
|
/**
|
||||||
|
* Last update time corresponding to the new version of the entity in Unix epoch time
|
||||||
|
* milliseconds.
|
||||||
|
*/
|
||||||
|
updatedAt?: number;
|
||||||
|
/**
|
||||||
|
* User who made the update.
|
||||||
|
*/
|
||||||
|
updatedBy?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How often the test case should run.
|
||||||
|
*/
|
||||||
|
export enum TestCaseExecutionFrequency {
|
||||||
|
Daily = 'Daily',
|
||||||
|
Hourly = 'Hourly',
|
||||||
|
Weekly = 'Weekly',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Owner of this Pipeline.
|
||||||
|
*
|
||||||
|
* This schema defines the EntityReference type used for referencing an entity.
|
||||||
|
* EntityReference is used for capturing relationships from one entity to another. For
|
||||||
|
* example, a table has an attribute called database of type EntityReference that captures
|
||||||
|
* the relationship of a table `belongs to a` database.
|
||||||
|
*/
|
||||||
|
export interface EntityReference {
|
||||||
|
/**
|
||||||
|
* Optional description of entity.
|
||||||
|
*/
|
||||||
|
description?: string;
|
||||||
|
/**
|
||||||
|
* Display Name that identifies this entity.
|
||||||
|
*/
|
||||||
|
displayName?: string;
|
||||||
|
/**
|
||||||
|
* Link to the entity resource.
|
||||||
|
*/
|
||||||
|
href?: string;
|
||||||
|
/**
|
||||||
|
* Unique identifier that identifies an entity instance.
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* Name of the entity instance. For entities such as tables, databases where the name is not
|
||||||
|
* unique, fullyQualifiedName is returned in this field.
|
||||||
|
*/
|
||||||
|
name?: string;
|
||||||
|
/**
|
||||||
|
* Entity type/class name - Examples: `database`, `table`, `metrics`, `databaseService`,
|
||||||
|
* `dashboardService`...
|
||||||
|
*/
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schema to capture test case result.
|
||||||
|
*/
|
||||||
|
export interface TestCaseResult {
|
||||||
|
/**
|
||||||
|
* Data one which profile is taken.
|
||||||
|
*/
|
||||||
|
executionTime?: number;
|
||||||
|
/**
|
||||||
|
* Details of test case results.
|
||||||
|
*/
|
||||||
|
result?: string;
|
||||||
|
/**
|
||||||
|
* sample data to capture rows/columns that didn't match the expressed testcase.
|
||||||
|
*/
|
||||||
|
sampleData?: string;
|
||||||
|
/**
|
||||||
|
* Status of Test Case run.
|
||||||
|
*/
|
||||||
|
testCaseStatus?: TestCaseStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status of Test Case run.
|
||||||
|
*/
|
||||||
|
export enum TestCaseStatus {
|
||||||
|
Aborted = 'Aborted',
|
||||||
|
Failed = 'Failed',
|
||||||
|
Success = 'Success',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Table Test Case.
|
||||||
|
*/
|
||||||
|
export interface TableTestCase {
|
||||||
|
config?: TableRowCountToBeBetween;
|
||||||
|
tableTestType?: TableTestType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This schema defines the test TableRowCountToEqual. Test the number of rows equal to a
|
||||||
|
* value.
|
||||||
|
*
|
||||||
|
* This scheam defines the test TableRowCountToBeBetween. Test the number of rows to between
|
||||||
|
* to two values.
|
||||||
|
*
|
||||||
|
* This scheam defines the test TableColumnCountToEqual. Test the number of columns equal to
|
||||||
|
* a value.
|
||||||
|
*/
|
||||||
|
export interface TableRowCountToBeBetween {
|
||||||
|
/**
|
||||||
|
* Expected number of rows {value}
|
||||||
|
*
|
||||||
|
* Expected number of columns to equal to a {value}
|
||||||
|
*/
|
||||||
|
value?: number;
|
||||||
|
/**
|
||||||
|
* Expected number of rows should be lower than or equal to {maxValue}. if maxValue is not
|
||||||
|
* included, minValue is treated as lowerBound and there will eb no maximum number of rows
|
||||||
|
*/
|
||||||
|
maxValue?: number;
|
||||||
|
/**
|
||||||
|
* Expected number of rows should be greater than or equal to {minValue}. If minValue is not
|
||||||
|
* included, maxValue is treated as upperBound and there will be no minimum number of rows
|
||||||
|
*/
|
||||||
|
minValue?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum TableTestType {
|
||||||
|
TableColumnCountToEqual = 'tableColumnCountToEqual',
|
||||||
|
TableRowCountToBeBetween = 'tableRowCountToBeBetween',
|
||||||
|
TableRowCountToEqual = 'tableRowCountToEqual',
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
/*
|
||||||
|
* Copyright 2021 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This scheam defines the test TableColumnCountToEqual. Test the number of columns equal to
|
||||||
|
* a value.
|
||||||
|
*/
|
||||||
|
export interface TableColumnCountToEqual {
|
||||||
|
/**
|
||||||
|
* Expected number of columns to equal to a {value}
|
||||||
|
*/
|
||||||
|
value: number;
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
/*
|
||||||
|
* Copyright 2021 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This scheam defines the test TableRowCountToBeBetween. Test the number of rows to between
|
||||||
|
* to two values.
|
||||||
|
*/
|
||||||
|
export interface TableRowCountToBeBetween {
|
||||||
|
/**
|
||||||
|
* Expected number of rows should be lower than or equal to {maxValue}. if maxValue is not
|
||||||
|
* included, minValue is treated as lowerBound and there will eb no maximum number of rows
|
||||||
|
*/
|
||||||
|
maxValue?: number;
|
||||||
|
/**
|
||||||
|
* Expected number of rows should be greater than or equal to {minValue}. If minValue is not
|
||||||
|
* included, maxValue is treated as upperBound and there will be no minimum number of rows
|
||||||
|
*/
|
||||||
|
minValue?: number;
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
/*
|
||||||
|
* Copyright 2021 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This schema defines the test TableRowCountToEqual. Test the number of rows equal to a
|
||||||
|
* value.
|
||||||
|
*/
|
||||||
|
export interface TableRowCountToEqual {
|
||||||
|
/**
|
||||||
|
* Expected number of rows {value}
|
||||||
|
*/
|
||||||
|
value: number;
|
||||||
|
}
|
@ -0,0 +1,170 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
/*
|
||||||
|
* Copyright 2021 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TableTest is a test definition to capture data quality tests against tables and columns.
|
||||||
|
*/
|
||||||
|
export interface TableTest {
|
||||||
|
/**
|
||||||
|
* Description of the testcase.
|
||||||
|
*/
|
||||||
|
description?: string;
|
||||||
|
executionFrequency?: TestCaseExecutionFrequency;
|
||||||
|
/**
|
||||||
|
* Unique identifier of this table instance.
|
||||||
|
*/
|
||||||
|
id?: string;
|
||||||
|
/**
|
||||||
|
* Name that identifies this test case.
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* Owner of this Pipeline.
|
||||||
|
*/
|
||||||
|
owner?: EntityReference;
|
||||||
|
/**
|
||||||
|
* List of results of the test case.
|
||||||
|
*/
|
||||||
|
results?: TestCaseResult[];
|
||||||
|
testCase: TableTestCase;
|
||||||
|
/**
|
||||||
|
* Last update time corresponding to the new version of the entity in Unix epoch time
|
||||||
|
* milliseconds.
|
||||||
|
*/
|
||||||
|
updatedAt?: number;
|
||||||
|
/**
|
||||||
|
* User who made the update.
|
||||||
|
*/
|
||||||
|
updatedBy?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How often the test case should run.
|
||||||
|
*/
|
||||||
|
export enum TestCaseExecutionFrequency {
|
||||||
|
Daily = 'Daily',
|
||||||
|
Hourly = 'Hourly',
|
||||||
|
Weekly = 'Weekly',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Owner of this Pipeline.
|
||||||
|
*
|
||||||
|
* This schema defines the EntityReference type used for referencing an entity.
|
||||||
|
* EntityReference is used for capturing relationships from one entity to another. For
|
||||||
|
* example, a table has an attribute called database of type EntityReference that captures
|
||||||
|
* the relationship of a table `belongs to a` database.
|
||||||
|
*/
|
||||||
|
export interface EntityReference {
|
||||||
|
/**
|
||||||
|
* Optional description of entity.
|
||||||
|
*/
|
||||||
|
description?: string;
|
||||||
|
/**
|
||||||
|
* Display Name that identifies this entity.
|
||||||
|
*/
|
||||||
|
displayName?: string;
|
||||||
|
/**
|
||||||
|
* Link to the entity resource.
|
||||||
|
*/
|
||||||
|
href?: string;
|
||||||
|
/**
|
||||||
|
* Unique identifier that identifies an entity instance.
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* Name of the entity instance. For entities such as tables, databases where the name is not
|
||||||
|
* unique, fullyQualifiedName is returned in this field.
|
||||||
|
*/
|
||||||
|
name?: string;
|
||||||
|
/**
|
||||||
|
* Entity type/class name - Examples: `database`, `table`, `metrics`, `databaseService`,
|
||||||
|
* `dashboardService`...
|
||||||
|
*/
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schema to capture test case result.
|
||||||
|
*/
|
||||||
|
export interface TestCaseResult {
|
||||||
|
/**
|
||||||
|
* Data one which profile is taken.
|
||||||
|
*/
|
||||||
|
executionTime?: number;
|
||||||
|
/**
|
||||||
|
* Details of test case results.
|
||||||
|
*/
|
||||||
|
result?: string;
|
||||||
|
/**
|
||||||
|
* sample data to capture rows/columns that didn't match the expressed testcase.
|
||||||
|
*/
|
||||||
|
sampleData?: string;
|
||||||
|
/**
|
||||||
|
* Status of Test Case run.
|
||||||
|
*/
|
||||||
|
testCaseStatus?: TestCaseStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status of Test Case run.
|
||||||
|
*/
|
||||||
|
export enum TestCaseStatus {
|
||||||
|
Aborted = 'Aborted',
|
||||||
|
Failed = 'Failed',
|
||||||
|
Success = 'Success',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Table Test Case.
|
||||||
|
*/
|
||||||
|
export interface TableTestCase {
|
||||||
|
config?: TableRowCountToBeBetween;
|
||||||
|
tableTestType?: TableTestType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This schema defines the test TableRowCountToEqual. Test the number of rows equal to a
|
||||||
|
* value.
|
||||||
|
*
|
||||||
|
* This scheam defines the test TableRowCountToBeBetween. Test the number of rows to between
|
||||||
|
* to two values.
|
||||||
|
*
|
||||||
|
* This scheam defines the test TableColumnCountToEqual. Test the number of columns equal to
|
||||||
|
* a value.
|
||||||
|
*/
|
||||||
|
export interface TableRowCountToBeBetween {
|
||||||
|
/**
|
||||||
|
* Expected number of rows {value}
|
||||||
|
*
|
||||||
|
* Expected number of columns to equal to a {value}
|
||||||
|
*/
|
||||||
|
value?: number;
|
||||||
|
/**
|
||||||
|
* Expected number of rows should be lower than or equal to {maxValue}. if maxValue is not
|
||||||
|
* included, minValue is treated as lowerBound and there will eb no maximum number of rows
|
||||||
|
*/
|
||||||
|
maxValue?: number;
|
||||||
|
/**
|
||||||
|
* Expected number of rows should be greater than or equal to {minValue}. If minValue is not
|
||||||
|
* included, maxValue is treated as upperBound and there will be no minimum number of rows
|
||||||
|
*/
|
||||||
|
minValue?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum TableTestType {
|
||||||
|
TableColumnCountToEqual = 'tableColumnCountToEqual',
|
||||||
|
TableRowCountToBeBetween = 'tableRowCountToBeBetween',
|
||||||
|
TableRowCountToEqual = 'tableRowCountToEqual',
|
||||||
|
}
|
@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 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 { ColumnTestType } from '../enums/columnTest.enum';
|
||||||
|
import { Column } from '../generated/entity/data/table';
|
||||||
|
import {
|
||||||
|
EntityReference,
|
||||||
|
TableTestType,
|
||||||
|
TestCaseExecutionFrequency,
|
||||||
|
TestCaseResult,
|
||||||
|
} from '../generated/tests/tableTest';
|
||||||
|
|
||||||
|
export interface TestCaseConfigType {
|
||||||
|
value?: number;
|
||||||
|
maxValue?: number;
|
||||||
|
minValue?: number;
|
||||||
|
regex?: string;
|
||||||
|
forbiddenValues?: Array<number | string>;
|
||||||
|
missingCountValue?: number;
|
||||||
|
missingValueMatch?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateColumnTest {
|
||||||
|
id?: string;
|
||||||
|
columnName: string;
|
||||||
|
description?: string;
|
||||||
|
executionFrequency?: TestCaseExecutionFrequency;
|
||||||
|
owner?: EntityReference;
|
||||||
|
testCase: {
|
||||||
|
columnTestType: ColumnTestType;
|
||||||
|
config?: TestCaseConfigType;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DatasetTestModeType = 'table' | 'column';
|
||||||
|
|
||||||
|
export interface ModifiedTableColumn extends Column {
|
||||||
|
columnTests?: CreateColumnTest[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TableTestDataType {
|
||||||
|
description?: string;
|
||||||
|
executionFrequency?: TestCaseExecutionFrequency;
|
||||||
|
columnName?: string;
|
||||||
|
id?: string;
|
||||||
|
name: string;
|
||||||
|
owner?: EntityReference;
|
||||||
|
results?: TestCaseResult[];
|
||||||
|
testCase: {
|
||||||
|
config?: TestCaseConfigType;
|
||||||
|
tableTestType?: TableTestType;
|
||||||
|
columnTestType?: ColumnTestType;
|
||||||
|
};
|
||||||
|
updatedAt?: number;
|
||||||
|
updatedBy?: string;
|
||||||
|
}
|
@ -35,7 +35,11 @@ import {
|
|||||||
import { getLineageByFQN } from '../../axiosAPIs/lineageAPI';
|
import { getLineageByFQN } from '../../axiosAPIs/lineageAPI';
|
||||||
import { addLineage, deleteLineageEdge } from '../../axiosAPIs/miscAPI';
|
import { addLineage, deleteLineageEdge } from '../../axiosAPIs/miscAPI';
|
||||||
import {
|
import {
|
||||||
|
addColumnTestCase,
|
||||||
addFollower,
|
addFollower,
|
||||||
|
addTableTestCase,
|
||||||
|
deleteColumnTestCase,
|
||||||
|
deleteTableTestCase,
|
||||||
getTableDetailsByFQN,
|
getTableDetailsByFQN,
|
||||||
patchTableDetails,
|
patchTableDetails,
|
||||||
removeFollower,
|
removeFollower,
|
||||||
@ -54,10 +58,13 @@ import {
|
|||||||
getTableTabPath,
|
getTableTabPath,
|
||||||
getVersionPath,
|
getVersionPath,
|
||||||
} from '../../constants/constants';
|
} from '../../constants/constants';
|
||||||
|
import { ColumnTestType } from '../../enums/columnTest.enum';
|
||||||
import { EntityType, TabSpecificField } from '../../enums/entity.enum';
|
import { EntityType, TabSpecificField } from '../../enums/entity.enum';
|
||||||
import { ServiceCategory } from '../../enums/service.enum';
|
import { ServiceCategory } from '../../enums/service.enum';
|
||||||
import { CreateThread } from '../../generated/api/feed/createThread';
|
import { CreateThread } from '../../generated/api/feed/createThread';
|
||||||
|
import { CreateTableTest } from '../../generated/api/tests/createTableTest';
|
||||||
import {
|
import {
|
||||||
|
Column,
|
||||||
EntityReference,
|
EntityReference,
|
||||||
Table,
|
Table,
|
||||||
TableData,
|
TableData,
|
||||||
@ -65,9 +72,15 @@ import {
|
|||||||
TypeUsedToReturnUsageDetailsOfAnEntity,
|
TypeUsedToReturnUsageDetailsOfAnEntity,
|
||||||
} from '../../generated/entity/data/table';
|
} from '../../generated/entity/data/table';
|
||||||
import { User } from '../../generated/entity/teams/user';
|
import { User } from '../../generated/entity/teams/user';
|
||||||
|
import { TableTest, TableTestType } from '../../generated/tests/tableTest';
|
||||||
import { EntityLineage } from '../../generated/type/entityLineage';
|
import { EntityLineage } from '../../generated/type/entityLineage';
|
||||||
import { TagLabel } from '../../generated/type/tagLabel';
|
import { TagLabel } from '../../generated/type/tagLabel';
|
||||||
import useToastContext from '../../hooks/useToastContext';
|
import useToastContext from '../../hooks/useToastContext';
|
||||||
|
import {
|
||||||
|
CreateColumnTest,
|
||||||
|
DatasetTestModeType,
|
||||||
|
ModifiedTableColumn,
|
||||||
|
} from '../../interface/dataQuality.interface';
|
||||||
import {
|
import {
|
||||||
addToRecentViewed,
|
addToRecentViewed,
|
||||||
getCurrentUserId,
|
getCurrentUserId,
|
||||||
@ -149,6 +162,19 @@ const DatasetDetailsPage: FunctionComponent = () => {
|
|||||||
EntityFieldThreadCount[]
|
EntityFieldThreadCount[]
|
||||||
>([]);
|
>([]);
|
||||||
|
|
||||||
|
// Data Quality tab state
|
||||||
|
const [testMode, setTestMode] = useState<DatasetTestModeType>('table');
|
||||||
|
const [showTestForm, setShowTestForm] = useState(false);
|
||||||
|
const [tableTestCase, setTableTestCase] = useState<TableTest[]>([]);
|
||||||
|
|
||||||
|
const handleTestModeChange = (mode: DatasetTestModeType) => {
|
||||||
|
setTestMode(mode);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleShowTestForm = (value: boolean) => {
|
||||||
|
setShowTestForm(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) {
|
||||||
@ -161,6 +187,7 @@ const DatasetDetailsPage: FunctionComponent = () => {
|
|||||||
datasetTableTabs[currentTabIndex].path
|
datasetTableTabs[currentTabIndex].path
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
handleShowTestForm(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -252,6 +279,10 @@ const DatasetDetailsPage: FunctionComponent = () => {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
if (res.data.tableTests && res.data.tableTests.length > 0) {
|
||||||
|
setTableTestCase(res.data.tableTests);
|
||||||
|
}
|
||||||
|
|
||||||
addToRecentViewed({
|
addToRecentViewed({
|
||||||
entityType: EntityType.TABLE,
|
entityType: EntityType.TABLE,
|
||||||
fqn: fullyQualifiedName,
|
fqn: fullyQualifiedName,
|
||||||
@ -588,6 +619,113 @@ const DatasetDetailsPage: FunctionComponent = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAddTableTestCase = (data: CreateTableTest) => {
|
||||||
|
addTableTestCase(tableDetails.id, data)
|
||||||
|
.then((res: AxiosResponse) => {
|
||||||
|
const { tableTests } = res.data;
|
||||||
|
let itsNewTest = true;
|
||||||
|
const existingData = tableTestCase.map((test) => {
|
||||||
|
if (test.name === tableTests[0].name) {
|
||||||
|
itsNewTest = false;
|
||||||
|
|
||||||
|
return tableTests[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return test;
|
||||||
|
});
|
||||||
|
if (itsNewTest) {
|
||||||
|
existingData.push(tableTests[0]);
|
||||||
|
}
|
||||||
|
setTableTestCase(existingData);
|
||||||
|
handleShowTestForm(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
showToast({
|
||||||
|
variant: 'error',
|
||||||
|
body: 'Something went wrong.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddColumnTestCase = (data: CreateColumnTest) => {
|
||||||
|
addColumnTestCase(tableDetails.id, data)
|
||||||
|
.then((res: AxiosResponse) => {
|
||||||
|
const columnTestRes = res.data.columns.find(
|
||||||
|
(d: Column) => d.name === data.columnName
|
||||||
|
);
|
||||||
|
const updatedColumns = columns.map((d) => {
|
||||||
|
if (d.name === data.columnName) {
|
||||||
|
const oldTest =
|
||||||
|
(d as ModifiedTableColumn)?.columnTests?.filter(
|
||||||
|
(test) => test.id !== columnTestRes.columnTests[0].id
|
||||||
|
) || [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
...d,
|
||||||
|
columnTests: [...oldTest, columnTestRes.columnTests[0]],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return d;
|
||||||
|
});
|
||||||
|
setColumns(updatedColumns);
|
||||||
|
handleShowTestForm(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
showToast({
|
||||||
|
variant: 'error',
|
||||||
|
body: 'Something went wrong.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveTableTest = (testType: TableTestType) => {
|
||||||
|
deleteTableTestCase(tableDetails.id, testType)
|
||||||
|
.then(() => {
|
||||||
|
const updatedTest = tableTestCase.filter(
|
||||||
|
(d) => d.testCase.tableTestType !== testType
|
||||||
|
);
|
||||||
|
setTableTestCase(updatedTest);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
showToast({
|
||||||
|
variant: 'error',
|
||||||
|
body: 'Something went wrong.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveColumnTest = (
|
||||||
|
columnName: string,
|
||||||
|
testType: ColumnTestType
|
||||||
|
) => {
|
||||||
|
deleteColumnTestCase(tableDetails.id, columnName, testType)
|
||||||
|
.then(() => {
|
||||||
|
const updatedColumns = columns.map((d) => {
|
||||||
|
if (d.name === columnName) {
|
||||||
|
const updatedTest =
|
||||||
|
(d as ModifiedTableColumn)?.columnTests?.filter(
|
||||||
|
(test) => test.testCase.columnTestType !== testType
|
||||||
|
) || [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
...d,
|
||||||
|
columnTests: updatedTest,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return d;
|
||||||
|
});
|
||||||
|
setColumns(updatedColumns);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
showToast({
|
||||||
|
variant: 'error',
|
||||||
|
body: 'Something went wrong.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchTableDetail();
|
fetchTableDetail();
|
||||||
setActiveTab(getCurrentDatasetTab(tab));
|
setActiveTab(getCurrentDatasetTab(tab));
|
||||||
@ -632,6 +770,12 @@ const DatasetDetailsPage: FunctionComponent = () => {
|
|||||||
feedCount={feedCount}
|
feedCount={feedCount}
|
||||||
followTableHandler={followTable}
|
followTableHandler={followTable}
|
||||||
followers={followers}
|
followers={followers}
|
||||||
|
handleAddColumnTestCase={handleAddColumnTestCase}
|
||||||
|
handleAddTableTestCase={handleAddTableTestCase}
|
||||||
|
handleRemoveColumnTest={handleRemoveColumnTest}
|
||||||
|
handleRemoveTableTest={handleRemoveTableTest}
|
||||||
|
handleShowTestForm={handleShowTestForm}
|
||||||
|
handleTestModeChange={handleTestModeChange}
|
||||||
isLineageLoading={isLineageLoading}
|
isLineageLoading={isLineageLoading}
|
||||||
isNodeLoading={isNodeLoading}
|
isNodeLoading={isNodeLoading}
|
||||||
isQueriesLoading={isTableQueriesLoading}
|
isQueriesLoading={isTableQueriesLoading}
|
||||||
@ -646,11 +790,14 @@ const DatasetDetailsPage: FunctionComponent = () => {
|
|||||||
sampleData={sampleData}
|
sampleData={sampleData}
|
||||||
setActiveTabHandler={activeTabHandler}
|
setActiveTabHandler={activeTabHandler}
|
||||||
settingsUpdateHandler={settingsUpdateHandler}
|
settingsUpdateHandler={settingsUpdateHandler}
|
||||||
|
showTestForm={showTestForm}
|
||||||
slashedTableName={slashedTableName}
|
slashedTableName={slashedTableName}
|
||||||
tableDetails={tableDetails}
|
tableDetails={tableDetails}
|
||||||
tableProfile={tableProfile}
|
tableProfile={tableProfile}
|
||||||
tableQueries={tableQueries}
|
tableQueries={tableQueries}
|
||||||
tableTags={tableTags}
|
tableTags={tableTags}
|
||||||
|
tableTestCase={tableTestCase}
|
||||||
|
testMode={testMode}
|
||||||
tier={tier as TagLabel}
|
tier={tier as TagLabel}
|
||||||
unfollowTableHandler={unfollowTable}
|
unfollowTableHandler={unfollowTable}
|
||||||
usageSummary={usageSummary}
|
usageSummary={usageSummary}
|
||||||
|
@ -189,6 +189,12 @@ const TourPage = () => {
|
|||||||
feedCount={0}
|
feedCount={0}
|
||||||
followTableHandler={handleCountChange}
|
followTableHandler={handleCountChange}
|
||||||
followers={mockDatasetData.followers}
|
followers={mockDatasetData.followers}
|
||||||
|
handleAddColumnTestCase={handleCountChange}
|
||||||
|
handleAddTableTestCase={handleCountChange}
|
||||||
|
handleRemoveColumnTest={handleCountChange}
|
||||||
|
handleRemoveTableTest={handleCountChange}
|
||||||
|
handleShowTestForm={handleCountChange}
|
||||||
|
handleTestModeChange={handleCountChange}
|
||||||
isNodeLoading={{
|
isNodeLoading={{
|
||||||
id: undefined,
|
id: undefined,
|
||||||
state: false,
|
state: false,
|
||||||
@ -203,6 +209,7 @@ const TourPage = () => {
|
|||||||
sampleData={mockDatasetData.sampleData}
|
sampleData={mockDatasetData.sampleData}
|
||||||
setActiveTabHandler={(tab) => setdatasetActiveTab(tab)}
|
setActiveTabHandler={(tab) => setdatasetActiveTab(tab)}
|
||||||
settingsUpdateHandler={() => Promise.resolve()}
|
settingsUpdateHandler={() => Promise.resolve()}
|
||||||
|
showTestForm={false}
|
||||||
slashedTableName={mockDatasetData.slashedTableName}
|
slashedTableName={mockDatasetData.slashedTableName}
|
||||||
tableDetails={mockDatasetData.tableDetails as unknown as Table}
|
tableDetails={mockDatasetData.tableDetails as unknown as Table}
|
||||||
tableProfile={
|
tableProfile={
|
||||||
@ -210,6 +217,8 @@ const TourPage = () => {
|
|||||||
}
|
}
|
||||||
tableQueries={[]}
|
tableQueries={[]}
|
||||||
tableTags={mockDatasetData.tableTags}
|
tableTags={mockDatasetData.tableTags}
|
||||||
|
tableTestCase={[]}
|
||||||
|
testMode="table"
|
||||||
tier={'' as unknown as TagLabel}
|
tier={'' as unknown as TagLabel}
|
||||||
unfollowTableHandler={handleCountChange}
|
unfollowTableHandler={handleCountChange}
|
||||||
usageSummary={
|
usageSummary={
|
||||||
|
@ -15,7 +15,7 @@ import { TabSpecificField } from '../enums/entity.enum';
|
|||||||
|
|
||||||
export const defaultFields = `${TabSpecificField.COLUMNS}, ${TabSpecificField.USAGE_SUMMARY},
|
export const defaultFields = `${TabSpecificField.COLUMNS}, ${TabSpecificField.USAGE_SUMMARY},
|
||||||
${TabSpecificField.FOLLOWERS}, ${TabSpecificField.JOINS}, ${TabSpecificField.TAGS}, ${TabSpecificField.OWNER},
|
${TabSpecificField.FOLLOWERS}, ${TabSpecificField.JOINS}, ${TabSpecificField.TAGS}, ${TabSpecificField.OWNER},
|
||||||
${TabSpecificField.DATAMODEL},${TabSpecificField.TABLE_PROFILE}`;
|
${TabSpecificField.DATAMODEL},${TabSpecificField.TABLE_PROFILE},${TabSpecificField.TESTS}`;
|
||||||
|
|
||||||
export const datasetTableTabs = [
|
export const datasetTableTabs = [
|
||||||
{
|
{
|
||||||
@ -41,6 +41,10 @@ export const datasetTableTabs = [
|
|||||||
name: 'Profiler',
|
name: 'Profiler',
|
||||||
path: 'profiler',
|
path: 'profiler',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Data Quality',
|
||||||
|
path: 'data-quality',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Lineage',
|
name: 'Lineage',
|
||||||
path: 'lineage',
|
path: 'lineage',
|
||||||
@ -77,21 +81,26 @@ export const getCurrentDatasetTab = (tab: string) => {
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'lineage':
|
case 'data-quality':
|
||||||
currentTab = 6;
|
currentTab = 6;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'dbt':
|
case 'lineage':
|
||||||
currentTab = 7;
|
currentTab = 7;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'manage':
|
case 'dbt':
|
||||||
currentTab = 8;
|
currentTab = 8;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'manage':
|
||||||
|
currentTab = 9;
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
case 'schema':
|
case 'schema':
|
||||||
default:
|
default:
|
||||||
currentTab = 1;
|
currentTab = 1;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user