Display inspection query (#16766)

* Display inspection query

* added reset for global state

* fixed test failure
This commit is contained in:
Shailesh Parmar 2024-06-25 11:09:53 +05:30 committed by GitHub
parent b55890f1f9
commit 04d3720df9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 257 additions and 151 deletions

View File

@ -30,9 +30,9 @@ import {
Thread,
ThreadTaskStatus,
} from '../../../../generated/entity/feed/thread';
import { EntityReference } from '../../../../generated/entity/type';
import { useElementInView } from '../../../../hooks/useElementInView';
import { useFqn } from '../../../../hooks/useFqn';
import { useTestCaseStore } from '../../../../pages/IncidentManager/IncidentManagerDetailPage/useTestCase.store';
import ActivityFeedListV1 from '../../../ActivityFeed/ActivityFeedList/ActivityFeedListV1.component';
import { useActivityFeedProvider } from '../../../ActivityFeed/ActivityFeedProvider/ActivityFeedProvider';
import { TaskFilter } from '../../../ActivityFeed/ActivityFeedTab/ActivityFeedTab.interface';
@ -40,9 +40,12 @@ import Loader from '../../../common/Loader/Loader';
import { TaskTab } from '../../../Entity/Task/TaskTab/TaskTab.component';
import './test-case-incident-tab.style.less';
const TestCaseIncidentTab = ({ owner }: { owner?: EntityReference }) => {
const TestCaseIncidentTab = () => {
const { t } = useTranslation();
const { fqn: decodedFqn } = useFqn();
const { testCase } = useTestCaseStore();
const owner = useMemo(() => testCase?.owner, [testCase]);
const {
selectedThread,

View File

@ -25,6 +25,22 @@ const mockUseActivityFeedProviderValue = {
setActiveThread: jest.fn(),
};
jest.mock(
'../../../../pages/IncidentManager/IncidentManagerDetailPage/useTestCase.store',
() => ({
useTestCaseStore: jest.fn().mockImplementation(() => ({
testCase: {
owner: {
name: 'arron_johnson',
displayName: 'Arron Johnson',
id: '1',
type: 'user',
},
},
})),
})
);
jest.mock('../../../Entity/Task/TaskTab/TaskTab.component', () => {
return {
TaskTab: jest.fn().mockImplementation(({ onAfterClose }) => (

View File

@ -30,6 +30,7 @@ import { EntityType } from '../../../../enums/entity.enum';
import { Operation } from '../../../../generated/entity/policies/policy';
import { TestCaseParameterValue } from '../../../../generated/tests/testCase';
import { useTestCaseStore } from '../../../../pages/IncidentManager/IncidentManagerDetailPage/useTestCase.store';
import { updateTestCaseById } from '../../../../rest/testAPI';
import { checkPermission } from '../../../../utils/PermissionsUtils';
import { showErrorToast, showSuccessToast } from '../../../../utils/ToastUtils';
@ -39,14 +40,11 @@ import SchemaEditor from '../../../Database/SchemaEditor/SchemaEditor';
import EditTestCaseModal from '../../AddDataQualityTest/EditTestCaseModal';
import '../incident-manager.style.less';
import './test-case-result-tab.style.less';
import { TestCaseResultTabProps } from './TestCaseResultTab.interface';
import testCaseResultTabClassBase from './TestCaseResultTabClassBase';
const TestCaseResultTab = ({
testCaseData,
onTestCaseUpdate,
}: TestCaseResultTabProps) => {
const TestCaseResultTab = () => {
const { t } = useTranslation();
const { testCase: testCaseData, setTestCase } = useTestCaseStore();
const additionalComponent =
testCaseResultTabClassBase.getAdditionalComponents();
const [isDescriptionEdit, setIsDescriptionEdit] = useState<boolean>(false);
@ -95,7 +93,7 @@ const TestCaseResultTab = ({
testCaseData.id ?? '',
jsonPatch
);
onTestCaseUpdate(res);
setTestCase(res);
showSuccessToast(
t('server.update-entity-success', {
entity: t('label.test-case'),
@ -109,7 +107,7 @@ const TestCaseResultTab = ({
}
}
},
[testCaseData, updateTestCaseById, onTestCaseUpdate]
[testCaseData, updateTestCaseById, setTestCase]
);
const handleCancelParameter = useCallback(
@ -225,7 +223,7 @@ const TestCaseResultTab = ({
testCase={testCaseData}
visible={isParameterEdit}
onCancel={handleCancelParameter}
onUpdate={onTestCaseUpdate}
onUpdate={setTestCase}
/>
)}
</Row>

View File

@ -1,19 +0,0 @@
/*
* Copyright 2023 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 { TestCase } from '../../../../generated/tests/testCase';
export interface TestCaseResultTabProps {
testCaseData?: TestCase;
onTestCaseUpdate: (data: TestCase) => void;
}

View File

@ -21,10 +21,8 @@ import React from 'react';
import { TestCase } from '../../../../generated/tests/testCase';
import { checkPermission } from '../../../../utils/PermissionsUtils';
import TestCaseResultTab from './TestCaseResultTab.component';
import { TestCaseResultTabProps } from './TestCaseResultTab.interface';
const mockProps: TestCaseResultTabProps = {
testCaseData: {
const mockTestCaseData = {
id: '1b748634-d24b-4879-9791-289f2f90fc3c',
name: 'table_column_count_equals',
fullyQualifiedName:
@ -65,10 +63,19 @@ const mockProps: TestCaseResultTabProps = {
},
updatedAt: 1703570589915,
updatedBy: 'admin',
} as TestCase,
onTestCaseUpdate: jest.fn(),
};
} as TestCase;
const mockFunc = jest.fn();
jest.mock(
'../../../../pages/IncidentManager/IncidentManagerDetailPage/useTestCase.store',
() => ({
useTestCaseStore: jest.fn().mockImplementation(() => ({
testCase: mockTestCaseData,
setTestCase: mockFunc,
})),
})
);
jest.mock('../../../common/EntityDescription/DescriptionV1', () => {
return jest.fn().mockImplementation(() => <div>DescriptionV1</div>);
});
@ -97,7 +104,7 @@ jest.mock('../../../../utils/PermissionsUtils', () => ({
describe('TestCaseResultTab', () => {
it('Should render component', async () => {
render(<TestCaseResultTab {...mockProps} />);
render(<TestCaseResultTab />);
expect(
await screen.findByTestId('test-case-result-tab-container')
@ -113,7 +120,7 @@ describe('TestCaseResultTab', () => {
});
it("EditTestCaseModal should be rendered when 'Edit' button is clicked", async () => {
render(<TestCaseResultTab {...mockProps} />);
render(<TestCaseResultTab />);
const editButton = await screen.findByTestId('edit-parameter-icon');
fireEvent.click(editButton);
@ -122,7 +129,7 @@ describe('TestCaseResultTab', () => {
});
it('EditTestCaseModal should be removed on cancel click', async () => {
const { container } = render(<TestCaseResultTab {...mockProps} />);
const { container } = render(<TestCaseResultTab />);
const editButton = await screen.findByTestId('edit-parameter-icon');
fireEvent.click(editButton);
@ -136,7 +143,7 @@ describe('TestCaseResultTab', () => {
});
it('onTestCaseUpdate should be called while updating params', async () => {
render(<TestCaseResultTab {...mockProps} />);
render(<TestCaseResultTab />);
const editButton = await screen.findByTestId('edit-parameter-icon');
fireEvent.click(editButton);
@ -146,14 +153,12 @@ describe('TestCaseResultTab', () => {
const updateButton = await screen.findByTestId('update-test');
fireEvent.click(updateButton);
expect(mockProps.onTestCaseUpdate).toHaveBeenCalledWith(
mockProps.testCaseData
);
expect(mockFunc).toHaveBeenCalledWith(mockTestCaseData);
});
it("Should not show edit icon if user doesn't have edit permission", () => {
(checkPermission as jest.Mock).mockReturnValueOnce(false);
const { container } = render(<TestCaseResultTab {...mockProps} />);
const { container } = render(<TestCaseResultTab />);
const editButton = queryByTestId(container, 'edit-parameter-icon');

View File

@ -15,6 +15,7 @@ import { TestCaseResolutionStatus } from '../../generated/tests/testCaseResoluti
export enum IncidentManagerTabs {
TEST_CASE_RESULTS = 'test-case-results',
SQL_QUERY = 'sql-query',
ISSUES = 'issues',
}
export interface TestCaseIncidentStatusData {

View File

@ -13,10 +13,12 @@
import { act, fireEvent, render, screen } from '@testing-library/react';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { TestCase } from '../../../generated/tests/testCase';
import { getTestCaseByFqn } from '../../../rest/testAPI';
import { checkPermission } from '../../../utils/PermissionsUtils';
import { IncidentManagerTabs } from '../IncidentManager.interface';
import IncidentManagerDetailPage from './IncidentManagerDetailPage';
import { UseTestCaseStoreInterface } from './useTestCase.store';
const mockTestCaseData = {
id: '1b748634-d24b-4879-9791-289f2f90fc3c',
@ -67,7 +69,17 @@ const mockTestCaseData = {
version: 0.1,
updatedAt: 1703570589915,
updatedBy: 'admin',
} as TestCase;
const mockUseTestCase: UseTestCaseStoreInterface = {
testCase: mockTestCaseData,
setTestCase: jest.fn(),
isLoading: false,
setIsLoading: jest.fn(),
reset: jest.fn(),
};
jest.mock('./useTestCase.store', () => ({
useTestCaseStore: jest.fn().mockImplementation(() => mockUseTestCase),
}));
jest.mock('../../../rest/testAPI', () => ({
getTestCaseByFqn: jest
@ -179,13 +191,17 @@ describe('IncidentManagerDetailPage', () => {
});
it('should render no data placeholder message if there is no data', async () => {
mockUseTestCase.testCase = undefined;
(getTestCaseByFqn as jest.Mock).mockImplementationOnce(() =>
Promise.reject()
);
await act(async () => {
render(<IncidentManagerDetailPage />, { wrapper: MemoryRouter });
});
expect(await screen.findByText('ErrorPlaceHolder')).toBeInTheDocument();
mockUseTestCase.testCase = mockTestCaseData;
});
});

View File

@ -22,12 +22,9 @@ import ActivityFeedProvider from '../../../components/ActivityFeed/ActivityFeedP
import ManageButton from '../../../components/common/EntityPageInfos/ManageButton/ManageButton';
import ErrorPlaceHolder from '../../../components/common/ErrorWithPlaceholder/ErrorPlaceHolder';
import Loader from '../../../components/common/Loader/Loader';
import TabsLabel from '../../../components/common/TabsLabel/TabsLabel.component';
import TitleBreadcrumb from '../../../components/common/TitleBreadcrumb/TitleBreadcrumb.component';
import { TitleBreadcrumbProps } from '../../../components/common/TitleBreadcrumb/TitleBreadcrumb.interface';
import IncidentManagerPageHeader from '../../../components/DataQuality/IncidentManager/IncidentManagerPageHeader/IncidentManagerPageHeader.component';
import TestCaseIncidentTab from '../../../components/DataQuality/IncidentManager/TestCaseIncidentTab/TestCaseIncidentTab.component';
import TestCaseResultTab from '../../../components/DataQuality/IncidentManager/TestCaseResultTab/TestCaseResultTab.component';
import EntityHeaderTitle from '../../../components/Entity/EntityHeaderTitle/EntityHeaderTitle.component';
import { EntityName } from '../../../components/Modals/EntityNameModal/EntityNameModal.interface';
import PageLayoutV1 from '../../../components/PageLayoutV1/PageLayoutV1';
@ -38,7 +35,7 @@ import { ResourceEntity } from '../../../context/PermissionProvider/PermissionPr
import { ERROR_PLACEHOLDER_TYPE } from '../../../enums/common.enum';
import { EntityTabs, EntityType } from '../../../enums/entity.enum';
import { Operation } from '../../../generated/entity/policies/policy';
import { EntityReference, TestCase } from '../../../generated/tests/testCase';
import { EntityReference } from '../../../generated/tests/testCase';
import { useFqn } from '../../../hooks/useFqn';
import { FeedCounts } from '../../../interface/feed.interface';
import { getTestCaseByFqn, updateTestCaseById } from '../../../rest/testAPI';
@ -47,7 +44,8 @@ import { checkPermission } from '../../../utils/PermissionsUtils';
import { getIncidentManagerDetailPagePath } from '../../../utils/RouterUtils';
import { showErrorToast } from '../../../utils/ToastUtils';
import { IncidentManagerTabs } from '../IncidentManager.interface';
import { TestCaseData } from './IncidentManagerDetailPage.interface';
import testCaseClassBase from './TestCaseClassBase';
import { useTestCaseStore } from './useTestCase.store';
const IncidentManagerDetailPage = () => {
const { t } = useTranslation();
@ -60,10 +58,8 @@ const IncidentManagerDetailPage = () => {
const { fqn: testCaseFQN } = useFqn();
const [testCaseData, setTestCaseData] = useState<TestCaseData>({
data: undefined,
isLoading: true,
});
const { isLoading, setIsLoading, setTestCase, testCase, reset } =
useTestCaseStore();
const [feedCount, setFeedCount] = useState<FeedCounts>(
FEED_COUNT_INITIAL_DATA
);
@ -90,59 +86,33 @@ const IncidentManagerDetailPage = () => {
};
}, [permissions]);
const onTestCaseUpdate = (data: TestCase) => {
setTestCaseData((prev) => ({ ...prev, data }));
};
const tabDetails: TabsProps['items'] = useMemo(() => {
const tabs = testCaseClassBase.getTab(feedCount.openTaskCount);
const tabDetails: TabsProps['items'] = useMemo(
() => [
{
label: (
<TabsLabel id="test-case-result" name={t('label.test-case-result')} />
),
children: (
<TestCaseResultTab
testCaseData={testCaseData.data}
onTestCaseUpdate={onTestCaseUpdate}
/>
),
key: IncidentManagerTabs.TEST_CASE_RESULTS,
},
{
label: (
<TabsLabel
count={feedCount.openTaskCount}
id="incident"
name={t('label.incident')}
/>
),
key: IncidentManagerTabs.ISSUES,
children: <TestCaseIncidentTab owner={testCaseData.data?.owner} />,
},
],
[testCaseData, feedCount.openTaskCount]
);
return tabs.map(({ LabelComponent, labelProps, key, Tab }) => ({
key,
label: <LabelComponent {...labelProps} />,
children: <Tab />,
}));
}, [feedCount.openTaskCount, testCaseClassBase.showSqlQueryTab]);
const fetchTestCaseData = async () => {
setTestCaseData((prev) => ({ ...prev, isLoading: true }));
setIsLoading(true);
try {
const response = await getTestCaseByFqn(testCaseFQN, {
fields: [
'testSuite',
'testCaseResult',
'testDefinition',
'owner',
'incidentId',
],
fields: testCaseClassBase.getFields(),
});
setTestCaseData((prev) => ({ ...prev, data: response }));
testCaseClassBase.setShowSqlQueryTab(
!isUndefined(response.inspectionQuery)
);
setTestCase(response);
} catch (error) {
showErrorToast(
error as AxiosError,
t('server.entity-fetch-error', { entity: t('label.test-case') })
);
} finally {
setTestCaseData((prev) => ({ ...prev, isLoading: false }));
setIsLoading(false);
}
};
@ -160,12 +130,12 @@ const IncidentManagerDetailPage = () => {
return [
...data,
{
name: testCaseData?.data?.name ?? '',
name: testCase?.name ?? '',
url: '',
activeTitle: true,
},
];
}, [testCaseData]);
}, [testCase]);
const handleTabChange = (activeKey: string) => {
if (activeKey !== activeTab) {
@ -180,38 +150,36 @@ const IncidentManagerDetailPage = () => {
const updateTestCase = async (id: string, patch: PatchOperation[]) => {
try {
const res = await updateTestCaseById(id, patch);
onTestCaseUpdate(res);
setTestCase(res);
} catch (error) {
showErrorToast(error as AxiosError);
}
};
const handleOwnerChange = async (owner?: EntityReference) => {
const data = testCaseData.data;
if (data) {
if (testCase) {
const updatedTestCase = {
...data,
...testCase,
owner,
};
const jsonPatch = compare(data, updatedTestCase);
const jsonPatch = compare(testCase, updatedTestCase);
if (jsonPatch.length && data.id) {
await updateTestCase(data.id, jsonPatch);
if (jsonPatch.length && testCase.id) {
await updateTestCase(testCase.id, jsonPatch);
}
}
};
const handleDisplayNameChange = async (entityName?: EntityName) => {
try {
const data = testCaseData.data;
if (data) {
if (testCase) {
const updatedTestCase = {
...data,
...testCase,
...entityName,
};
const jsonPatch = compare(data, updatedTestCase);
const jsonPatch = compare(testCase, updatedTestCase);
if (jsonPatch.length && data.id) {
await updateTestCase(data.id, jsonPatch);
if (jsonPatch.length && testCase.id) {
await updateTestCase(testCase.id, jsonPatch);
}
}
} catch (error) {
@ -232,11 +200,17 @@ const IncidentManagerDetailPage = () => {
fetchTestCaseData();
getEntityFeedCount();
} else {
setTestCaseData((prev) => ({ ...prev, isLoading: false }));
setIsLoading(false);
}
// Cleanup function for unmount
return () => {
reset();
testCaseClassBase.setShowSqlQueryTab(false);
};
}, [testCaseFQN, hasViewPermission]);
if (testCaseData.isLoading) {
if (isLoading) {
return <Loader />;
}
@ -244,7 +218,7 @@ const IncidentManagerDetailPage = () => {
return <ErrorPlaceHolder type={ERROR_PLACEHOLDER_TYPE.PERMISSION} />;
}
if (isUndefined(testCaseData.data)) {
if (isUndefined(testCase)) {
return <ErrorPlaceHolder />;
}
@ -262,9 +236,9 @@ const IncidentManagerDetailPage = () => {
<Col span={23}>
<EntityHeaderTitle
className="w-max-full-45"
displayName={testCaseData.data?.displayName}
displayName={testCase?.displayName}
icon={<TestCaseIcon className="h-9" />}
name={testCaseData.data?.name ?? ''}
name={testCase?.name ?? ''}
serviceName="testCase"
/>
</Col>
@ -276,11 +250,11 @@ const IncidentManagerDetailPage = () => {
}
allowSoftDelete={false}
canDelete={hasDeletePermission}
displayName={testCaseData.data.displayName}
displayName={testCase.displayName}
editDisplayNamePermission={editDisplayNamePermission}
entityFQN={testCaseData.data.fullyQualifiedName}
entityId={testCaseData.data.id}
entityName={testCaseData.data.name}
entityFQN={testCase.fullyQualifiedName}
entityId={testCase.id}
entityName={testCase.name}
entityType={EntityType.TEST_CASE}
onEditDisplayName={handleDisplayNameChange}
/>
@ -290,7 +264,7 @@ const IncidentManagerDetailPage = () => {
<Col className="p-x-lg">
<IncidentManagerPageHeader
fetchTaskCount={getEntityFeedCount}
testCaseData={testCaseData.data}
testCaseData={testCase}
onOwnerUpdate={handleOwnerChange}
/>
</Col>

View File

@ -0,0 +1,77 @@
/*
* 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 { ReactElement } from 'react';
import TabsLabel from '../../../components/common/TabsLabel/TabsLabel.component';
import { TabsLabelProps } from '../../../components/common/TabsLabel/TabsLabel.interface';
import TestCaseIncidentTab from '../../../components/DataQuality/IncidentManager/TestCaseIncidentTab/TestCaseIncidentTab.component';
import TestCaseResultTab from '../../../components/DataQuality/IncidentManager/TestCaseResultTab/TestCaseResultTab.component';
import i18n from '../../../utils/i18next/LocalUtil';
import { IncidentManagerTabs } from '../IncidentManager.interface';
export interface TestCaseTabType {
LabelComponent: typeof TabsLabel;
labelProps: TabsLabelProps;
Tab: () => ReactElement;
key: IncidentManagerTabs;
}
class TestCaseClassBase {
showSqlQueryTab: boolean;
constructor() {
this.showSqlQueryTab = false;
}
public getTab(openTaskCount: number): TestCaseTabType[] {
return [
{
LabelComponent: TabsLabel,
labelProps: {
id: 'test-case-result',
name: i18n.t('label.test-case-result'),
},
Tab: TestCaseResultTab,
key: IncidentManagerTabs.TEST_CASE_RESULTS,
},
{
LabelComponent: TabsLabel,
labelProps: {
id: 'incident',
name: i18n.t('label.incident'),
count: openTaskCount,
},
Tab: TestCaseIncidentTab,
key: IncidentManagerTabs.ISSUES,
},
];
}
setShowSqlQueryTab(showSqlQueryTab: boolean) {
this.showSqlQueryTab = showSqlQueryTab;
}
public getFields(): string[] {
return [
'testSuite',
'testCaseResult',
'testDefinition',
'owner',
'incidentId',
];
}
}
const testCaseClassBase = new TestCaseClassBase();
export default testCaseClassBase;
export { TestCaseClassBase };

View File

@ -0,0 +1,35 @@
/*
* 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 { create } from 'zustand';
import { TestCase } from '../../../generated/tests/testCase';
export interface UseTestCaseStoreInterface {
testCase: TestCase | undefined;
isLoading: boolean;
setTestCase: (testCase: TestCase) => void;
setIsLoading: (isLoading: boolean) => void;
reset: () => void;
}
export const useTestCaseStore = create<UseTestCaseStoreInterface>()((set) => ({
testCase: undefined,
isLoading: true,
setTestCase: (testCase: TestCase) => {
set({ testCase });
},
setIsLoading: (isLoading: boolean) => {
set({ isLoading });
},
reset: () => {
set({ testCase: undefined, isLoading: true });
},
}));