ui: worked on DQ feedback part 3 (#12149)

* ui: worked on DQ feedback part 3

* updated name of test suite ingestion name
- Added description field in test case expand view
- Added functionality to update displayname of test case

* added testCase in re-indexing

* integrated permission in DQ
This commit is contained in:
Shailesh Parmar 2023-06-26 22:35:23 +05:30 committed by GitHub
parent 0010a4fe2a
commit fecb65c513
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 284 additions and 256 deletions

View File

@ -116,7 +116,7 @@ const EditTestCaseModal: React.FC<EditTestCaseModalProps> = ({
...testCase, ...testCase,
parameterValues, parameterValues,
description, description,
name: value.name, displayName: value.displayName,
}; };
const jsonPatch = compare(testCase, updatedTestCase); const jsonPatch = compare(testCase, updatedTestCase);
@ -124,10 +124,10 @@ const EditTestCaseModal: React.FC<EditTestCaseModalProps> = ({
try { try {
setIsLoadingOnSave(true); setIsLoadingOnSave(true);
const updateRes = await updateTestCaseById( const updateRes = await updateTestCaseById(
testCase.id || '', testCase.id ?? '',
jsonPatch jsonPatch
); );
onUpdate && onUpdate(updateRes); onUpdate?.(updateRes);
showSuccessToast( showSuccessToast(
t('server.update-entity-success', { entity: t('label.test-case') }) t('server.update-entity-success', { entity: t('label.test-case') })
); );
@ -216,17 +216,17 @@ const EditTestCaseModal: React.FC<EditTestCaseModalProps> = ({
layout="vertical" layout="vertical"
name="tableTestForm" name="tableTestForm"
onFinish={handleFormSubmit}> onFinish={handleFormSubmit}>
<Form.Item required label={`${t('label.table')}:`} name="table"> <Form.Item required label={`${t('label.table')}`} name="table">
<Input disabled /> <Input disabled />
</Form.Item> </Form.Item>
{isColumn && ( {isColumn && (
<Form.Item required label={`${t('label.column')}:`} name="column"> <Form.Item required label={`${t('label.column')}`} name="column">
<Input disabled /> <Input disabled />
</Form.Item> </Form.Item>
)} )}
<Form.Item <Form.Item
required required
label={`${t('label.name')}:`} label={`${t('label.name')}`}
name="name" name="name"
rules={[ rules={[
{ {
@ -236,6 +236,9 @@ const EditTestCaseModal: React.FC<EditTestCaseModalProps> = ({
]}> ]}>
<Input disabled placeholder={t('message.enter-test-case-name')} /> <Input disabled placeholder={t('message.enter-test-case-name')} />
</Form.Item> </Form.Item>
<Form.Item label={t('label.display-name')} name="displayName">
<Input placeholder={t('message.enter-test-case-name')} />
</Form.Item>
<Form.Item <Form.Item
required required
label={`${t('label.test-entity', { label={`${t('label.test-entity', {
@ -247,7 +250,7 @@ const EditTestCaseModal: React.FC<EditTestCaseModalProps> = ({
{GenerateParamsField()} {GenerateParamsField()}
<Form.Item label={`${t('label.description')}:`} name="description"> <Form.Item label={`${t('label.description')}`} name="description">
<RichTextEditor <RichTextEditor
height="200px" height="200px"
initialValue={testCase?.description || ''} initialValue={testCase?.description || ''}

View File

@ -22,6 +22,7 @@ import {
deployIngestionPipelineById, deployIngestionPipelineById,
updateIngestionPipeline as putIngestionPipeline, updateIngestionPipeline as putIngestionPipeline,
} from 'rest/ingestionPipelineAPI'; } from 'rest/ingestionPipelineAPI';
import { getIngestionName } from 'utils/ServiceUtils';
import { import {
DEPLOYED_PROGRESS_VAL, DEPLOYED_PROGRESS_VAL,
INGESTION_PROGRESS_END_VAL, INGESTION_PROGRESS_END_VAL,
@ -36,7 +37,8 @@ import {
import { IngestionPipeline } from '../../generated/entity/services/ingestionPipelines/ingestionPipeline'; import { IngestionPipeline } from '../../generated/entity/services/ingestionPipelines/ingestionPipeline';
import { import {
getIngestionFrequency, getIngestionFrequency,
replaceSpaceWith_, getNameFromFQN,
replaceAllSpacialCharWith_,
Transi18next, Transi18next,
} from '../../utils/CommonUtils'; } from '../../utils/CommonUtils';
import { showErrorToast } from '../../utils/ToastUtils'; import { showErrorToast } from '../../utils/ToastUtils';
@ -113,7 +115,12 @@ const TestSuiteIngestion: React.FC<TestSuiteIngestionProps> = ({
}; };
const createIngestionPipeline = async (repeatFrequency: string) => { const createIngestionPipeline = async (repeatFrequency: string) => {
const updatedName = replaceSpaceWith_(testSuite.name); const tableName = replaceAllSpacialCharWith_(
getNameFromFQN(
testSuite.executableEntityReference?.fullyQualifiedName ?? ''
)
);
const updatedName = getIngestionName(tableName, PipelineType.TestSuite);
const ingestionPayload: CreateIngestionPipeline = { const ingestionPayload: CreateIngestionPipeline = {
airflowConfig: { airflowConfig: {
@ -121,10 +128,10 @@ const TestSuiteIngestion: React.FC<TestSuiteIngestionProps> = ({
? undefined ? undefined
: repeatFrequency, : repeatFrequency,
}, },
name: `${updatedName}_${PipelineType.TestSuite}`, name: updatedName,
pipelineType: PipelineType.TestSuite, pipelineType: PipelineType.TestSuite,
service: { service: {
id: testSuite.id || '', id: testSuite.id ?? '',
type: camelCase(PipelineType.TestSuite), type: camelCase(PipelineType.TestSuite),
}, },
sourceConfig: { sourceConfig: {

View File

@ -138,16 +138,18 @@ const TestCaseForm: React.FC<TestCaseFormProps> = ({
: value, : value,
}) })
); );
const name =
value.testName?.trim() ||
`${columnName ? columnName : table.name}_${snakeCase(
selectedTestType
)}_${cryptoRandomString({
length: 4,
type: 'alphanumeric',
})}`;
return { return {
name: name,
value.testName?.trim() || displayName: name,
`${columnName ? columnName : table.name}_${snakeCase(
selectedTestType
)}_${cryptoRandomString({
length: 4,
type: 'alphanumeric',
})}`,
entityLink: generateEntityLink( entityLink: generateEntityLink(
isColumnFqn ? `${decodedEntityFQN}.${columnName}` : decodedEntityFQN, isColumnFqn ? `${decodedEntityFQN}.${columnName}` : decodedEntityFQN,
isColumnFqn isColumnFqn

View File

@ -10,11 +10,10 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import { Checkbox, Col, List, Modal, Row, Space, Typography } from 'antd'; import { Button, Checkbox, Col, List, Row, Space, Typography } from 'antd';
import { AxiosError } from 'axios';
import Searchbar from 'components/common/searchbar/Searchbar'; import Searchbar from 'components/common/searchbar/Searchbar';
import Loader from 'components/Loader/Loader'; import Loader from 'components/Loader/Loader';
import { getTableTabPath, PAGE_SIZE_MEDIUM } from 'constants/constants'; import { getTableTabPath, PAGE_SIZE } from 'constants/constants';
import { SearchIndex } from 'enums/search.enum'; import { SearchIndex } from 'enums/search.enum';
import { TestCase } from 'generated/tests/testCase'; import { TestCase } from 'generated/tests/testCase';
import { import {
@ -26,13 +25,11 @@ import React, { UIEventHandler, useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { searchQuery } from 'rest/searchAPI'; import { searchQuery } from 'rest/searchAPI';
import { addTestCaseToLogicalTestSuite } from 'rest/testAPI';
import { getNameFromFQN } from 'utils/CommonUtils'; import { getNameFromFQN } from 'utils/CommonUtils';
import { getEntityName } from 'utils/EntityUtils'; import { getEntityName } from 'utils/EntityUtils';
import { getDecodedFqn } from 'utils/StringsUtils'; import { getDecodedFqn } from 'utils/StringsUtils';
import { getEntityFqnFromEntityLink } from 'utils/TableUtils'; import { getEntityFqnFromEntityLink } from 'utils/TableUtils';
import { showErrorToast } from 'utils/ToastUtils'; import { AddTestCaseModalProps } from './AddTestCaseList.interface';
import { AddTestCaseModalProps } from './AddTestCaseModal.interface';
// Todo: need to help from backend guys for ES query // Todo: need to help from backend guys for ES query
// export const getQueryFilterToExcludeTest = (testCase: EntityReference[]) => ({ // export const getQueryFilterToExcludeTest = (testCase: EntityReference[]) => ({
@ -47,12 +44,12 @@ import { AddTestCaseModalProps } from './AddTestCaseModal.interface';
// }, // },
// }); // });
export const AddTestCaseModal = ({ export const AddTestCaseList = ({
open,
onCancel, onCancel,
existingTest, existingTest,
testSuiteId,
onSubmit, onSubmit,
cancelText,
submitText,
}: AddTestCaseModalProps) => { }: AddTestCaseModalProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [searchTerm, setSearchTerm] = useState<string>(''); const [searchTerm, setSearchTerm] = useState<string>('');
@ -74,7 +71,7 @@ export const AddTestCaseModal = ({
setIsLoading(true); setIsLoading(true);
const res = await searchQuery({ const res = await searchQuery({
pageNumber: page, pageNumber: page,
pageSize: PAGE_SIZE_MEDIUM, pageSize: PAGE_SIZE,
searchIndex: SearchIndex.TEST_CASE, searchIndex: SearchIndex.TEST_CASE,
query: searchText, query: searchText,
// queryFilter: getQueryFilterToExcludeTest(existingTest), // queryFilter: getQueryFilterToExcludeTest(existingTest),
@ -100,17 +97,8 @@ export const AddTestCaseModal = ({
const testCaseIds = [...(selectedItems?.values() ?? [])].map( const testCaseIds = [...(selectedItems?.values() ?? [])].map(
(test) => test.id ?? '' (test) => test.id ?? ''
); );
onSubmit(testCaseIds);
try { setIsLoading(false);
await addTestCaseToLogicalTestSuite({ testCaseIds, testSuiteId });
onSubmit();
onCancel();
} catch (error) {
showErrorToast(error as AxiosError);
} finally {
setIsLoading(false);
}
}; };
const onScroll: UIEventHandler<HTMLElement> = useCallback( const onScroll: UIEventHandler<HTMLElement> = useCallback(
@ -126,7 +114,7 @@ export const AddTestCaseModal = ({
}); });
} }
}, },
[searchTerm, totalCount, items] [searchTerm, totalCount, items, isLoading]
); );
const handleCardClick = (details: TestCase) => { const handleCardClick = (details: TestCase) => {
@ -160,99 +148,92 @@ export const AddTestCaseModal = ({
} }
}; };
useEffect(() => { useEffect(() => {
if (open) { fetchTestCases({ searchText: searchTerm });
fetchTestCases({ searchText: searchTerm }); }, [searchTerm]);
}
}, [open, searchTerm]);
return ( return (
<Modal <Row gutter={[0, 16]}>
centered <Col span={24}>
destroyOnClose <Searchbar
closable={false} removeMargin
okButtonProps={{ showClearSearch
loading: isLoading, showLoadingStatus
}} placeholder={t('label.search-entity', {
open={open} entity: t('label.test-case-plural'),
title={t('label.add-entity', { entity: t('label.test-case-plural') })} })}
width={750} searchValue={searchTerm}
onCancel={onCancel} onSearch={handleSearch}
onOk={handleSubmit}> />
<Row gutter={[0, 16]}> </Col>
<Col span={24}> <Col span={24}>
<Searchbar <List loading={{ spinning: false, indicator: <Loader /> }}>
removeMargin <VirtualList
showClearSearch data={items}
showLoadingStatus height={500}
placeholder={t('label.search-entity', { itemKey="id"
entity: t('label.test-case-plural'), onScroll={onScroll}>
})} {({ _source: test }) => {
searchValue={searchTerm} const tableFqn = getEntityFqnFromEntityLink(test.entityLink);
onSearch={handleSearch} const tableName = getNameFromFQN(tableFqn);
/> const isColumn = test.entityLink.includes('::columns::');
</Col>
<Col span={24}>
<List loading={{ spinning: false, indicator: <Loader /> }}>
<VirtualList
data={items}
height={500}
itemKey="id"
onScroll={onScroll}>
{({ _source: test }) => {
const tableFqn = getEntityFqnFromEntityLink(test.entityLink);
const tableName = getNameFromFQN(tableFqn);
const isColumn = test.entityLink.includes('::columns::');
return ( return (
<Space <Space
className="m-b-md border rounded-4 p-sm cursor-pointer" className="m-b-md border rounded-4 p-sm cursor-pointer"
direction="vertical" direction="vertical"
onClick={() => handleCardClick(test)}> onClick={() => handleCardClick(test)}>
<Space className="justify-between w-full"> <Space className="justify-between w-full">
<Typography.Paragraph <Typography.Paragraph
className="m-0 font-medium text-base" className="m-0 font-medium text-base"
data-testid={test.name}> data-testid={test.name}>
{getEntityName(test)} {getEntityName(test)}
</Typography.Paragraph> </Typography.Paragraph>
<Checkbox checked={selectedItems?.has(test.id ?? '')} /> <Checkbox checked={selectedItems?.has(test.id ?? '')} />
</Space>
<Typography.Paragraph className="m-0">
{getEntityName(test.testDefinition)}
</Typography.Paragraph>
<Typography.Paragraph className="m-0">
<Link
data-testid="table-link"
to={getTableTabPath(tableFqn, 'profiler')}
onClick={(e) => e.stopPropagation()}>
{tableName}
</Link>
</Typography.Paragraph>
{isColumn && (
<Space>
<Typography.Text className="font-medium text-xs">{`${t(
'label.column'
)}:`}</Typography.Text>
<Typography.Text className="text-grey-muted text-xs">
{getNameFromFQN(
getDecodedFqn(
getEntityFqnFromEntityLink(
test.entityLink,
isColumn
),
true
)
) ?? '--'}
</Typography.Text>
</Space>
)}
</Space> </Space>
); <Typography.Paragraph className="m-0">
}} {getEntityName(test.testDefinition)}
</VirtualList> </Typography.Paragraph>
</List> <Typography.Paragraph className="m-0">
</Col> <Link
</Row> data-testid="table-link"
</Modal> to={getTableTabPath(tableFqn, 'profiler')}
onClick={(e) => e.stopPropagation()}>
{tableName}
</Link>
</Typography.Paragraph>
{isColumn && (
<Space>
<Typography.Text className="font-medium text-xs">{`${t(
'label.column'
)}:`}</Typography.Text>
<Typography.Text className="text-grey-muted text-xs">
{getNameFromFQN(
getDecodedFqn(
getEntityFqnFromEntityLink(
test.entityLink,
isColumn
),
true
)
) ?? '--'}
</Typography.Text>
</Space>
)}
</Space>
);
}}
</VirtualList>
</List>
</Col>
<Col className="d-flex justify-end items-center p-y-xss" span={24}>
<Button type="link" onClick={onCancel}>
{cancelText ?? t('label.cancel')}
</Button>
<Button loading={isLoading} type="primary" onClick={handleSubmit}>
{submitText ?? t('label.submit')}
</Button>
</Col>
</Row>
); );
}; };

View File

@ -13,9 +13,9 @@
import { EntityReference } from 'generated/tests/testCase'; import { EntityReference } from 'generated/tests/testCase';
export interface AddTestCaseModalProps { export interface AddTestCaseModalProps {
open: boolean; onCancel?: () => void;
onCancel: () => void; onSubmit: (testCaseIds: string[]) => void;
onSubmit: () => void; existingTest?: EntityReference[];
existingTest: EntityReference[]; cancelText?: string;
testSuiteId: string; submitText?: string;
} }

View File

@ -13,6 +13,7 @@
import { Col, Row } from 'antd'; import { Col, Row } from 'antd';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import { SummaryCard } from 'components/common/SummaryCard/SummaryCard.component'; import { SummaryCard } from 'components/common/SummaryCard/SummaryCard.component';
import { usePermissionProvider } from 'components/PermissionProvider/PermissionProvider';
import { TestSummary } from 'generated/tests/testSuite'; import { TestSummary } from 'generated/tests/testSuite';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -23,6 +24,8 @@ import { SummaryPanelProps } from './SummaryPanel.interface';
export const SummaryPanel = ({ testSuiteId }: SummaryPanelProps) => { export const SummaryPanel = ({ testSuiteId }: SummaryPanelProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { permissions } = usePermissionProvider();
const { testCase: testCasePermission } = permissions;
const [summary, setSummary] = useState<TestSummary>(); const [summary, setSummary] = useState<TestSummary>();
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
@ -40,8 +43,10 @@ export const SummaryPanel = ({ testSuiteId }: SummaryPanelProps) => {
}; };
useEffect(() => { useEffect(() => {
fetchTestSummary(); if (testCasePermission?.ViewAll || testCasePermission?.ViewBasic) {
}, [testSuiteId]); fetchTestSummary();
}
}, [testSuiteId, testCasePermission]);
return ( return (
<Row wrap gutter={[16, 16]}> <Row wrap gutter={[16, 16]}>

View File

@ -13,6 +13,7 @@
import { Col, Row } from 'antd'; import { Col, Row } from 'antd';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import Searchbar from 'components/common/searchbar/Searchbar'; import Searchbar from 'components/common/searchbar/Searchbar';
import { usePermissionProvider } from 'components/PermissionProvider/PermissionProvider';
import DataQualityTab from 'components/ProfilerDashboard/component/DataQualityTab'; import DataQualityTab from 'components/ProfilerDashboard/component/DataQualityTab';
import { INITIAL_PAGING_VALUE, PAGE_SIZE } from 'constants/constants'; import { INITIAL_PAGING_VALUE, PAGE_SIZE } from 'constants/constants';
import { SearchIndex } from 'enums/search.enum'; import { SearchIndex } from 'enums/search.enum';
@ -29,7 +30,11 @@ import QueryString from 'qs';
import React, { useEffect, useMemo, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { useHistory, useLocation, useParams } from 'react-router-dom'; import { useHistory, useLocation, useParams } from 'react-router-dom';
import { searchQuery } from 'rest/searchAPI'; import { searchQuery } from 'rest/searchAPI';
import { getListTestCase, ListTestCaseParams } from 'rest/testAPI'; import {
getListTestCase,
getTestCaseById,
ListTestCaseParams,
} from 'rest/testAPI';
import { showErrorToast } from 'utils/ToastUtils'; import { showErrorToast } from 'utils/ToastUtils';
import { DataQualitySearchParams } from '../DataQuality.interface'; import { DataQualitySearchParams } from '../DataQuality.interface';
import { SummaryPanel } from '../SummaryPannel/SummaryPanel.component'; import { SummaryPanel } from '../SummaryPannel/SummaryPanel.component';
@ -38,6 +43,8 @@ export const TestCases = () => {
const history = useHistory(); const history = useHistory();
const location = useLocation(); const location = useLocation();
const { tab } = useParams<{ tab: DataQualityPageTabs }>(); const { tab } = useParams<{ tab: DataQualityPageTabs }>();
const { permissions } = usePermissionProvider();
const { testCase: testCasePermission } = permissions;
const params = useMemo(() => { const params = useMemo(() => {
const search = location.search; const search = location.search;
@ -115,16 +122,31 @@ export const TestCases = () => {
pageSize: PAGE_SIZE, pageSize: PAGE_SIZE,
searchIndex: SearchIndex.TEST_CASE, searchIndex: SearchIndex.TEST_CASE,
query: searchValue, query: searchValue,
fetchSource: false,
}); });
const hits = ( const promise = (
response.hits.hits as SearchHitBody< response.hits.hits as SearchHitBody<
SearchIndex.TEST_CASE, SearchIndex.TEST_CASE,
TestCaseSearchSource TestCaseSearchSource
>[] >[]
).map((value) => value._source); ).map((value) =>
getTestCaseById(value._id ?? '', {
fields: 'testDefinition,testCaseResult,testSuite',
})
);
const value = await Promise.allSettled(promise);
const testSuites = value.reduce((prev, curr) => {
if (curr.status === 'fulfilled') {
return [...prev, curr.value.data];
}
return prev;
}, [] as TestCase[]);
setTestCase({ setTestCase({
data: hits, data: testSuites,
paging: { total: response.hits.total.value ?? 0 }, paging: { total: response.hits.total.value ?? 0 },
}); });
} catch (error) { } catch (error) {
@ -151,14 +173,16 @@ export const TestCases = () => {
}; };
useEffect(() => { useEffect(() => {
if (tab === DataQualityPageTabs.TEST_CASES) { if (testCasePermission?.ViewAll || testCasePermission?.ViewBasic) {
if (searchValue) { if (tab === DataQualityPageTabs.TEST_CASES) {
searchTestCases(); if (searchValue) {
} else { searchTestCases();
fetchTestCases(); } else {
fetchTestCases();
}
} }
} }
}, [tab, searchValue]); }, [tab, searchValue, testCasePermission]);
return ( return (
<Row className="p-x-lg p-t-md" gutter={[16, 16]}> <Row className="p-x-lg p-t-md" gutter={[16, 16]}>

View File

@ -17,6 +17,7 @@ import FilterTablePlaceHolder from 'components/common/error-with-placeholder/Fil
import NextPrevious from 'components/common/next-previous/NextPrevious'; import NextPrevious from 'components/common/next-previous/NextPrevious';
import { OwnerLabel } from 'components/common/OwnerLabel/OwnerLabel.component'; import { OwnerLabel } from 'components/common/OwnerLabel/OwnerLabel.component';
import Searchbar from 'components/common/searchbar/Searchbar'; import Searchbar from 'components/common/searchbar/Searchbar';
import { usePermissionProvider } from 'components/PermissionProvider/PermissionProvider';
import { TableProfilerTab } from 'components/ProfilerDashboard/profilerDashboard.interface'; import { TableProfilerTab } from 'components/ProfilerDashboard/profilerDashboard.interface';
import ProfilerProgressWidget from 'components/TableProfiler/Component/ProfilerProgressWidget'; import ProfilerProgressWidget from 'components/TableProfiler/Component/ProfilerProgressWidget';
import { import {
@ -55,6 +56,8 @@ export const TestSuites = () => {
useParams<{ tab: DataQualityPageTabs }>(); useParams<{ tab: DataQualityPageTabs }>();
const history = useHistory(); const history = useHistory();
const location = useLocation(); const location = useLocation();
const { permissions } = usePermissionProvider();
const { testSuite: testSuitePermission } = permissions;
const [testSuites, setTestSuites] = useState<PagingResponse<TestSuite[]>>({ const [testSuites, setTestSuites] = useState<PagingResponse<TestSuite[]>>({
data: [], data: [],
@ -177,8 +180,10 @@ export const TestSuites = () => {
}; };
useEffect(() => { useEffect(() => {
fetchTestSuites(); if (testSuitePermission?.ViewAll || testSuitePermission?.ViewBasic) {
}, [tab]); fetchTestSuites();
}
}, [tab, testSuitePermission]);
return ( return (
<Row className="p-x-lg p-t-md" gutter={[16, 16]}> <Row className="p-x-lg p-t-md" gutter={[16, 16]}>
@ -192,13 +197,14 @@ export const TestSuites = () => {
/> />
</Col> </Col>
<Col> <Col>
{tab === DataQualityPageTabs.TEST_SUITES && ( {tab === DataQualityPageTabs.TEST_SUITES &&
<Link to={ROUTES.ADD_TEST_SUITES}> testSuitePermission?.Create && (
<Button type="primary"> <Link to={ROUTES.ADD_TEST_SUITES}>
{t('label.add-entity', { entity: t('label.test-suite') })} <Button type="primary">
</Button> {t('label.add-entity', { entity: t('label.test-suite') })}
</Link> </Button>
)} </Link>
)}
</Col> </Col>
</Row> </Row>
</Col> </Col>

View File

@ -11,8 +11,9 @@
* limitations under the License. * limitations under the License.
*/ */
import { Button, Col, Row, Typography } from 'antd'; import { Button, Col, Row, Space, Typography } from 'antd';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import RichTextEditorPreviewer from 'components/common/rich-text-editor/RichTextEditorPreviewer';
import DatePickerMenu from 'components/DatePickerMenu/DatePickerMenu.component'; import DatePickerMenu from 'components/DatePickerMenu/DatePickerMenu.component';
import { import {
GREEN_3, GREEN_3,
@ -97,13 +98,13 @@ const TestSummary: React.FC<TestSummaryProps> = ({
const values = result.testResultValue?.reduce((acc, curr) => { const values = result.testResultValue?.reduce((acc, curr) => {
return { return {
...acc, ...acc,
[curr.name || 'value']: round(parseFloat(curr.value ?? ''), 2) || 0, [curr.name ?? 'value']: round(parseFloat(curr.value ?? ''), 2) || 0,
}; };
}, {}); }, {});
chartData.push({ chartData.push({
name: getFormattedDateFromSeconds(result.timestamp as number), name: getFormattedDateFromSeconds(result.timestamp as number),
status: result.testCaseStatus || '', status: result.testCaseStatus ?? '',
...values, ...values,
}); });
}); });
@ -111,9 +112,9 @@ const TestSummary: React.FC<TestSummaryProps> = ({
setChartData({ setChartData({
information: information:
currentData[0]?.testResultValue?.map((info, i) => ({ currentData[0]?.testResultValue?.map((info, i) => ({
label: info.name || '', label: info.name ?? '',
color: COLORS[i], color: COLORS[i],
})) || [], })) ?? [],
data: chartData, data: chartData,
}); });
}; };
@ -251,13 +252,16 @@ const TestSummary: React.FC<TestSummaryProps> = ({
if (isSqlQuery) { if (isSqlQuery) {
return ( return (
<Row className="sql-expression-container" gutter={8} key={param.name}> <Row
<Col span={showExpandIcon ? 2 : 3}> className="sql-expression-container"
gutter={[8, 8]}
key={param.name}>
<Col span={24}>
<Typography.Text className="text-grey-muted"> <Typography.Text className="text-grey-muted">
{`${param.name}:`} {`${param.name}:`}
</Typography.Text> </Typography.Text>
</Col> </Col>
<Col span={showExpandIcon ? 22 : 21}> <Col span={24}>
<SchemaEditor <SchemaEditor
editorClass="table-query-editor" editorClass="table-query-editor"
mode={{ name: CSMode.SQL }} mode={{ name: CSMode.SQL }}
@ -344,30 +348,37 @@ const TestSummary: React.FC<TestSummaryProps> = ({
)} )}
</Col> </Col>
<Col span={24}> <Col span={24}>
{showParameters && ( <Row align="top" data-testid="params-container" gutter={[16, 16]}>
<Row align="top" data-testid="params-container" gutter={16}> {showParameters && (
<Col> <Col>
<Typography.Text className="text-grey-muted"> <Typography.Text className="text-grey-muted">
{`${t('label.parameter')}:`} {`${t('label.parameter')}:`}
</Typography.Text> </Typography.Text>
</Col>
<Col>
{!isEmpty(parameterValuesWithoutSqlExpression) ? ( {!isEmpty(parameterValuesWithoutSqlExpression) ? (
<Row className="parameter-value-container" gutter={[4, 4]}> <Row className="parameter-value-container" gutter={[4, 4]}>
{parameterValuesWithoutSqlExpression?.map(showParamsData)} {parameterValuesWithoutSqlExpression?.map(showParamsData)}
</Row> </Row>
) : ( ) : (
<Typography.Text type="secondary"> <Typography.Text className="m-l-xs" type="secondary">
{t('label.no-parameter-available')} {t('label.no-parameter-available')}
</Typography.Text> </Typography.Text>
)} )}
</Col> </Col>
</Row> )}
)} {!isUndefined(parameterValuesWithSqlExpression) ? (
<Col>{parameterValuesWithSqlExpression.map(showParamsData)}</Col>
{!isUndefined(parameterValuesWithSqlExpression) ) : null}
? parameterValuesWithSqlExpression.map(showParamsData) {data.description && (
: null} <Col>
<Space direction="vertical" size={4}>
<Typography.Text className="text-grey-muted">
{`${t('label.description')}:`}
</Typography.Text>
<RichTextEditorPreviewer markdown={data.description} />
</Space>
</Col>
)}
</Row>
</Col> </Col>
</Row> </Row>
); );

View File

@ -118,6 +118,7 @@ const AddTestSuiteForm: React.FC<AddTestSuiteFormProps> = ({
<Form.Item label={t('label.description')} name="description"> <Form.Item label={t('label.description')} name="description">
<RichTextEditor <RichTextEditor
data-testid="test-suite-description" data-testid="test-suite-description"
height="200px"
initialValue={testSuite?.description ?? ''} initialValue={testSuite?.description ?? ''}
onTextChange={(value) => form.setFieldsValue({ description: value })} onTextChange={(value) => form.setFieldsValue({ description: value })}
/> />

View File

@ -11,34 +11,28 @@
* limitations under the License. * limitations under the License.
*/ */
import { Button, Col, Form, Row, Select, Space, Typography } from 'antd'; import { Col, Row, Space, Typography } from 'antd';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import RightPanel from 'components/AddDataQualityTest/components/RightPanel'; import RightPanel from 'components/AddDataQualityTest/components/RightPanel';
import { getRightPanelForAddTestSuitePage } from 'components/AddDataQualityTest/rightPanelData'; import { getRightPanelForAddTestSuitePage } from 'components/AddDataQualityTest/rightPanelData';
import { AddTestCaseList } from 'components/AddTestCaseList/AddTestCaseList.component';
import ResizablePanels from 'components/common/ResizablePanels/ResizablePanels'; import ResizablePanels from 'components/common/ResizablePanels/ResizablePanels';
import SuccessScreen from 'components/common/success-screen/SuccessScreen'; import SuccessScreen from 'components/common/success-screen/SuccessScreen';
import TitleBreadcrumb from 'components/common/title-breadcrumb/title-breadcrumb.component'; import TitleBreadcrumb from 'components/common/title-breadcrumb/title-breadcrumb.component';
import IngestionStepper from 'components/IngestionStepper/IngestionStepper.component'; import IngestionStepper from 'components/IngestionStepper/IngestionStepper.component';
import { HTTP_STATUS_CODE } from 'constants/auth.constants'; import { HTTP_STATUS_CODE } from 'constants/auth.constants';
import { PAGE_SIZE_LARGE } from 'constants/constants';
import { import {
STEPS_FOR_ADD_TEST_SUITE, STEPS_FOR_ADD_TEST_SUITE,
TEST_SUITE_STEPPER_BREADCRUMB, TEST_SUITE_STEPPER_BREADCRUMB,
} from 'constants/TestSuite.constant'; } from 'constants/TestSuite.constant';
import { FormSubmitType } from 'enums/form.enum'; import { FormSubmitType } from 'enums/form.enum';
import { OwnerType } from 'enums/user.enum'; import { OwnerType } from 'enums/user.enum';
import { TestCase } from 'generated/tests/testCase';
import { TestSuite } from 'generated/tests/testSuite'; import { TestSuite } from 'generated/tests/testSuite';
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { import { addTestCaseToLogicalTestSuite, createTestSuites } from 'rest/testAPI';
addTestCaseToLogicalTestSuite,
createTestSuites,
getListTestCase,
} from 'rest/testAPI';
import { getCurrentUserId } from 'utils/CommonUtils'; import { getCurrentUserId } from 'utils/CommonUtils';
import { getEntityName } from 'utils/EntityUtils';
import { getTestSuitePath } from 'utils/RouterUtils'; import { getTestSuitePath } from 'utils/RouterUtils';
import { showErrorToast } from 'utils/ToastUtils'; import { showErrorToast } from 'utils/ToastUtils';
import AddTestSuiteForm from '../AddTestSuiteForm/AddTestSuiteForm'; import AddTestSuiteForm from '../AddTestSuiteForm/AddTestSuiteForm';
@ -48,11 +42,9 @@ const TestSuiteStepper = () => {
const history = useHistory(); const history = useHistory();
const [activeServiceStep, setActiveServiceStep] = useState(1); const [activeServiceStep, setActiveServiceStep] = useState(1);
const [testSuiteResponse, setTestSuiteResponse] = useState<TestSuite>(); const [testSuiteResponse, setTestSuiteResponse] = useState<TestSuite>();
const [testCases, setTestCases] = useState<TestCase[]>([]);
const [selectedTestCase, setSelectedTestCase] = useState<string[]>([]);
const handleViewTestSuiteClick = () => { const handleViewTestSuiteClick = () => {
history.push(getTestSuitePath(testSuiteResponse?.fullyQualifiedName || '')); history.push(getTestSuitePath(testSuiteResponse?.fullyQualifiedName ?? ''));
}; };
const handleTestSuitNextClick = (data: TestSuite) => { const handleTestSuitNextClick = (data: TestSuite) => {
@ -60,7 +52,7 @@ const TestSuiteStepper = () => {
setActiveServiceStep(2); setActiveServiceStep(2);
}; };
const onSubmit = async (data: { testCase: string[] }) => { const onSubmit = async (data: string[]) => {
try { try {
const owner = { const owner = {
id: getCurrentUserId(), id: getCurrentUserId(),
@ -74,7 +66,7 @@ const TestSuiteStepper = () => {
}); });
setTestSuiteResponse(response); setTestSuiteResponse(response);
await addTestCaseToLogicalTestSuite({ await addTestCaseToLogicalTestSuite({
testCaseIds: data.testCase, testCaseIds: data,
testSuiteId: response.id ?? '', testSuiteId: response.id ?? '',
}); });
setActiveServiceStep(3); setActiveServiceStep(3);
@ -100,61 +92,15 @@ const TestSuiteStepper = () => {
} }
}; };
const fetchTestCases = async () => {
try {
const response = await getListTestCase({ limit: PAGE_SIZE_LARGE });
setTestCases(response.data);
} catch (error) {
setTestCases([]);
}
};
useEffect(() => {
fetchTestCases();
}, []);
const selectTestCase = useMemo(() => {
return (
<Form
data-testid="test-case-form"
initialValues={{ testCase: selectedTestCase }}
layout="vertical"
name="selectTestCase"
onFinish={onSubmit}
onValuesChange={({ testCase }) => setSelectedTestCase(testCase)}>
<Form.Item label={t('label.test-case-plural')} name="testCase">
<Select
mode="multiple"
placeholder={t('label.please-select-entity', {
entity: t('label.test-case-plural'),
})}>
{testCases.map((test) => (
<Select.Option key={test.id}>{getEntityName(test)}</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item noStyle>
<Space className="w-full justify-end" size={16}>
<Button
data-testid="back-button"
onClick={() => setActiveServiceStep(1)}>
{t('label.back')}
</Button>
<Button
data-testid="submit-button"
htmlType="submit"
type="primary">
{t('label.submit')}
</Button>
</Space>
</Form.Item>
</Form>
);
}, [testCases, selectedTestCase, testSuiteResponse]);
const RenderSelectedTab = useCallback(() => { const RenderSelectedTab = useCallback(() => {
if (activeServiceStep === 2) { if (activeServiceStep === 2) {
return selectTestCase; return (
<AddTestCaseList
cancelText={t('label.back')}
onCancel={() => setActiveServiceStep(1)}
onSubmit={onSubmit}
/>
);
} else if (activeServiceStep === 3) { } else if (activeServiceStep === 3) {
return ( return (
<SuccessScreen <SuccessScreen

View File

@ -72,6 +72,10 @@ export const ELASTIC_SEARCH_INDEX_ENTITIES = [
value: 'query', value: 'query',
label: t('label.query'), label: t('label.query'),
}, },
{
value: 'testCase',
label: t('label.test-case'),
},
]; ];
export const ELASTIC_SEARCH_INITIAL_VALUES = { export const ELASTIC_SEARCH_INITIAL_VALUES = {

View File

@ -11,9 +11,9 @@
* limitations under the License. * limitations under the License.
*/ */
import { Button, Col, Row, Space } from 'antd'; import { Button, Col, Modal, Row, Space } from 'antd';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import { AddTestCaseModal } from 'components/AddTestCaseModal/AddTestCaseModal.component'; import { AddTestCaseList } from 'components/AddTestCaseList/AddTestCaseList.component';
import { useAuthContext } from 'components/authentication/auth-provider/AuthProvider'; import { useAuthContext } from 'components/authentication/auth-provider/AuthProvider';
import Description from 'components/common/description/Description'; import Description from 'components/common/description/Description';
import ManageButton from 'components/common/entityPageInfo/ManageButton/ManageButton'; import ManageButton from 'components/common/entityPageInfo/ManageButton/ManageButton';
@ -38,6 +38,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useHistory, useParams } from 'react-router-dom'; import { useHistory, useParams } from 'react-router-dom';
import { import {
addTestCaseToLogicalTestSuite,
getListTestCase, getListTestCase,
getTestSuiteByName, getTestSuiteByName,
ListTestCaseParams, ListTestCaseParams,
@ -154,6 +155,19 @@ const TestSuiteDetailsPage = () => {
fetchTestCases(); fetchTestCases();
}; };
const handleAddTestCaseSubmit = async (testCaseIds: string[]) => {
try {
await addTestCaseToLogicalTestSuite({
testCaseIds,
testSuiteId: testSuite?.id ?? '',
});
setIsTestCaseModalOpen(false);
fetchTestCases();
} catch (error) {
showErrorToast(error as AxiosError);
}
};
const fetchTestSuiteByName = async () => { const fetchTestSuiteByName = async () => {
try { try {
const response = await getTestSuiteByName(testSuiteFQN, { const response = await getTestSuiteByName(testSuiteFQN, {
@ -309,11 +323,16 @@ const TestSuiteDetailsPage = () => {
titleLinks={slashedBreadCrumb} titleLinks={slashedBreadCrumb}
/> />
<Space> <Space>
<Button {(testSuitePermissions.EditAll ||
type="primary" testSuitePermissions.EditTests) && (
onClick={() => setIsTestCaseModalOpen(true)}> <Button
{t('label.add-entity', { entity: t('label.test-case-plural') })} type="primary"
</Button> onClick={() => setIsTestCaseModalOpen(true)}>
{t('label.add-entity', {
entity: t('label.test-case-plural'),
})}
</Button>
)}
<ManageButton <ManageButton
isRecursiveDelete isRecursiveDelete
afterDeleteAction={afterDeleteAction} afterDeleteAction={afterDeleteAction}
@ -366,13 +385,22 @@ const TestSuiteDetailsPage = () => {
/> />
</Col> </Col>
<Col span={24}> <Col span={24}>
<AddTestCaseModal <Modal
existingTest={testSuite?.tests ?? []} centered
destroyOnClose
closable={false}
footer={null}
open={isTestCaseModalOpen} open={isTestCaseModalOpen}
testSuiteId={testSuite?.id ?? ''} title={t('label.add-entity', {
onCancel={() => setIsTestCaseModalOpen(false)} entity: t('label.test-case-plural'),
onSubmit={afterSubmitAction} })}
/> width={750}>
<AddTestCaseList
existingTest={testSuite?.tests ?? []}
onCancel={() => setIsTestCaseModalOpen(false)}
onSubmit={handleAddTestCaseSubmit}
/>
</Modal>
</Col> </Col>
</Row> </Row>
</PageLayoutV1> </PageLayoutV1>

View File

@ -113,6 +113,16 @@ export const getTestCaseByFqn = async (
return response; return response;
}; };
export const getTestCaseById = async (
id: string,
params?: Pick<ListParams, 'fields' | 'include'>
) => {
const response = await APIClient.get<TestCase>(`${testCaseUrl}/${id}`, {
params,
});
return response;
};
export const createTestCase = async (data: CreateTestCase) => { export const createTestCase = async (data: CreateTestCase) => {
const response = await APIClient.post< const response = await APIClient.post<