mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-11-03 03:59:12 +00:00
* fix #7729 Number of passed/failed rows should be captured * added unit test * added unit test for testSummaryCustomTooltip * address review comment
This commit is contained in:
parent
ec32d9c573
commit
e416810be2
@ -15,13 +15,8 @@ import { Form, FormProps, Input } from 'antd';
|
||||
import Modal from 'antd/lib/modal/Modal';
|
||||
import { AxiosError } from 'axios';
|
||||
import { compare } from 'fast-json-patch';
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { isEmpty } from 'lodash';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ENTITY_NAME_REGEX } from '../../constants/regex.constants';
|
||||
import { Table } from '../../generated/entity/data/table';
|
||||
@ -30,13 +25,18 @@ import {
|
||||
TestDataType,
|
||||
TestDefinition,
|
||||
} from '../../generated/tests/testDefinition';
|
||||
import {
|
||||
FieldProp,
|
||||
FieldTypes,
|
||||
FormItemLayout,
|
||||
} from '../../interface/FormUtils.interface';
|
||||
import { getTableDetailsByFQN } from '../../rest/tableAPI';
|
||||
import { getTestDefinitionById, updateTestCaseById } from '../../rest/testAPI';
|
||||
import { getNameFromFQN } from '../../utils/CommonUtils';
|
||||
import { generateFormFields } from '../../utils/formUtils';
|
||||
import { getEntityFqnFromEntityLink } from '../../utils/TableUtils';
|
||||
import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils';
|
||||
import RichTextEditor from '../common/RichTextEditor/RichTextEditor';
|
||||
import { EditorContentRef } from '../common/RichTextEditor/RichTextEditor.interface';
|
||||
import Loader from '../Loader/Loader';
|
||||
import { EditTestCaseModalProps } from './AddDataQualityTest.interface';
|
||||
import ParameterForm from './components/ParameterForm';
|
||||
@ -55,13 +55,30 @@ const EditTestCaseModal: React.FC<EditTestCaseModalProps> = ({
|
||||
const [isLoadingOnSave, setIsLoadingOnSave] = useState(false);
|
||||
const [table, setTable] = useState<Table>();
|
||||
|
||||
const markdownRef = useRef<EditorContentRef>();
|
||||
|
||||
const isColumn = useMemo(
|
||||
() => testCase?.entityLink.includes('::columns::'),
|
||||
[testCase]
|
||||
);
|
||||
|
||||
const isComputeRowCountFieldVisible = useMemo(() => {
|
||||
return selectedDefinition?.supportsRowLevelPassedFailed ?? false;
|
||||
}, [selectedDefinition]);
|
||||
|
||||
const formFields: FieldProp[] = [
|
||||
{
|
||||
name: 'computePassedFailedRowCount',
|
||||
label: t('label.compute-row-count'),
|
||||
type: FieldTypes.SWITCH,
|
||||
helperText: t('message.compute-row-count-helper-text'),
|
||||
required: false,
|
||||
props: {
|
||||
'data-testid': 'compute-passed-failed-row-count',
|
||||
},
|
||||
id: 'root/computePassedFailedRowCount',
|
||||
formItemLayout: FormItemLayout.HORIZONTAL,
|
||||
},
|
||||
];
|
||||
|
||||
const GenerateParamsField = useCallback(() => {
|
||||
if (selectedDefinition?.parameterDefinition) {
|
||||
return <ParameterForm definition={selectedDefinition} table={table} />;
|
||||
@ -104,19 +121,17 @@ const EditTestCaseModal: React.FC<EditTestCaseModalProps> = ({
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
parameterValues: parameterValues as TestCaseParameterValue[],
|
||||
description: markdownRef.current?.getEditorContent(),
|
||||
};
|
||||
return parameterValues as TestCaseParameterValue[];
|
||||
};
|
||||
|
||||
const handleFormSubmit: FormProps['onFinish'] = async (value) => {
|
||||
const { parameterValues, description } = createTestCaseObj(value);
|
||||
const parameterValues = createTestCaseObj(value);
|
||||
const updatedTestCase = {
|
||||
...testCase,
|
||||
parameterValues,
|
||||
description,
|
||||
description: isEmpty(value.description) ? undefined : value.description,
|
||||
displayName: value.displayName,
|
||||
computePassedFailedRowCount: value.computePassedFailedRowCount,
|
||||
};
|
||||
const jsonPatch = compare(testCase, updatedTestCase);
|
||||
|
||||
@ -179,6 +194,7 @@ const EditTestCaseModal: React.FC<EditTestCaseModalProps> = ({
|
||||
column: getNameFromFQN(
|
||||
getEntityFqnFromEntityLink(testCase?.entityLink, isColumn)
|
||||
),
|
||||
computePassedFailedRowCount: testCase?.computePassedFailedRowCount,
|
||||
});
|
||||
|
||||
const isContainsColumnName = testCase.parameterValues?.find(
|
||||
@ -252,16 +268,21 @@ const EditTestCaseModal: React.FC<EditTestCaseModalProps> = ({
|
||||
|
||||
{GenerateParamsField()}
|
||||
|
||||
<Form.Item label={t('label.description')} name="description">
|
||||
<Form.Item
|
||||
label={t('label.description')}
|
||||
name="description"
|
||||
trigger="onTextChange">
|
||||
<RichTextEditor
|
||||
height="200px"
|
||||
initialValue={testCase?.description || ''}
|
||||
ref={markdownRef}
|
||||
style={{
|
||||
margin: 0,
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
{isComputeRowCountFieldVisible
|
||||
? generateFormFields(formFields)
|
||||
: null}
|
||||
</Form>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
@ -0,0 +1,223 @@
|
||||
/*
|
||||
* Copyright 2024 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 {
|
||||
findByRole,
|
||||
render,
|
||||
screen,
|
||||
waitForElement,
|
||||
} from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React, { forwardRef } from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { ProfilerDashboardType } from '../../../enums/table.enum';
|
||||
import { MOCK_TABLE } from '../../../mocks/TableData.mock';
|
||||
import TestCaseForm from './TestCaseForm';
|
||||
|
||||
const mockProps = {
|
||||
onSubmit: jest.fn(),
|
||||
onCancel: jest.fn(),
|
||||
table: MOCK_TABLE,
|
||||
};
|
||||
|
||||
const mockParams = {
|
||||
entityTypeFQN: 'sample_data.ecommerce_db.shopify.dim_address',
|
||||
dashboardType: ProfilerDashboardType.TABLE,
|
||||
};
|
||||
|
||||
const mockTestDefinition = {
|
||||
data: [
|
||||
{
|
||||
id: '21bda32d-3c62-4d19-a477-1a99fd1737fa',
|
||||
name: 'columnValueLengthsToBeBetween',
|
||||
displayName: 'Column Value Lengths To Be Between',
|
||||
fullyQualifiedName: 'columnValueLengthsToBeBetween',
|
||||
entityType: 'COLUMN',
|
||||
testPlatforms: ['OpenMetadata'],
|
||||
supportedDataTypes: [
|
||||
'BYTES',
|
||||
'STRING',
|
||||
'MEDIUMTEXT',
|
||||
'TEXT',
|
||||
'CHAR',
|
||||
'VARCHAR',
|
||||
'ARRAY',
|
||||
],
|
||||
parameterDefinition: [
|
||||
{
|
||||
name: 'minLength',
|
||||
displayName: 'Min',
|
||||
dataType: 'INT',
|
||||
description: 'description',
|
||||
required: false,
|
||||
optionValues: [],
|
||||
},
|
||||
{
|
||||
name: 'maxLength',
|
||||
displayName: 'Max',
|
||||
dataType: 'INT',
|
||||
description: 'description',
|
||||
required: false,
|
||||
optionValues: [],
|
||||
},
|
||||
],
|
||||
supportsRowLevelPassedFailed: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
useHistory: jest.fn(),
|
||||
useParams: jest.fn().mockImplementation(() => mockParams),
|
||||
}));
|
||||
jest.mock('../../../rest/testAPI', () => ({
|
||||
getListTestCase: jest
|
||||
.fn()
|
||||
.mockImplementation(() => Promise.resolve({ data: [] })),
|
||||
getListTestDefinitions: jest
|
||||
.fn()
|
||||
.mockImplementation(() => Promise.resolve(mockTestDefinition)),
|
||||
}));
|
||||
jest.mock('../../common/RichTextEditor/RichTextEditor', () =>
|
||||
forwardRef(
|
||||
jest.fn().mockImplementation(() => <div>RichTextEditor.component</div>)
|
||||
)
|
||||
);
|
||||
jest.mock('./ParameterForm', () =>
|
||||
jest.fn().mockImplementation(() => <div>ParameterForm.component</div>)
|
||||
);
|
||||
jest.mock('crypto-random-string-with-promisify-polyfill', () =>
|
||||
jest.fn().mockImplementation(() => '4B3B')
|
||||
);
|
||||
|
||||
describe('TestCaseForm', () => {
|
||||
it('should render component', async () => {
|
||||
await act(async () => {
|
||||
render(<TestCaseForm {...mockProps} />);
|
||||
});
|
||||
|
||||
expect(await screen.findByTestId('test-case-form')).toBeInTheDocument();
|
||||
expect(await screen.findByTestId('cancel-btn')).toBeInTheDocument();
|
||||
expect(await screen.findByTestId('submit-test')).toBeInTheDocument();
|
||||
expect(await screen.findByTestId('test-case-name')).toBeInTheDocument();
|
||||
expect(await screen.findByTestId('test-type')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId('compute-passed-failed-row-count')
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('column')).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText('ParameterForm.component')
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
await screen.findByText('RichTextEditor.component')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call onCancel when click 'Cancel' button", async () => {
|
||||
await act(async () => {
|
||||
render(<TestCaseForm {...mockProps} />);
|
||||
});
|
||||
|
||||
const cancelBtn = await screen.findByTestId('cancel-btn');
|
||||
await act(async () => {
|
||||
cancelBtn.click();
|
||||
});
|
||||
|
||||
expect(mockProps.onCancel).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should call onSubmit when click 'Submit' button", async () => {
|
||||
await act(async () => {
|
||||
render(<TestCaseForm {...mockProps} />);
|
||||
});
|
||||
|
||||
const typeSelector = await findByRole(
|
||||
await screen.findByTestId('test-type'),
|
||||
'combobox'
|
||||
);
|
||||
await act(async () => {
|
||||
userEvent.click(typeSelector);
|
||||
});
|
||||
|
||||
expect(typeSelector).toBeInTheDocument();
|
||||
|
||||
await waitForElement(() =>
|
||||
screen.findByText('Column Value Lengths To Be Between')
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(
|
||||
await screen.findByText('Column Value Lengths To Be Between')
|
||||
);
|
||||
});
|
||||
|
||||
expect(
|
||||
await screen.findByTestId('compute-passed-failed-row-count')
|
||||
).toBeInTheDocument();
|
||||
|
||||
const submitBtn = await screen.findByTestId('submit-test');
|
||||
await act(async () => {
|
||||
submitBtn.click();
|
||||
});
|
||||
|
||||
expect(mockProps.onSubmit).toHaveBeenCalledWith({
|
||||
computePassedFailedRowCount: undefined,
|
||||
description: undefined,
|
||||
displayName: 'dim_address_column_value_lengths_to_be_between_4B3B',
|
||||
entityLink: '<#E::table::sample_data.ecommerce_db.shopify.dim_address>',
|
||||
name: 'dim_address_column_value_lengths_to_be_between_4B3B',
|
||||
parameterValues: [],
|
||||
testDefinition: 'columnValueLengthsToBeBetween',
|
||||
testSuite: '',
|
||||
});
|
||||
});
|
||||
|
||||
// column test case
|
||||
it("should show column section when test type is 'Column'", async () => {
|
||||
mockParams.dashboardType = ProfilerDashboardType.COLUMN;
|
||||
await act(async () => {
|
||||
render(<TestCaseForm {...mockProps} />);
|
||||
});
|
||||
|
||||
expect(await screen.findByTestId('column')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show compute row count field, if supportsRowLevelPassedFailed is true in test definition', async () => {
|
||||
await act(async () => {
|
||||
render(<TestCaseForm {...mockProps} />);
|
||||
});
|
||||
|
||||
const typeSelector = await findByRole(
|
||||
await screen.findByTestId('test-type'),
|
||||
'combobox'
|
||||
);
|
||||
await act(async () => {
|
||||
userEvent.click(typeSelector);
|
||||
});
|
||||
|
||||
expect(typeSelector).toBeInTheDocument();
|
||||
|
||||
await waitForElement(() =>
|
||||
screen.findByText('Column Value Lengths To Be Between')
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(
|
||||
await screen.findByText('Column Value Lengths To Be Between')
|
||||
);
|
||||
});
|
||||
|
||||
expect(
|
||||
await screen.findByTestId('compute-passed-failed-row-count')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@ -17,13 +17,7 @@ import cryptoRandomString from 'crypto-random-string-with-promisify-polyfill';
|
||||
import { t } from 'i18next';
|
||||
import { isEmpty, snakeCase } from 'lodash';
|
||||
import Qs from 'qs';
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
import { PAGE_SIZE_LARGE } from '../../../constants/constants';
|
||||
import { ENTITY_NAME_REGEX } from '../../../constants/regex.constants';
|
||||
@ -39,17 +33,22 @@ import {
|
||||
TestDefinition,
|
||||
TestPlatform,
|
||||
} from '../../../generated/tests/testDefinition';
|
||||
import {
|
||||
FieldProp,
|
||||
FieldTypes,
|
||||
FormItemLayout,
|
||||
} from '../../../interface/FormUtils.interface';
|
||||
import { getListTestCase, getListTestDefinitions } from '../../../rest/testAPI';
|
||||
import {
|
||||
getNameFromFQN,
|
||||
replaceAllSpacialCharWith_,
|
||||
} from '../../../utils/CommonUtils';
|
||||
import { getEntityName } from '../../../utils/EntityUtils';
|
||||
import { generateFormFields } from '../../../utils/formUtils';
|
||||
import { getDecodedFqn } from '../../../utils/StringsUtils';
|
||||
import { generateEntityLink } from '../../../utils/TableUtils';
|
||||
import { showErrorToast } from '../../../utils/ToastUtils';
|
||||
import RichTextEditor from '../../common/RichTextEditor/RichTextEditor';
|
||||
import { EditorContentRef } from '../../common/RichTextEditor/RichTextEditor.interface';
|
||||
import { TestCaseFormProps } from '../AddDataQualityTest.interface';
|
||||
import ParameterForm from './ParameterForm';
|
||||
|
||||
@ -73,7 +72,6 @@ const TestCaseForm: React.FC<TestCaseFormProps> = ({
|
||||
const decodedEntityFQN = getDecodedFqn(entityTypeFQN);
|
||||
const isColumnFqn = dashboardType === ProfilerDashboardType.COLUMN;
|
||||
const [form] = Form.useForm();
|
||||
const markdownRef = useRef<EditorContentRef>();
|
||||
const [testDefinitions, setTestDefinitions] = useState<TestDefinition[]>([]);
|
||||
const [selectedTestType, setSelectedTestType] = useState<string | undefined>(
|
||||
initialValue?.testDefinition
|
||||
@ -82,6 +80,21 @@ const TestCaseForm: React.FC<TestCaseFormProps> = ({
|
||||
|
||||
const columnName = Form.useWatch('column', form);
|
||||
|
||||
const formFields: FieldProp[] = [
|
||||
{
|
||||
name: 'computePassedFailedRowCount',
|
||||
label: t('label.compute-row-count'),
|
||||
type: FieldTypes.SWITCH,
|
||||
helperText: t('message.compute-row-count-helper-text'),
|
||||
required: false,
|
||||
props: {
|
||||
'data-testid': 'compute-passed-failed-row-count',
|
||||
},
|
||||
id: 'root/computePassedFailedRowCount',
|
||||
formItemLayout: FormItemLayout.HORIZONTAL,
|
||||
},
|
||||
];
|
||||
|
||||
const fetchAllTestDefinitions = async () => {
|
||||
try {
|
||||
const { data } = await getListTestDefinitions({
|
||||
@ -103,7 +116,6 @@ const TestCaseForm: React.FC<TestCaseFormProps> = ({
|
||||
const fetchAllTestCases = async () => {
|
||||
try {
|
||||
const { data } = await getListTestCase({
|
||||
fields: 'testDefinition',
|
||||
limit: PAGE_SIZE_LARGE,
|
||||
entityLink: generateEntityLink(
|
||||
isColumnFqn ? `${decodedEntityFQN}.${columnName}` : decodedEntityFQN,
|
||||
@ -113,7 +125,7 @@ const TestCaseForm: React.FC<TestCaseFormProps> = ({
|
||||
|
||||
setTestCases(data);
|
||||
} catch (error) {
|
||||
showErrorToast(error as AxiosError);
|
||||
setTestCases([]);
|
||||
}
|
||||
};
|
||||
|
||||
@ -126,6 +138,11 @@ const TestCaseForm: React.FC<TestCaseFormProps> = ({
|
||||
(definition) => definition.fullyQualifiedName === testType
|
||||
);
|
||||
};
|
||||
const isComputeRowCountFieldVisible = useMemo(() => {
|
||||
const selectedDefinition = getSelectedTestDefinition();
|
||||
|
||||
return selectedDefinition?.supportsRowLevelPassedFailed ?? false;
|
||||
}, [selectedTestType, initialValue, testDefinitions]);
|
||||
|
||||
const GenerateParamsField = useCallback(() => {
|
||||
const selectedDefinition = getSelectedTestDefinition();
|
||||
@ -140,6 +157,8 @@ const TestCaseForm: React.FC<TestCaseFormProps> = ({
|
||||
testName: string;
|
||||
params: Record<string, string | { [key: string]: string }[]>;
|
||||
testTypeId: string;
|
||||
computePassedFailedRowCount?: boolean;
|
||||
description?: string;
|
||||
}): CreateTestCase => {
|
||||
const selectedDefinition = getSelectedTestDefinition();
|
||||
const paramsValue = selectedDefinition?.parameterDefinition?.[0];
|
||||
@ -168,13 +187,14 @@ const TestCaseForm: React.FC<TestCaseFormProps> = ({
|
||||
return {
|
||||
name,
|
||||
displayName: name,
|
||||
computePassedFailedRowCount: value.computePassedFailedRowCount,
|
||||
entityLink: generateEntityLink(
|
||||
isColumnFqn ? `${decodedEntityFQN}.${columnName}` : decodedEntityFQN,
|
||||
isColumnFqn
|
||||
),
|
||||
parameterValues: parameterValues as TestCaseParameterValue[],
|
||||
testDefinition: value.testTypeId,
|
||||
description: markdownRef.current?.getEditorContent(),
|
||||
description: isEmpty(value.description) ? undefined : value.description,
|
||||
testSuite: '',
|
||||
};
|
||||
};
|
||||
@ -246,6 +266,7 @@ const TestCaseForm: React.FC<TestCaseFormProps> = ({
|
||||
|
||||
return (
|
||||
<Form
|
||||
data-testid="test-case-form"
|
||||
form={form}
|
||||
layout="vertical"
|
||||
name="tableTestForm"
|
||||
@ -265,6 +286,7 @@ const TestCaseForm: React.FC<TestCaseFormProps> = ({
|
||||
},
|
||||
]}>
|
||||
<Select
|
||||
data-testid="column"
|
||||
placeholder={t('label.please-select-entity', {
|
||||
entity: t('label.column-lowercase'),
|
||||
})}>
|
||||
@ -296,7 +318,10 @@ const TestCaseForm: React.FC<TestCaseFormProps> = ({
|
||||
},
|
||||
},
|
||||
]}>
|
||||
<Input placeholder={t('message.enter-test-case-name')} />
|
||||
<Input
|
||||
data-testid="test-case-name"
|
||||
placeholder={t('message.enter-test-case-name')}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('label.test-type')}
|
||||
@ -311,6 +336,7 @@ const TestCaseForm: React.FC<TestCaseFormProps> = ({
|
||||
]}>
|
||||
<Select
|
||||
showSearch
|
||||
data-testid="test-type"
|
||||
options={testDefinitions.map((suite) => ({
|
||||
label: getEntityName(suite),
|
||||
value: suite.fullyQualifiedName,
|
||||
@ -321,20 +347,26 @@ const TestCaseForm: React.FC<TestCaseFormProps> = ({
|
||||
|
||||
{GenerateParamsField()}
|
||||
|
||||
<Form.Item label={t('label.description')} name="description">
|
||||
<Form.Item
|
||||
label={t('label.description')}
|
||||
name="description"
|
||||
trigger="onTextChange">
|
||||
<RichTextEditor
|
||||
height="200px"
|
||||
initialValue={initialValue?.description || ''}
|
||||
ref={markdownRef}
|
||||
style={{
|
||||
margin: 0,
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{isComputeRowCountFieldVisible ? generateFormFields(formFields) : null}
|
||||
|
||||
<Form.Item noStyle>
|
||||
<Space className="w-full justify-end" size={16}>
|
||||
<Button onClick={onBack}>{t('label.back')}</Button>
|
||||
<Button data-testid="cancel-btn" onClick={onBack}>
|
||||
{t('label.back')}
|
||||
</Button>
|
||||
<Button data-testid="submit-test" htmlType="submit" type="primary">
|
||||
{t('label.submit')}
|
||||
</Button>
|
||||
|
||||
@ -173,7 +173,7 @@ const AddDomainForm = ({
|
||||
/>
|
||||
),
|
||||
},
|
||||
formItemLayout: FormItemLayout.HORIZONATAL,
|
||||
formItemLayout: FormItemLayout.HORIZONTAL,
|
||||
formItemProps: {
|
||||
valuePropName: 'owner',
|
||||
trigger: 'onUpdate',
|
||||
@ -198,7 +198,7 @@ const AddDomainForm = ({
|
||||
/>
|
||||
),
|
||||
},
|
||||
formItemLayout: FormItemLayout.HORIZONATAL,
|
||||
formItemLayout: FormItemLayout.HORIZONTAL,
|
||||
formItemProps: {
|
||||
valuePropName: 'selectedUsers',
|
||||
trigger: 'onUpdate',
|
||||
|
||||
@ -168,7 +168,7 @@ const AddGlossary = ({
|
||||
'data-testid': 'mutually-exclusive-button',
|
||||
},
|
||||
id: 'root/mutuallyExclusive',
|
||||
formItemLayout: FormItemLayout.HORIZONATAL,
|
||||
formItemLayout: FormItemLayout.HORIZONTAL,
|
||||
},
|
||||
];
|
||||
|
||||
@ -189,7 +189,7 @@ const AddGlossary = ({
|
||||
/>
|
||||
),
|
||||
},
|
||||
formItemLayout: FormItemLayout.HORIZONATAL,
|
||||
formItemLayout: FormItemLayout.HORIZONTAL,
|
||||
formItemProps: {
|
||||
valuePropName: 'owner',
|
||||
trigger: 'onUpdate',
|
||||
@ -214,7 +214,7 @@ const AddGlossary = ({
|
||||
/>
|
||||
),
|
||||
},
|
||||
formItemLayout: FormItemLayout.HORIZONATAL,
|
||||
formItemLayout: FormItemLayout.HORIZONTAL,
|
||||
formItemProps: {
|
||||
valuePropName: 'selectedUsers',
|
||||
trigger: 'onUpdate',
|
||||
|
||||
@ -291,7 +291,7 @@ const AddGlossaryTermForm = ({
|
||||
'data-testid': 'mutually-exclusive-button',
|
||||
},
|
||||
id: 'root/mutuallyExclusive',
|
||||
formItemLayout: FormItemLayout.HORIZONATAL,
|
||||
formItemLayout: FormItemLayout.HORIZONTAL,
|
||||
},
|
||||
];
|
||||
|
||||
@ -312,7 +312,7 @@ const AddGlossaryTermForm = ({
|
||||
/>
|
||||
),
|
||||
},
|
||||
formItemLayout: FormItemLayout.HORIZONATAL,
|
||||
formItemLayout: FormItemLayout.HORIZONTAL,
|
||||
formItemProps: {
|
||||
valuePropName: 'owner',
|
||||
trigger: 'onUpdate',
|
||||
@ -338,7 +338,7 @@ const AddGlossaryTermForm = ({
|
||||
/>
|
||||
),
|
||||
},
|
||||
formItemLayout: FormItemLayout.HORIZONATAL,
|
||||
formItemLayout: FormItemLayout.HORIZONTAL,
|
||||
formItemProps: {
|
||||
valuePropName: 'selectedUsers',
|
||||
trigger: 'onUpdate',
|
||||
|
||||
@ -131,7 +131,7 @@ export const AddEditPersonaForm = ({
|
||||
trigger: 'onUpdate',
|
||||
initialValue: [],
|
||||
},
|
||||
formItemLayout: FormItemLayout.HORIZONATAL,
|
||||
formItemLayout: FormItemLayout.HORIZONTAL,
|
||||
type: FieldTypes.USER_MULTI_SELECT,
|
||||
props: {
|
||||
'data-testid': 'user',
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
import { Button, Col, Row, Space, Typography } from 'antd';
|
||||
import { AxiosError } from 'axios';
|
||||
import { t } from 'i18next';
|
||||
import { isEmpty, isEqual, isUndefined, round, uniqueId } from 'lodash';
|
||||
import { isEmpty, isEqual, isUndefined, omitBy, round, uniqueId } from 'lodash';
|
||||
import Qs from 'qs';
|
||||
import React, { ReactElement, useEffect, useMemo, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
@ -63,12 +63,16 @@ import RichTextEditorPreviewer from '../../common/RichTextEditor/RichTextEditorP
|
||||
import DatePickerMenu from '../../DatePickerMenu/DatePickerMenu.component';
|
||||
import Loader from '../../Loader/Loader';
|
||||
import SchemaEditor from '../../SchemaEditor/SchemaEditor';
|
||||
import { TestSummaryProps } from '../profilerDashboard.interface';
|
||||
import {
|
||||
MetricChartType,
|
||||
TestSummaryProps,
|
||||
} from '../profilerDashboard.interface';
|
||||
import './test-summary.less';
|
||||
import TestSummaryCustomTooltip from './TestSummaryCustomTooltip.component';
|
||||
|
||||
type ChartDataType = {
|
||||
information: { label: string; color: string }[];
|
||||
data: { [key: string]: string }[];
|
||||
data: MetricChartType['data'];
|
||||
};
|
||||
|
||||
export interface DateRangeObject {
|
||||
@ -110,7 +114,7 @@ const TestSummary: React.FC<TestSummaryProps> = ({
|
||||
};
|
||||
|
||||
const generateChartData = (currentData: TestCaseResult[]) => {
|
||||
const chartData: { [key: string]: string }[] = [];
|
||||
const chartData: ChartDataType['data'] = [];
|
||||
currentData.forEach((result) => {
|
||||
const values = result.testResultValue?.reduce((acc, curr) => {
|
||||
return {
|
||||
@ -118,11 +122,22 @@ const TestSummary: React.FC<TestSummaryProps> = ({
|
||||
[curr.name ?? 'value']: round(parseFloat(curr.value ?? ''), 2) || 0,
|
||||
};
|
||||
}, {});
|
||||
const metric = {
|
||||
passedRows: result.passedRows,
|
||||
failedRows: result.failedRows,
|
||||
passedRowsPercentage: isUndefined(result.passedRowsPercentage)
|
||||
? undefined
|
||||
: `${result.passedRowsPercentage}%`,
|
||||
failedRowsPercentage: isUndefined(result.failedRowsPercentage)
|
||||
? undefined
|
||||
: `${result.failedRowsPercentage}%`,
|
||||
};
|
||||
|
||||
chartData.push({
|
||||
name: formatDateTime(result.timestamp),
|
||||
status: result.testCaseStatus ?? '',
|
||||
status: result.testCaseStatus,
|
||||
...values,
|
||||
...omitBy(metric, isUndefined),
|
||||
});
|
||||
});
|
||||
chartData.reverse();
|
||||
@ -219,7 +234,7 @@ const TestSummary: React.FC<TestSummaryProps> = ({
|
||||
padding={{ top: 8, bottom: 8 }}
|
||||
tickFormatter={(value) => axisTickFormatter(value)}
|
||||
/>
|
||||
<Tooltip />
|
||||
<Tooltip content={<TestSummaryCustomTooltip />} />
|
||||
<Legend />
|
||||
{data.parameterValues?.length === 2 && referenceArea()}
|
||||
{chartData?.information?.map((info) => (
|
||||
|
||||
@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2024 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 { Card, Typography } from 'antd';
|
||||
import { entries, omit, startCase } from 'lodash';
|
||||
import React from 'react';
|
||||
import { TooltipProps } from 'recharts';
|
||||
|
||||
const TestSummaryCustomTooltip = (
|
||||
props: TooltipProps<string | number, string>
|
||||
) => {
|
||||
const { active, payload = [] } = props;
|
||||
const data = payload.length ? entries(omit(payload[0].payload, 'name')) : [];
|
||||
|
||||
if (!active || payload.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={
|
||||
<Typography.Title level={5}>{payload[0].payload.name}</Typography.Title>
|
||||
}>
|
||||
<ul data-testid="test-summary-tool-tip-container">
|
||||
{data.map(([key, value]) => (
|
||||
<li
|
||||
className="d-flex items-center justify-between gap-6 p-b-xss text-sm"
|
||||
key={`item-${key}`}>
|
||||
<span className="flex items-center text-grey-muted">
|
||||
{startCase(key)}
|
||||
</span>
|
||||
<span className="font-medium" data-testid={key}>
|
||||
{value}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default TestSummaryCustomTooltip;
|
||||
@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2024 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 { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import TestSummaryCustomTooltip from './TestSummaryCustomTooltip.component';
|
||||
|
||||
const mockProps = {
|
||||
active: true,
|
||||
payload: [
|
||||
{
|
||||
stroke: '#7147E8',
|
||||
strokeWidth: 1,
|
||||
fill: '#fff',
|
||||
dataKey: 'minValueLength',
|
||||
name: 'minValueLength',
|
||||
color: '#7147E8',
|
||||
value: 36,
|
||||
payload: {
|
||||
name: 'Jan 3, 2024, 6:45 PM',
|
||||
status: 'Failed',
|
||||
minValueLength: 12,
|
||||
maxValueLength: 24,
|
||||
passedRows: 4,
|
||||
failedRows: 2,
|
||||
passedRowsPercentage: '60%',
|
||||
failedRowsPercentage: '40%',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('Test AddServicePage component', () => {
|
||||
it('AddServicePage component should render', async () => {
|
||||
render(<TestSummaryCustomTooltip {...mockProps} />);
|
||||
|
||||
expect(
|
||||
await screen.findByTestId('test-summary-tool-tip-container')
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
await screen.findByTestId('test-summary-tool-tip-container')
|
||||
).toBeInTheDocument();
|
||||
expect((await screen.findByTestId('status')).textContent).toEqual('Failed');
|
||||
expect((await screen.findByTestId('minValueLength')).textContent).toEqual(
|
||||
'12'
|
||||
);
|
||||
expect((await screen.findByTestId('maxValueLength')).textContent).toEqual(
|
||||
'24'
|
||||
);
|
||||
expect((await screen.findByTestId('passedRows')).textContent).toEqual('4');
|
||||
expect((await screen.findByTestId('failedRows')).textContent).toEqual('2');
|
||||
expect(
|
||||
(await screen.findByTestId('passedRowsPercentage')).textContent
|
||||
).toEqual('60%');
|
||||
expect(
|
||||
(await screen.findByTestId('failedRowsPercentage')).textContent
|
||||
).toEqual('40%');
|
||||
expect(screen.queryByText('name')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@ -21,7 +21,7 @@ export type FormValidationRules = Record<
|
||||
>;
|
||||
|
||||
export enum FormItemLayout {
|
||||
HORIZONATAL = 'horizontal',
|
||||
HORIZONTAL = 'horizontal',
|
||||
VERTICAL = 'vertical',
|
||||
}
|
||||
|
||||
|
||||
@ -167,6 +167,7 @@
|
||||
"comment-lowercase": "kommentar",
|
||||
"completed": "Abgeschlossen",
|
||||
"completed-entity": "Abgeschlossen {{entity}}",
|
||||
"compute-row-count": "Compute Row Count",
|
||||
"condition": "Bedingung",
|
||||
"config": "Konfiguration",
|
||||
"configuration": "Konfiguration",
|
||||
@ -1236,6 +1237,7 @@
|
||||
"click-text-to-view-details": "Klicken Sie auf <0>{{text}}</0>, um Details anzuzeigen.",
|
||||
"closed-this-task": "hat diese Aufgabe geschlossen",
|
||||
"collaborate-with-other-user": "um mit anderen Benutzern zusammenzuarbeiten.",
|
||||
"compute-row-count-helper-text": "Compute the passed and failed row count for the test case.",
|
||||
"confidence-percentage-message": "Legen Sie den Vertrauensgrad für das NLP-Modell fest, um zu bestimmen, ob eine Spalte PII-Daten enthält oder nicht.",
|
||||
"configure-a-service-description": "Geben Sie einen eindeutigen Dienstnamen ein. Der Name muss eindeutig innerhalb der Dienstkategorie sein. Beispielsweise können sowohl MySQL als auch Snowflake nicht denselben Dienstnamen (z. B. customer_data) haben, aber unterschiedliche Dienstkategorien (Dashboard, Pipeline) können denselben Dienstnamen haben. Leerzeichen im Dienstnamen werden nicht unterstützt. Zeichen wie - _ werden unterstützt. Fügen Sie außerdem eine Beschreibung hinzu.",
|
||||
"configure-airflow": "Um die Metadatengewinnung über die Benutzeroberfläche einzurichten, müssen Sie zuerst Airflow konfigurieren und damit verbinden. Weitere Details finden Sie in unserer <0>{{text}}</0>.",
|
||||
|
||||
@ -167,6 +167,7 @@
|
||||
"comment-lowercase": "comment",
|
||||
"completed": "Completed",
|
||||
"completed-entity": "Completed {{entity}}",
|
||||
"compute-row-count": "Compute Row Count",
|
||||
"condition": "Condition",
|
||||
"config": "Config",
|
||||
"configuration": "Configuration",
|
||||
@ -1236,6 +1237,7 @@
|
||||
"click-text-to-view-details": "Click <0>{{text}}</0> to view details.",
|
||||
"closed-this-task": "closed this task",
|
||||
"collaborate-with-other-user": "to collaborate with other users.",
|
||||
"compute-row-count-helper-text": "Compute the passed and failed row count for the test case.",
|
||||
"confidence-percentage-message": "Set the confidence level for the NLP model to use when infering whether a column contains PII data or not.",
|
||||
"configure-a-service-description": "Enter a unique service name. The name must be unique across the category of services. For e.g., among database services, both MySQL and Snowflake cannot have the same service name (E.g. customer_data). However, different service categories (dashboard, pipeline) can have the same service name. Spaces are not supported in the service name. Characters like - _ are supported. Also, add a description.",
|
||||
"configure-airflow": "To set up metadata extraction through UI, you first need to configure and connect to Airflow. For more details visit our <0>{{text}}</0>.",
|
||||
|
||||
@ -167,6 +167,7 @@
|
||||
"comment-lowercase": "comentario",
|
||||
"completed": "Completado",
|
||||
"completed-entity": "{{entity}} completado",
|
||||
"compute-row-count": "Compute Row Count",
|
||||
"condition": "Condición",
|
||||
"config": "Configuración",
|
||||
"configuration": "Configuration",
|
||||
@ -1236,6 +1237,7 @@
|
||||
"click-text-to-view-details": "Haz clic en <0>{{text}}</0> para ver detalles.",
|
||||
"closed-this-task": "cerró esta tarea",
|
||||
"collaborate-with-other-user": "para colaborar con otros usuarios.",
|
||||
"compute-row-count-helper-text": "Compute the passed and failed row count for the test case.",
|
||||
"confidence-percentage-message": "Establece el nivel de confianza para el modelo de NLP a utilizar al inferir si una columna contiene o no datos PII.",
|
||||
"configure-a-service-description": "Ingresa un nombre de servicio único. El nombre debe ser único en toda la categoría de servicios. Por ejemplo, entre los servicios de bases de datos, tanto MySQL como Snowflake no pueden tener el mismo nombre de servicio (por ejemplo, customer_data). Sin embargo, diferentes categorías de servicios (como dashboard, pipeline) pueden tener el mismo nombre de servicio. Los espacios no están permitidos en el nombre del servicio. Los caracteres como - _ son permitidos. Además, añade una descripción.",
|
||||
"configure-airflow": "Para configurar la extracción de metadatos a través de la interfaz de usuario, primero debes configurar y conectarte a Airflow. Para obtener más detalles, visita nuestro",
|
||||
|
||||
@ -167,6 +167,7 @@
|
||||
"comment-lowercase": "commentaire",
|
||||
"completed": "Terminé",
|
||||
"completed-entity": "Terminé {{entity}}",
|
||||
"compute-row-count": "Compute Row Count",
|
||||
"condition": "Condition",
|
||||
"config": "Configuration",
|
||||
"configuration": "Configuration",
|
||||
@ -1236,6 +1237,7 @@
|
||||
"click-text-to-view-details": "Cliquez sur <0>{{text}}</0> pour voir les détails.",
|
||||
"closed-this-task": "fermer cette tâche",
|
||||
"collaborate-with-other-user": "pour collaborer avec d'autres utilisateurs.",
|
||||
"compute-row-count-helper-text": "Compute the passed and failed row count for the test case.",
|
||||
"confidence-percentage-message": "Entrez le niveau de confiance à utiliser pour le modèle NLP lorsque celui-ci détermine si un champ contient des données PII.",
|
||||
"configure-a-service-description": "Entrez un nom de service unique. Le nom doit être unique dans la catégorie des services. Par exemple, parmi les services de base de données, MySQL et Snowflake ne peuvent pas avoir le même nom de service (par exemple, customer_data). Cependant, différentes catégories de services (tableau de bord, pipeline) peuvent avoir le même nom de service. Les espaces ne sont pas pris en charge dans le nom du service. Les caractères comme - _ sont pris en charge. Ajoutez également une description.",
|
||||
"configure-airflow": "Pour configurer l'extraction de métadonnées via l'interface utilisateur, vous devez d'abord configurer et vous connecter à Airflow. Pour plus de détails, visitez notre",
|
||||
|
||||
@ -167,6 +167,7 @@
|
||||
"comment-lowercase": "コメント",
|
||||
"completed": "完了",
|
||||
"completed-entity": "完了した{{entity}}",
|
||||
"compute-row-count": "Compute Row Count",
|
||||
"condition": "条件",
|
||||
"config": "設定",
|
||||
"configuration": "Configuration",
|
||||
@ -1236,6 +1237,7 @@
|
||||
"click-text-to-view-details": "Click <0>{{text}}</0> to view details.",
|
||||
"closed-this-task": "このタスクを閉じました",
|
||||
"collaborate-with-other-user": "to collaborate with other users.",
|
||||
"compute-row-count-helper-text": "Compute the passed and failed row count for the test case.",
|
||||
"confidence-percentage-message": "Set the confidence level for the NLP model to use when infering whether a column contains PII data or not.",
|
||||
"configure-a-service-description": "Enter a unique service name. The name must be unique across the category of services. For e.g., among database services, both MySQL and Snowflake cannot have the same service name (E.g. customer_data). However, different service categories (dashboard, pipeline) can have the same service name. Spaces are not supported in the service name. Characters like - _ are supported. Also, add a description.",
|
||||
"configure-airflow": "To set up metadata extraction through UI, you first need to configure and connect to Airflow. For more details visit our",
|
||||
|
||||
@ -167,6 +167,7 @@
|
||||
"comment-lowercase": "comentário",
|
||||
"completed": "Concluído",
|
||||
"completed-entity": "{{entity}} Concluído",
|
||||
"compute-row-count": "Compute Row Count",
|
||||
"condition": "Condição",
|
||||
"config": "Configuração",
|
||||
"configuration": "Configuração",
|
||||
@ -1236,6 +1237,7 @@
|
||||
"click-text-to-view-details": "Clique em <0>{{text}}</0> para ver detalhes.",
|
||||
"closed-this-task": "fechou esta tarefa",
|
||||
"collaborate-with-other-user": "para colaborar com outros usuários.",
|
||||
"compute-row-count-helper-text": "Compute the passed and failed row count for the test case.",
|
||||
"confidence-percentage-message": "Defina o nível de confiança para o modelo de PNL usar ao inferir se uma coluna contém dados de PII ou não.",
|
||||
"configure-a-service-description": "Insira um nome de serviço único. O nome deve ser único entre a categoria de serviços. Por exemplo, entre os serviços de banco de dados, MySQL e Snowflake não podem ter o mesmo nome de serviço (por exemplo, customer_data). No entanto, diferentes categorias de serviço (painel, pipeline) podem ter o mesmo nome de serviço. Espaços não são suportados no nome do serviço. Caracteres como - _ são suportados. Além disso, adicione uma descrição.",
|
||||
"configure-airflow": "Para configurar a extração de metadados através da UI, primeiro você precisa configurar e conectar-se ao Airflow. Para mais detalhes visite nosso <0>{{text}}</0>.",
|
||||
|
||||
@ -167,6 +167,7 @@
|
||||
"comment-lowercase": "комментарий",
|
||||
"completed": "Завершено",
|
||||
"completed-entity": "Завершенный {{entity}}",
|
||||
"compute-row-count": "Compute Row Count",
|
||||
"condition": "Состояние",
|
||||
"config": "Конфигурация",
|
||||
"configuration": "Конфигурация",
|
||||
@ -1236,6 +1237,7 @@
|
||||
"click-text-to-view-details": "Нажмите <0>{{text}}</0>, чтобы просмотреть подробности.",
|
||||
"closed-this-task": "задача закрыта",
|
||||
"collaborate-with-other-user": "для совместной работы с другими пользователями.",
|
||||
"compute-row-count-helper-text": "Compute the passed and failed row count for the test case.",
|
||||
"confidence-percentage-message": "Установите уровень достоверности для модели NLP, который будет использоваться при выводе о том, содержит ли столбец данные PII или нет.",
|
||||
"configure-a-service-description": "Введите уникальное имя сервиса. Имя должно быть уникальным для всей категории сервисов. Например, среди сервисов баз данных MySQL и Snowflake не могут иметь одинаковые имя сервисов (например, customer_data). Однако разные категории сервисов (панель мониторинга, пайплайн) могут иметь одно и то же имя. Пробелы в имени сервиса не поддерживаются. Поддерживаются такие символы, как -_. Также добавьте описание.",
|
||||
"configure-airflow": "Чтобы настроить извлечение метаданных через пользовательский интерфейс, сначала необходимо настроить и подключиться к Airflow. Для получения более подробной информации посетите наш <0>{{text}}</0>.",
|
||||
|
||||
@ -167,6 +167,7 @@
|
||||
"comment-lowercase": "评论",
|
||||
"completed": "已完成",
|
||||
"completed-entity": "已完成{{entity}}",
|
||||
"compute-row-count": "Compute Row Count",
|
||||
"condition": "条件",
|
||||
"config": "配置",
|
||||
"configuration": "配置",
|
||||
@ -1236,6 +1237,7 @@
|
||||
"click-text-to-view-details": "点击<0>{{text}}</0>查看详情",
|
||||
"closed-this-task": "关闭了此任务",
|
||||
"collaborate-with-other-user": "与其他用户协作",
|
||||
"compute-row-count-helper-text": "Compute the passed and failed row count for the test case.",
|
||||
"confidence-percentage-message": "设置 NLP 模型在推断列是否包含 PII 数据时,使用的置信度",
|
||||
"configure-a-service-description": "输入唯一的服务名称。名称必须在服务类别中唯一。例如,在数据库服务中,MySQL 和 Snowflake 不能拥有相同的服务名称(例如,customer_data)。但是,不同的服务类别(仪表板、工作流)可以拥有相同的服务名称。服务名称不支持空格。支持使用 - _ 等字符。同时,请添加一个描述。",
|
||||
"configure-airflow": "要通过本界面设置元数据提取,需要先配置和连接到 Airflow。更多说明内容,请访问我们的官方文档。",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user