mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-10-07 14:53:28 +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 { 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 APIClient from './index';
|
||||
|
||||
@ -111,3 +115,48 @@ export const removeFollower: Function = (
|
||||
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 TabsPane from '../common/TabsPane/TabsPane';
|
||||
import PageContainer from '../containers/PageContainer';
|
||||
import DataQualityTab from '../DataQualityTab/DataQualityTab';
|
||||
import Entitylineage from '../EntityLineage/EntityLineage.component';
|
||||
import FrequentlyJoinedTables from '../FrequentlyJoinedTables/FrequentlyJoinedTables.component';
|
||||
import ManageTab from '../ManageTab/ManageTab.component';
|
||||
@ -100,7 +101,16 @@ const DatasetDetails: React.FC<DatasetDetailsProps> = ({
|
||||
postFeedHandler,
|
||||
feedCount,
|
||||
entityFieldThreadCount,
|
||||
testMode,
|
||||
tableTestCase,
|
||||
handleTestModeChange,
|
||||
createThread,
|
||||
handleAddTableTestCase,
|
||||
handleAddColumnTestCase,
|
||||
showTestForm,
|
||||
handleShowTestForm,
|
||||
handleRemoveTableTest,
|
||||
handleRemoveColumnTest,
|
||||
}: DatasetDetailsProps) => {
|
||||
const { isAuthDisabled } = useAuth();
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
@ -209,6 +219,17 @@ const DatasetDetails: React.FC<DatasetDetailsProps> = ({
|
||||
isProtected: false,
|
||||
position: 5,
|
||||
},
|
||||
{
|
||||
name: 'Data Quality',
|
||||
icon: {
|
||||
alt: 'data-quality',
|
||||
name: 'icon-quality',
|
||||
title: 'Data Quality',
|
||||
selectedName: '',
|
||||
},
|
||||
isProtected: false,
|
||||
position: 6,
|
||||
},
|
||||
{
|
||||
name: 'Lineage',
|
||||
icon: {
|
||||
@ -218,7 +239,7 @@ const DatasetDetails: React.FC<DatasetDetailsProps> = ({
|
||||
selectedName: 'icon-lineagecolor',
|
||||
},
|
||||
isProtected: false,
|
||||
position: 6,
|
||||
position: 7,
|
||||
},
|
||||
{
|
||||
name: 'DBT',
|
||||
@ -230,7 +251,7 @@ const DatasetDetails: React.FC<DatasetDetailsProps> = ({
|
||||
},
|
||||
isProtected: false,
|
||||
isHidden: !dataModel?.sql,
|
||||
position: 7,
|
||||
position: 8,
|
||||
},
|
||||
{
|
||||
name: 'Manage',
|
||||
@ -243,7 +264,7 @@ const DatasetDetails: React.FC<DatasetDetailsProps> = ({
|
||||
isProtected: false,
|
||||
isHidden: deleted,
|
||||
protectedState: !owner || hasEditAccess(),
|
||||
position: 8,
|
||||
position: 9,
|
||||
},
|
||||
];
|
||||
|
||||
@ -600,7 +621,23 @@ const DatasetDetails: React.FC<DatasetDetailsProps> = ({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{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
|
||||
className={classNames(
|
||||
location.pathname.includes(ROUTES.TOUR)
|
||||
@ -622,7 +659,7 @@ const DatasetDetails: React.FC<DatasetDetailsProps> = ({
|
||||
/>
|
||||
</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">
|
||||
<SchemaEditor
|
||||
className="tw-h-full"
|
||||
@ -631,7 +668,7 @@ const DatasetDetails: React.FC<DatasetDetailsProps> = ({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{activeTab === 8 && !deleted && (
|
||||
{activeTab === 9 && !deleted && (
|
||||
<div>
|
||||
<ManageTab
|
||||
currentTier={tier?.tagFQN}
|
||||
|
@ -19,7 +19,9 @@ import {
|
||||
LineagePos,
|
||||
LoadingNodeState,
|
||||
} from 'Models';
|
||||
import { ColumnTestType } from '../../enums/columnTest.enum';
|
||||
import { CreateThread } from '../../generated/api/feed/createThread';
|
||||
import { CreateTableTest } from '../../generated/api/tests/createTableTest';
|
||||
import {
|
||||
EntityReference,
|
||||
Table,
|
||||
@ -28,8 +30,13 @@ import {
|
||||
TypeUsedToReturnUsageDetailsOfAnEntity,
|
||||
} from '../../generated/entity/data/table';
|
||||
import { User } from '../../generated/entity/teams/user';
|
||||
import { TableTest, TableTestType } from '../../generated/tests/tableTest';
|
||||
import { EntityLineage } from '../../generated/type/entityLineage';
|
||||
import { TagLabel } from '../../generated/type/tagLabel';
|
||||
import {
|
||||
CreateColumnTest,
|
||||
DatasetTestModeType,
|
||||
} from '../../interface/dataQuality.interface';
|
||||
import { TitleBreadcrumbProps } from '../common/title-breadcrumb/title-breadcrumb.interface';
|
||||
import { Edge, EdgeData } from '../EntityLineage/EntityLineage.interface';
|
||||
|
||||
@ -68,6 +75,11 @@ export interface DatasetDetailsProps {
|
||||
isentityThreadLoading: boolean;
|
||||
feedCount: number;
|
||||
entityFieldThreadCount: EntityFieldThreadCount[];
|
||||
testMode: DatasetTestModeType;
|
||||
tableTestCase: TableTest[];
|
||||
showTestForm: boolean;
|
||||
handleShowTestForm: (value: boolean) => void;
|
||||
handleTestModeChange: (mode: DatasetTestModeType) => void;
|
||||
createThread: (data: CreateThread) => void;
|
||||
setActiveTabHandler: (value: number) => void;
|
||||
followTableHandler: () => void;
|
||||
@ -81,4 +93,11 @@ export interface DatasetDetailsProps {
|
||||
removeLineageHandler: (data: EdgeData) => void;
|
||||
entityLineageHandler: (lineage: EntityLineage) => 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';
|
||||
import { EntityLineage } from '../../generated/type/entityLineage';
|
||||
import { TagLabel } from '../../generated/type/tagLabel';
|
||||
import { DatasetTestModeType } from '../../interface/dataQuality.interface';
|
||||
import DatasetDetails from './DatasetDetails.component';
|
||||
import { DatasetOwner } from './DatasetDetails.interface';
|
||||
|
||||
@ -83,7 +84,16 @@ const DatasetDetailsProps = {
|
||||
postFeedHandler: jest.fn(),
|
||||
feedCount: 0,
|
||||
entityFieldThreadCount: [],
|
||||
showTestForm: false,
|
||||
testMode: 'table' as DatasetTestModeType,
|
||||
handleAddTableTestCase: jest.fn(),
|
||||
tableTestCase: [],
|
||||
handleAddColumnTestCase: jest.fn(),
|
||||
createThread: jest.fn(),
|
||||
handleShowTestForm: jest.fn(),
|
||||
handleRemoveTableTest: jest.fn(),
|
||||
handleRemoveColumnTest: jest.fn(),
|
||||
handleTestModeChange: jest.fn(),
|
||||
};
|
||||
jest.mock('../ManageTab/ManageTab.component', () => {
|
||||
return jest.fn().mockReturnValue(<p>ManageTab</p>);
|
||||
|
@ -73,13 +73,16 @@ const DropDownList: FunctionComponent<DropDownListProp> = ({
|
||||
aria-disabled={item.disabled as boolean}
|
||||
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',
|
||||
!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"
|
||||
id={`menu-item-${index}`}
|
||||
key={index}
|
||||
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}>
|
||||
{item.name}
|
||||
</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',
|
||||
TASKS = 'tasks',
|
||||
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 { addLineage, deleteLineageEdge } from '../../axiosAPIs/miscAPI';
|
||||
import {
|
||||
addColumnTestCase,
|
||||
addFollower,
|
||||
addTableTestCase,
|
||||
deleteColumnTestCase,
|
||||
deleteTableTestCase,
|
||||
getTableDetailsByFQN,
|
||||
patchTableDetails,
|
||||
removeFollower,
|
||||
@ -54,10 +58,13 @@ import {
|
||||
getTableTabPath,
|
||||
getVersionPath,
|
||||
} from '../../constants/constants';
|
||||
import { ColumnTestType } from '../../enums/columnTest.enum';
|
||||
import { EntityType, TabSpecificField } from '../../enums/entity.enum';
|
||||
import { ServiceCategory } from '../../enums/service.enum';
|
||||
import { CreateThread } from '../../generated/api/feed/createThread';
|
||||
import { CreateTableTest } from '../../generated/api/tests/createTableTest';
|
||||
import {
|
||||
Column,
|
||||
EntityReference,
|
||||
Table,
|
||||
TableData,
|
||||
@ -65,9 +72,15 @@ import {
|
||||
TypeUsedToReturnUsageDetailsOfAnEntity,
|
||||
} from '../../generated/entity/data/table';
|
||||
import { User } from '../../generated/entity/teams/user';
|
||||
import { TableTest, TableTestType } from '../../generated/tests/tableTest';
|
||||
import { EntityLineage } from '../../generated/type/entityLineage';
|
||||
import { TagLabel } from '../../generated/type/tagLabel';
|
||||
import useToastContext from '../../hooks/useToastContext';
|
||||
import {
|
||||
CreateColumnTest,
|
||||
DatasetTestModeType,
|
||||
ModifiedTableColumn,
|
||||
} from '../../interface/dataQuality.interface';
|
||||
import {
|
||||
addToRecentViewed,
|
||||
getCurrentUserId,
|
||||
@ -149,6 +162,19 @@ const DatasetDetailsPage: FunctionComponent = () => {
|
||||
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 currentTabIndex = tabValue - 1;
|
||||
if (datasetTableTabs[currentTabIndex].path !== tab) {
|
||||
@ -161,6 +187,7 @@ const DatasetDetailsPage: FunctionComponent = () => {
|
||||
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({
|
||||
entityType: EntityType.TABLE,
|
||||
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(() => {
|
||||
fetchTableDetail();
|
||||
setActiveTab(getCurrentDatasetTab(tab));
|
||||
@ -632,6 +770,12 @@ const DatasetDetailsPage: FunctionComponent = () => {
|
||||
feedCount={feedCount}
|
||||
followTableHandler={followTable}
|
||||
followers={followers}
|
||||
handleAddColumnTestCase={handleAddColumnTestCase}
|
||||
handleAddTableTestCase={handleAddTableTestCase}
|
||||
handleRemoveColumnTest={handleRemoveColumnTest}
|
||||
handleRemoveTableTest={handleRemoveTableTest}
|
||||
handleShowTestForm={handleShowTestForm}
|
||||
handleTestModeChange={handleTestModeChange}
|
||||
isLineageLoading={isLineageLoading}
|
||||
isNodeLoading={isNodeLoading}
|
||||
isQueriesLoading={isTableQueriesLoading}
|
||||
@ -646,11 +790,14 @@ const DatasetDetailsPage: FunctionComponent = () => {
|
||||
sampleData={sampleData}
|
||||
setActiveTabHandler={activeTabHandler}
|
||||
settingsUpdateHandler={settingsUpdateHandler}
|
||||
showTestForm={showTestForm}
|
||||
slashedTableName={slashedTableName}
|
||||
tableDetails={tableDetails}
|
||||
tableProfile={tableProfile}
|
||||
tableQueries={tableQueries}
|
||||
tableTags={tableTags}
|
||||
tableTestCase={tableTestCase}
|
||||
testMode={testMode}
|
||||
tier={tier as TagLabel}
|
||||
unfollowTableHandler={unfollowTable}
|
||||
usageSummary={usageSummary}
|
||||
|
@ -189,6 +189,12 @@ const TourPage = () => {
|
||||
feedCount={0}
|
||||
followTableHandler={handleCountChange}
|
||||
followers={mockDatasetData.followers}
|
||||
handleAddColumnTestCase={handleCountChange}
|
||||
handleAddTableTestCase={handleCountChange}
|
||||
handleRemoveColumnTest={handleCountChange}
|
||||
handleRemoveTableTest={handleCountChange}
|
||||
handleShowTestForm={handleCountChange}
|
||||
handleTestModeChange={handleCountChange}
|
||||
isNodeLoading={{
|
||||
id: undefined,
|
||||
state: false,
|
||||
@ -203,6 +209,7 @@ const TourPage = () => {
|
||||
sampleData={mockDatasetData.sampleData}
|
||||
setActiveTabHandler={(tab) => setdatasetActiveTab(tab)}
|
||||
settingsUpdateHandler={() => Promise.resolve()}
|
||||
showTestForm={false}
|
||||
slashedTableName={mockDatasetData.slashedTableName}
|
||||
tableDetails={mockDatasetData.tableDetails as unknown as Table}
|
||||
tableProfile={
|
||||
@ -210,6 +217,8 @@ const TourPage = () => {
|
||||
}
|
||||
tableQueries={[]}
|
||||
tableTags={mockDatasetData.tableTags}
|
||||
tableTestCase={[]}
|
||||
testMode="table"
|
||||
tier={'' as unknown as TagLabel}
|
||||
unfollowTableHandler={handleCountChange}
|
||||
usageSummary={
|
||||
|
@ -15,7 +15,7 @@ import { TabSpecificField } from '../enums/entity.enum';
|
||||
|
||||
export const defaultFields = `${TabSpecificField.COLUMNS}, ${TabSpecificField.USAGE_SUMMARY},
|
||||
${TabSpecificField.FOLLOWERS}, ${TabSpecificField.JOINS}, ${TabSpecificField.TAGS}, ${TabSpecificField.OWNER},
|
||||
${TabSpecificField.DATAMODEL},${TabSpecificField.TABLE_PROFILE}`;
|
||||
${TabSpecificField.DATAMODEL},${TabSpecificField.TABLE_PROFILE},${TabSpecificField.TESTS}`;
|
||||
|
||||
export const datasetTableTabs = [
|
||||
{
|
||||
@ -41,6 +41,10 @@ export const datasetTableTabs = [
|
||||
name: 'Profiler',
|
||||
path: 'profiler',
|
||||
},
|
||||
{
|
||||
name: 'Data Quality',
|
||||
path: 'data-quality',
|
||||
},
|
||||
{
|
||||
name: 'Lineage',
|
||||
path: 'lineage',
|
||||
@ -77,21 +81,26 @@ export const getCurrentDatasetTab = (tab: string) => {
|
||||
|
||||
break;
|
||||
|
||||
case 'lineage':
|
||||
case 'data-quality':
|
||||
currentTab = 6;
|
||||
|
||||
break;
|
||||
|
||||
case 'dbt':
|
||||
case 'lineage':
|
||||
currentTab = 7;
|
||||
|
||||
break;
|
||||
|
||||
case 'manage':
|
||||
case 'dbt':
|
||||
currentTab = 8;
|
||||
|
||||
break;
|
||||
|
||||
case 'manage':
|
||||
currentTab = 9;
|
||||
|
||||
break;
|
||||
|
||||
case 'schema':
|
||||
default:
|
||||
currentTab = 1;
|
||||
|
Loading…
x
Reference in New Issue
Block a user