mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-08-25 17:37:57 +00:00
supported async delete functionality for service entities (#20275)
* supported async delete functionality for service entities * remove the multiple delete showing logic in ui and handle the recursive false case in case entity is not empty * added unit test for the provider * change playwright as per async delete * change the toast notification to success for the delete process being started * fix playwright test * fix playwright failure * fix the service ingestion playwright failure
This commit is contained in:
parent
20086f71ee
commit
b61c6d6269
@ -97,13 +97,13 @@ test.describe('API service', () => {
|
|||||||
await page.fill('[data-testid="confirmation-text-input"]', 'DELETE');
|
await page.fill('[data-testid="confirmation-text-input"]', 'DELETE');
|
||||||
|
|
||||||
const deleteResponse = page.waitForResponse(
|
const deleteResponse = page.waitForResponse(
|
||||||
'/api/v1/services/apiServices/*?hardDelete=true&recursive=true'
|
'/api/v1/services/apiServices/async/*?hardDelete=true&recursive=true'
|
||||||
);
|
);
|
||||||
|
|
||||||
await page.click('[data-testid="confirm-button"]');
|
await page.click('[data-testid="confirm-button"]');
|
||||||
|
|
||||||
await deleteResponse;
|
await deleteResponse;
|
||||||
|
|
||||||
await toastNotification(page, /deleted successfully!/);
|
await toastNotification(page, /Delete operation initiated for/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -248,13 +248,13 @@ test.describe('Entity Version pages', () => {
|
|||||||
|
|
||||||
await page.fill('[data-testid="confirmation-text-input"]', 'DELETE');
|
await page.fill('[data-testid="confirmation-text-input"]', 'DELETE');
|
||||||
const deleteResponse = page.waitForResponse(
|
const deleteResponse = page.waitForResponse(
|
||||||
`/api/v1/${entity.endpoint}/*?hardDelete=false&recursive=true`
|
`/api/v1/${entity.endpoint}/async/*?hardDelete=false&recursive=true`
|
||||||
);
|
);
|
||||||
await page.click('[data-testid="confirm-button"]');
|
await page.click('[data-testid="confirm-button"]');
|
||||||
|
|
||||||
await deleteResponse;
|
await deleteResponse;
|
||||||
|
|
||||||
await toastNotification(page, /deleted successfully!/);
|
await toastNotification(page, /Delete operation initiated for/);
|
||||||
|
|
||||||
await page.reload();
|
await page.reload();
|
||||||
|
|
||||||
|
@ -208,13 +208,13 @@ test.describe('Service Version pages', () => {
|
|||||||
|
|
||||||
await page.fill('[data-testid="confirmation-text-input"]', 'DELETE');
|
await page.fill('[data-testid="confirmation-text-input"]', 'DELETE');
|
||||||
const deleteResponse = page.waitForResponse(
|
const deleteResponse = page.waitForResponse(
|
||||||
`/api/v1/${entity.endpoint}/*?hardDelete=false&recursive=true`
|
`/api/v1/${entity.endpoint}/async/*?hardDelete=false&recursive=true`
|
||||||
);
|
);
|
||||||
await page.click('[data-testid="confirm-button"]');
|
await page.click('[data-testid="confirm-button"]');
|
||||||
|
|
||||||
await deleteResponse;
|
await deleteResponse;
|
||||||
|
|
||||||
await toastNotification(page, /deleted successfully!/);
|
await toastNotification(page, /Delete operation initiated for/);
|
||||||
|
|
||||||
await page.reload();
|
await page.reload();
|
||||||
|
|
||||||
|
@ -1221,13 +1221,13 @@ export const softDeleteEntity = async (
|
|||||||
|
|
||||||
await page.fill('[data-testid="confirmation-text-input"]', 'DELETE');
|
await page.fill('[data-testid="confirmation-text-input"]', 'DELETE');
|
||||||
const deleteResponse = page.waitForResponse(
|
const deleteResponse = page.waitForResponse(
|
||||||
`/api/v1/${endPoint}/*?hardDelete=false&recursive=true`
|
`/api/v1/${endPoint}/async/*?hardDelete=false&recursive=true`
|
||||||
);
|
);
|
||||||
await page.click('[data-testid="confirm-button"]');
|
await page.click('[data-testid="confirm-button"]');
|
||||||
|
|
||||||
await deleteResponse;
|
await deleteResponse;
|
||||||
|
|
||||||
await toastNotification(page, /deleted successfully!/);
|
await toastNotification(page, /Delete operation initiated for/);
|
||||||
|
|
||||||
await page.reload();
|
await page.reload();
|
||||||
|
|
||||||
@ -1287,12 +1287,12 @@ export const hardDeleteEntity = async (
|
|||||||
await page.check('[data-testid="hard-delete"]');
|
await page.check('[data-testid="hard-delete"]');
|
||||||
await page.fill('[data-testid="confirmation-text-input"]', 'DELETE');
|
await page.fill('[data-testid="confirmation-text-input"]', 'DELETE');
|
||||||
const deleteResponse = page.waitForResponse(
|
const deleteResponse = page.waitForResponse(
|
||||||
`/api/v1/${endPoint}/*?hardDelete=true&recursive=true`
|
`/api/v1/${endPoint}/async/*?hardDelete=true&recursive=true`
|
||||||
);
|
);
|
||||||
await page.click('[data-testid="confirm-button"]');
|
await page.click('[data-testid="confirm-button"]');
|
||||||
await deleteResponse;
|
await deleteResponse;
|
||||||
|
|
||||||
await toastNotification(page, /deleted successfully!/);
|
await toastNotification(page, /Delete operation initiated for/);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const checkDataAssetWidget = async (page: Page, serviceType: string) => {
|
export const checkDataAssetWidget = async (page: Page, serviceType: string) => {
|
||||||
|
@ -111,7 +111,9 @@ export const deleteService = async (
|
|||||||
response
|
response
|
||||||
.url()
|
.url()
|
||||||
.includes(
|
.includes(
|
||||||
`/api/v1/services/${getServiceCategoryFromService(typeOfService)}`
|
`/api/v1/services/${getServiceCategoryFromService(
|
||||||
|
typeOfService
|
||||||
|
)}s/async`
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -120,7 +122,10 @@ export const deleteService = async (
|
|||||||
await deleteResponse;
|
await deleteResponse;
|
||||||
|
|
||||||
// Closing the toast notification
|
// Closing the toast notification
|
||||||
await toastNotification(page, `"${serviceName}" deleted successfully!`);
|
await toastNotification(
|
||||||
|
page,
|
||||||
|
`Delete operation initiated for ${serviceName}`
|
||||||
|
);
|
||||||
|
|
||||||
await page.waitForSelector(`[data-testid="service-name-${serviceName}"]`, {
|
await page.waitForSelector(`[data-testid="service-name-${serviceName}"]`, {
|
||||||
state: 'hidden',
|
state: 'hidden',
|
||||||
|
@ -23,6 +23,7 @@ import { EntityExportModalProvider } from './components/Entity/EntityExportModal
|
|||||||
import ApplicationsProvider from './components/Settings/Applications/ApplicationsProvider/ApplicationsProvider';
|
import ApplicationsProvider from './components/Settings/Applications/ApplicationsProvider/ApplicationsProvider';
|
||||||
import WebAnalyticsProvider from './components/WebAnalytics/WebAnalyticsProvider';
|
import WebAnalyticsProvider from './components/WebAnalytics/WebAnalyticsProvider';
|
||||||
import AntDConfigProvider from './context/AntDConfigProvider/AntDConfigProvider';
|
import AntDConfigProvider from './context/AntDConfigProvider/AntDConfigProvider';
|
||||||
|
import AsyncDeleteProvider from './context/AsyncDeleteProvider/AsyncDeleteProvider';
|
||||||
import PermissionProvider from './context/PermissionProvider/PermissionProvider';
|
import PermissionProvider from './context/PermissionProvider/PermissionProvider';
|
||||||
import TourProvider from './context/TourProvider/TourProvider';
|
import TourProvider from './context/TourProvider/TourProvider';
|
||||||
import WebSocketProvider from './context/WebSocketProvider/WebSocketProvider';
|
import WebSocketProvider from './context/WebSocketProvider/WebSocketProvider';
|
||||||
@ -83,9 +84,11 @@ const App: FC = () => {
|
|||||||
<PermissionProvider>
|
<PermissionProvider>
|
||||||
<WebSocketProvider>
|
<WebSocketProvider>
|
||||||
<ApplicationsProvider>
|
<ApplicationsProvider>
|
||||||
|
<AsyncDeleteProvider>
|
||||||
<EntityExportModalProvider>
|
<EntityExportModalProvider>
|
||||||
<AppRouter />
|
<AppRouter />
|
||||||
</EntityExportModalProvider>
|
</EntityExportModalProvider>
|
||||||
|
</AsyncDeleteProvider>
|
||||||
</ApplicationsProvider>
|
</ApplicationsProvider>
|
||||||
</WebSocketProvider>
|
</WebSocketProvider>
|
||||||
</PermissionProvider>
|
</PermissionProvider>
|
||||||
|
@ -536,6 +536,7 @@ export const DataAssetsHeader = ({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<ManageButton
|
<ManageButton
|
||||||
|
isAsyncDelete
|
||||||
afterDeleteAction={afterDeleteAction}
|
afterDeleteAction={afterDeleteAction}
|
||||||
allowSoftDelete={!dataAsset.deleted && allowSoftDelete}
|
allowSoftDelete={!dataAsset.deleted && allowSoftDelete}
|
||||||
canDelete={permissions.Delete}
|
canDelete={permissions.Delete}
|
||||||
|
@ -55,6 +55,8 @@ import {
|
|||||||
} from '../../constants/constants';
|
} from '../../constants/constants';
|
||||||
import { GlobalSettingsMenuCategory } from '../../constants/GlobalSettings.constants';
|
import { GlobalSettingsMenuCategory } from '../../constants/GlobalSettings.constants';
|
||||||
import { HELP_ITEMS_ENUM } from '../../constants/Navbar.constants';
|
import { HELP_ITEMS_ENUM } from '../../constants/Navbar.constants';
|
||||||
|
import { useAsyncDeleteProvider } from '../../context/AsyncDeleteProvider/AsyncDeleteProvider';
|
||||||
|
import { AsyncDeleteWebsocketResponse } from '../../context/AsyncDeleteProvider/AsyncDeleteProvider.interface';
|
||||||
import { useWebSocketConnector } from '../../context/WebSocketProvider/WebSocketProvider';
|
import { useWebSocketConnector } from '../../context/WebSocketProvider/WebSocketProvider';
|
||||||
import { EntityTabs, EntityType } from '../../enums/entity.enum';
|
import { EntityTabs, EntityType } from '../../enums/entity.enum';
|
||||||
import { EntityReference } from '../../generated/entity/type';
|
import { EntityReference } from '../../generated/entity/type';
|
||||||
@ -119,6 +121,7 @@ const NavBar = ({
|
|||||||
handleClear,
|
handleClear,
|
||||||
}: NavBarProps) => {
|
}: NavBarProps) => {
|
||||||
const { onUpdateCSVExportJob } = useEntityExportModalProvider();
|
const { onUpdateCSVExportJob } = useEntityExportModalProvider();
|
||||||
|
const { handleDeleteEntityWebsocketResponse } = useAsyncDeleteProvider();
|
||||||
const { searchCriteria, updateSearchCriteria } = useApplicationStore();
|
const { searchCriteria, updateSearchCriteria } = useApplicationStore();
|
||||||
const searchContainerRef = useRef<HTMLDivElement>(null);
|
const searchContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const Logo = useMemo(() => brandClassBase.getMonogram().src, []);
|
const Logo = useMemo(() => brandClassBase.getMonogram().src, []);
|
||||||
@ -405,14 +408,23 @@ const NavBar = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on(SOCKET_EVENTS.DELETE_ENTITY_CHANNEL, (deleteResponse) => {
|
||||||
|
if (deleteResponse) {
|
||||||
|
const deleteResponseData = JSON.parse(
|
||||||
|
deleteResponse
|
||||||
|
) as AsyncDeleteWebsocketResponse;
|
||||||
|
handleDeleteEntityWebsocketResponse(deleteResponseData);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
socket && socket.off(SOCKET_EVENTS.TASK_CHANNEL);
|
socket && socket.off(SOCKET_EVENTS.TASK_CHANNEL);
|
||||||
socket && socket.off(SOCKET_EVENTS.MENTION_CHANNEL);
|
socket && socket.off(SOCKET_EVENTS.MENTION_CHANNEL);
|
||||||
socket && socket.off(SOCKET_EVENTS.CSV_EXPORT_CHANNEL);
|
socket && socket.off(SOCKET_EVENTS.CSV_EXPORT_CHANNEL);
|
||||||
socket && socket.off(SOCKET_EVENTS.CSV_EXPORT_CHANNEL);
|
|
||||||
socket && socket.off(SOCKET_EVENTS.BACKGROUND_JOB_CHANNEL);
|
socket && socket.off(SOCKET_EVENTS.BACKGROUND_JOB_CHANNEL);
|
||||||
|
socket && socket.off(SOCKET_EVENTS.DELETE_ENTITY_CHANNEL);
|
||||||
};
|
};
|
||||||
}, [socket, onUpdateCSVExportJob]);
|
}, [socket, onUpdateCSVExportJob]);
|
||||||
|
|
||||||
|
@ -106,6 +106,7 @@ const PageLayoutV1: FC<PageLayoutProp> = ({
|
|||||||
<AlertBar message={alert.message} type={alert.type} />
|
<AlertBar message={alert.message} type={alert.type} />
|
||||||
</Col>
|
</Col>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Col className={`${alert && 'p-t-sm'}`} span={24}>
|
<Col className={`${alert && 'p-t-sm'}`} span={24}>
|
||||||
{children}
|
{children}
|
||||||
</Col>
|
</Col>
|
||||||
|
@ -32,6 +32,7 @@ export interface DeleteWidgetModalProps {
|
|||||||
entityType: EntityType;
|
entityType: EntityType;
|
||||||
isAdminUser?: boolean;
|
isAdminUser?: boolean;
|
||||||
entityId?: string;
|
entityId?: string;
|
||||||
|
isAsyncDelete?: boolean;
|
||||||
prepareType?: boolean;
|
prepareType?: boolean;
|
||||||
isRecursiveDelete?: boolean;
|
isRecursiveDelete?: boolean;
|
||||||
successMessage?: string;
|
successMessage?: string;
|
||||||
|
@ -31,6 +31,7 @@ import React, {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useAsyncDeleteProvider } from '../../../context/AsyncDeleteProvider/AsyncDeleteProvider';
|
||||||
import { EntityType } from '../../../enums/entity.enum';
|
import { EntityType } from '../../../enums/entity.enum';
|
||||||
import { useApplicationStore } from '../../../hooks/useApplicationStore';
|
import { useApplicationStore } from '../../../hooks/useApplicationStore';
|
||||||
import { deleteEntity } from '../../../rest/miscAPI';
|
import { deleteEntity } from '../../../rest/miscAPI';
|
||||||
@ -57,6 +58,7 @@ const DeleteWidgetModal = ({
|
|||||||
onCancel,
|
onCancel,
|
||||||
entityId,
|
entityId,
|
||||||
prepareType = true,
|
prepareType = true,
|
||||||
|
isAsyncDelete = false,
|
||||||
isRecursiveDelete,
|
isRecursiveDelete,
|
||||||
afterDeleteAction,
|
afterDeleteAction,
|
||||||
successMessage,
|
successMessage,
|
||||||
@ -67,6 +69,7 @@ const DeleteWidgetModal = ({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const { currentUser, onLogoutHandler } = useApplicationStore();
|
const { currentUser, onLogoutHandler } = useApplicationStore();
|
||||||
|
const { handleOnAsyncEntityDeleteConfirm } = useAsyncDeleteProvider();
|
||||||
const [deleteConfirmationText, setDeleteConfirmationText] =
|
const [deleteConfirmationText, setDeleteConfirmationText] =
|
||||||
useState<string>('');
|
useState<string>('');
|
||||||
const [deletionType, setDeletionType] = useState<DeleteType>(
|
const [deletionType, setDeletionType] = useState<DeleteType>(
|
||||||
@ -192,6 +195,36 @@ const DeleteWidgetModal = ({
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onFormFinish = useCallback(
|
||||||
|
async (values: DeleteWidgetFormFields) => {
|
||||||
|
if (isAsyncDelete) {
|
||||||
|
setIsLoading(true);
|
||||||
|
await handleOnAsyncEntityDeleteConfirm({
|
||||||
|
entityName,
|
||||||
|
entityId: entityId ?? '',
|
||||||
|
entityType,
|
||||||
|
deleteType: values.deleteType,
|
||||||
|
prepareType,
|
||||||
|
isRecursiveDelete: isRecursiveDelete ?? false,
|
||||||
|
});
|
||||||
|
setIsLoading(false);
|
||||||
|
handleOnEntityDeleteCancel();
|
||||||
|
} else {
|
||||||
|
onDelete ? onDelete(values) : handleOnEntityDeleteConfirm(values);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
entityId,
|
||||||
|
onDelete,
|
||||||
|
entityName,
|
||||||
|
entityType,
|
||||||
|
prepareType,
|
||||||
|
isRecursiveDelete,
|
||||||
|
handleOnEntityDeleteConfirm,
|
||||||
|
handleOnEntityDeleteCancel,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
const onChange = useCallback((e: RadioChangeEvent) => {
|
const onChange = useCallback((e: RadioChangeEvent) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
setDeletionType(value);
|
setDeletionType(value);
|
||||||
@ -278,7 +311,7 @@ const DeleteWidgetModal = ({
|
|||||||
open={visible}
|
open={visible}
|
||||||
title={`${t('label.delete')} ${entityType} "${entityName}"`}
|
title={`${t('label.delete')} ${entityType} "${entityName}"`}
|
||||||
onCancel={handleOnEntityDeleteCancel}>
|
onCancel={handleOnEntityDeleteCancel}>
|
||||||
<Form form={form} onFinish={onDelete ?? handleOnEntityDeleteConfirm}>
|
<Form form={form} onFinish={onFormFinish}>
|
||||||
<Form.Item<DeleteWidgetFormFields> className="m-0" name="deleteType">
|
<Form.Item<DeleteWidgetFormFields> className="m-0" name="deleteType">
|
||||||
<Radio.Group onChange={onChange}>
|
<Radio.Group onChange={onChange}>
|
||||||
{(deleteOptions ?? DELETE_OPTION).map(
|
{(deleteOptions ?? DELETE_OPTION).map(
|
||||||
|
@ -29,6 +29,7 @@ export interface ManageButtonProps {
|
|||||||
softDeleteMessagePostFix?: string;
|
softDeleteMessagePostFix?: string;
|
||||||
hardDeleteMessagePostFix?: string;
|
hardDeleteMessagePostFix?: string;
|
||||||
canDelete?: boolean;
|
canDelete?: boolean;
|
||||||
|
isAsyncDelete?: boolean;
|
||||||
extraDropdownContent?: ItemType[];
|
extraDropdownContent?: ItemType[];
|
||||||
onAnnouncementClick?: () => void;
|
onAnnouncementClick?: () => void;
|
||||||
onRestoreEntity?: () => Promise<void>;
|
onRestoreEntity?: () => Promise<void>;
|
||||||
|
@ -47,6 +47,7 @@ const ManageButton: FC<ManageButtonProps> = ({
|
|||||||
entityType,
|
entityType,
|
||||||
canDelete,
|
canDelete,
|
||||||
entityId,
|
entityId,
|
||||||
|
isAsyncDelete = false,
|
||||||
isRecursiveDelete,
|
isRecursiveDelete,
|
||||||
extraDropdownContent,
|
extraDropdownContent,
|
||||||
onAnnouncementClick,
|
onAnnouncementClick,
|
||||||
@ -278,6 +279,7 @@ const ManageButton: FC<ManageButtonProps> = ({
|
|||||||
entityName={displayName ?? entityName}
|
entityName={displayName ?? entityName}
|
||||||
entityType={entityType}
|
entityType={entityType}
|
||||||
hardDeleteMessagePostFix={hardDeleteMessagePostFix}
|
hardDeleteMessagePostFix={hardDeleteMessagePostFix}
|
||||||
|
isAsyncDelete={isAsyncDelete}
|
||||||
isRecursiveDelete={isRecursiveDelete}
|
isRecursiveDelete={isRecursiveDelete}
|
||||||
prepareType={prepareType}
|
prepareType={prepareType}
|
||||||
softDeleteMessagePostFix={softDeleteMessagePostFix}
|
softDeleteMessagePostFix={softDeleteMessagePostFix}
|
||||||
|
@ -299,6 +299,7 @@ export const SOCKET_EVENTS = {
|
|||||||
BULK_ASSETS_CHANNEL: 'bulkAssetsChannel',
|
BULK_ASSETS_CHANNEL: 'bulkAssetsChannel',
|
||||||
CSV_IMPORT_CHANNEL: 'csvImportChannel',
|
CSV_IMPORT_CHANNEL: 'csvImportChannel',
|
||||||
BACKGROUND_JOB_CHANNEL: 'backgroundJobStatus',
|
BACKGROUND_JOB_CHANNEL: 'backgroundJobStatus',
|
||||||
|
DELETE_ENTITY_CHANNEL: 'deleteEntityChannel',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IN_PAGE_SEARCH_ROUTES: Record<string, Array<string>> = {
|
export const IN_PAGE_SEARCH_ROUTES: Record<string, Array<string>> = {
|
||||||
|
@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 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 { ReactNode } from 'react';
|
||||||
|
import { DeleteType } from '../../components/common/DeleteWidget/DeleteWidget.interface';
|
||||||
|
|
||||||
|
export interface AsyncDeleteProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeleteWidgetAsyncFormFields {
|
||||||
|
entityName: string;
|
||||||
|
entityId: string;
|
||||||
|
entityType: string;
|
||||||
|
deleteType: DeleteType;
|
||||||
|
prepareType: boolean;
|
||||||
|
isRecursiveDelete: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AsyncDeleteContextType {
|
||||||
|
asyncDeleteJob?: Partial<AsyncDeleteJob>;
|
||||||
|
handleOnAsyncEntityDeleteConfirm: ({
|
||||||
|
deleteType,
|
||||||
|
}: DeleteWidgetAsyncFormFields) => Promise<void>;
|
||||||
|
handleDeleteEntityWebsocketResponse: (
|
||||||
|
response: AsyncDeleteWebsocketResponse
|
||||||
|
) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AsyncDeleteResponse = {
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AsyncDeleteWebsocketResponse = {
|
||||||
|
jobId: string;
|
||||||
|
status: 'COMPLETED' | 'FAILED';
|
||||||
|
entityName: string;
|
||||||
|
error: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AsyncDeleteJob = {
|
||||||
|
hardDelete: boolean;
|
||||||
|
recursive: boolean;
|
||||||
|
} & Partial<AsyncDeleteWebsocketResponse> &
|
||||||
|
AsyncDeleteResponse;
|
@ -0,0 +1,161 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 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 { act } from '@testing-library/react';
|
||||||
|
import { renderHook } from '@testing-library/react-hooks';
|
||||||
|
import React from 'react';
|
||||||
|
import { DeleteType } from '../../components/common/DeleteWidget/DeleteWidget.interface';
|
||||||
|
import { deleteAsyncEntity } from '../../rest/miscAPI';
|
||||||
|
import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils';
|
||||||
|
import AsyncDeleteProvider, {
|
||||||
|
useAsyncDeleteProvider,
|
||||||
|
} from './AsyncDeleteProvider';
|
||||||
|
import { AsyncDeleteWebsocketResponse } from './AsyncDeleteProvider.interface';
|
||||||
|
|
||||||
|
jest.mock('../../utils/ToastUtils', () => ({
|
||||||
|
showErrorToast: jest.fn(),
|
||||||
|
showSuccessToast: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../../rest/miscAPI', () => ({
|
||||||
|
deleteAsyncEntity: jest.fn().mockImplementation(() => Promise.resolve()),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('AsyncDeleteProvider', () => {
|
||||||
|
const mockResponse = {
|
||||||
|
entityName: 'DELETE',
|
||||||
|
hardDelete: false,
|
||||||
|
jobId: 'efc87367-01bd-4f9d-8d78-fea93bcb412f',
|
||||||
|
message: 'Delete operation initiated for DELETE',
|
||||||
|
recursive: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockDeleteParams = {
|
||||||
|
entityName: 'TestEntity',
|
||||||
|
entityId: 'test-id',
|
||||||
|
entityType: 'test-type',
|
||||||
|
deleteType: DeleteType.SOFT_DELETE,
|
||||||
|
prepareType: false,
|
||||||
|
isRecursiveDelete: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||||
|
<AsyncDeleteProvider>{children}</AsyncDeleteProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
it('should initialize with empty asyncDeleteJob', () => {
|
||||||
|
const { result } = renderHook(() => useAsyncDeleteProvider(), { wrapper });
|
||||||
|
|
||||||
|
expect(result.current.asyncDeleteJob).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle successful entity deletion', async () => {
|
||||||
|
(deleteAsyncEntity as jest.Mock).mockResolvedValueOnce(mockResponse);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useAsyncDeleteProvider(), { wrapper });
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.handleOnAsyncEntityDeleteConfirm(mockDeleteParams);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(deleteAsyncEntity).toHaveBeenCalledWith(
|
||||||
|
'test-type',
|
||||||
|
'test-id',
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
expect(showSuccessToast).toHaveBeenCalledWith(mockResponse.message);
|
||||||
|
expect(result.current.asyncDeleteJob).toEqual(mockResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle failed entity deletion', async () => {
|
||||||
|
const mockError = new Error('Delete failed');
|
||||||
|
(deleteAsyncEntity as jest.Mock).mockRejectedValueOnce(mockError);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useAsyncDeleteProvider(), { wrapper });
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.handleOnAsyncEntityDeleteConfirm(mockDeleteParams);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(showErrorToast).toHaveBeenCalledWith(
|
||||||
|
mockError,
|
||||||
|
'server.delete-entity-error'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle websocket response', async () => {
|
||||||
|
const mockWebsocketResponse: AsyncDeleteWebsocketResponse = {
|
||||||
|
status: 'COMPLETED',
|
||||||
|
jobId: '123',
|
||||||
|
entityName: 'TestEntity',
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useAsyncDeleteProvider(), { wrapper });
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.handleDeleteEntityWebsocketResponse(mockWebsocketResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.asyncDeleteJob).toEqual(
|
||||||
|
expect.objectContaining(mockWebsocketResponse)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle failed status from ref', async () => {
|
||||||
|
const mockFailedResponse: AsyncDeleteWebsocketResponse = {
|
||||||
|
status: 'FAILED',
|
||||||
|
error: 'Delete operation failed',
|
||||||
|
jobId: '123',
|
||||||
|
entityName: 'TestEntity',
|
||||||
|
};
|
||||||
|
|
||||||
|
(deleteAsyncEntity as jest.Mock).mockResolvedValueOnce(mockFailedResponse);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useAsyncDeleteProvider(), { wrapper });
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
result.current.handleDeleteEntityWebsocketResponse(mockFailedResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.handleOnAsyncEntityDeleteConfirm(mockDeleteParams);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(showErrorToast).toHaveBeenCalledWith('Delete operation failed');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle prepared entity type', async () => {
|
||||||
|
(deleteAsyncEntity as jest.Mock).mockResolvedValueOnce(mockResponse);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useAsyncDeleteProvider(), { wrapper });
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.handleOnAsyncEntityDeleteConfirm({
|
||||||
|
...mockDeleteParams,
|
||||||
|
prepareType: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(deleteAsyncEntity).toHaveBeenCalledWith(
|
||||||
|
expect.any(String),
|
||||||
|
'test-id',
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,114 @@
|
|||||||
|
/*
|
||||||
|
* 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 { AxiosError } from 'axios';
|
||||||
|
import React, {
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { DeleteType } from '../../components/common/DeleteWidget/DeleteWidget.interface';
|
||||||
|
import { deleteAsyncEntity } from '../../rest/miscAPI';
|
||||||
|
import deleteWidgetClassBase from '../../utils/DeleteWidget/DeleteWidgetClassBase';
|
||||||
|
import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils';
|
||||||
|
import {
|
||||||
|
AsyncDeleteContextType,
|
||||||
|
AsyncDeleteJob,
|
||||||
|
AsyncDeleteProviderProps,
|
||||||
|
AsyncDeleteWebsocketResponse,
|
||||||
|
DeleteWidgetAsyncFormFields,
|
||||||
|
} from './AsyncDeleteProvider.interface';
|
||||||
|
|
||||||
|
export const AsyncDeleteContext = createContext({} as AsyncDeleteContextType);
|
||||||
|
|
||||||
|
const AsyncDeleteProvider = ({ children }: AsyncDeleteProviderProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [asyncDeleteJob, setAsyncDeleteJob] =
|
||||||
|
useState<Partial<AsyncDeleteJob>>();
|
||||||
|
const asyncDeleteJobRef = useRef<Partial<AsyncDeleteJob>>();
|
||||||
|
|
||||||
|
const handleOnAsyncEntityDeleteConfirm = async ({
|
||||||
|
entityName,
|
||||||
|
entityId,
|
||||||
|
entityType,
|
||||||
|
deleteType,
|
||||||
|
prepareType,
|
||||||
|
isRecursiveDelete,
|
||||||
|
}: DeleteWidgetAsyncFormFields) => {
|
||||||
|
try {
|
||||||
|
const response = await deleteAsyncEntity(
|
||||||
|
prepareType
|
||||||
|
? deleteWidgetClassBase.prepareEntityType(entityType)
|
||||||
|
: entityType,
|
||||||
|
entityId ?? '',
|
||||||
|
Boolean(isRecursiveDelete),
|
||||||
|
deleteType === DeleteType.HARD_DELETE
|
||||||
|
);
|
||||||
|
|
||||||
|
// In case of recursive delete if false and entity has data.
|
||||||
|
// sometime socket throw the error before the response
|
||||||
|
if (asyncDeleteJobRef.current?.status === 'FAILED') {
|
||||||
|
showErrorToast(
|
||||||
|
asyncDeleteJobRef.current.error ??
|
||||||
|
t('server.delete-entity-error', {
|
||||||
|
entity: entityName,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setAsyncDeleteJob(response);
|
||||||
|
asyncDeleteJobRef.current = response;
|
||||||
|
showSuccessToast(response.message);
|
||||||
|
} catch (error) {
|
||||||
|
showErrorToast(
|
||||||
|
error as AxiosError,
|
||||||
|
t('server.delete-entity-error', {
|
||||||
|
entity: entityName,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteEntityWebsocketResponse = (
|
||||||
|
response: AsyncDeleteWebsocketResponse
|
||||||
|
) => {
|
||||||
|
const updatedAsyncDeleteJob: Partial<AsyncDeleteJob> = {
|
||||||
|
...response,
|
||||||
|
...asyncDeleteJob,
|
||||||
|
};
|
||||||
|
setAsyncDeleteJob(updatedAsyncDeleteJob);
|
||||||
|
asyncDeleteJobRef.current = updatedAsyncDeleteJob;
|
||||||
|
};
|
||||||
|
|
||||||
|
const activityFeedContextValues = useMemo(() => {
|
||||||
|
return {
|
||||||
|
asyncDeleteJob,
|
||||||
|
handleOnAsyncEntityDeleteConfirm,
|
||||||
|
handleDeleteEntityWebsocketResponse,
|
||||||
|
};
|
||||||
|
}, [handleOnAsyncEntityDeleteConfirm, handleDeleteEntityWebsocketResponse]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AsyncDeleteContext.Provider value={activityFeedContextValues}>
|
||||||
|
{children}
|
||||||
|
</AsyncDeleteContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAsyncDeleteProvider = () => useContext(AsyncDeleteContext);
|
||||||
|
|
||||||
|
export default AsyncDeleteProvider;
|
@ -15,6 +15,7 @@ import { AxiosResponse } from 'axios';
|
|||||||
import { Edge } from '../components/Entity/EntityLineage/EntityLineage.interface';
|
import { Edge } from '../components/Entity/EntityLineage/EntityLineage.interface';
|
||||||
import { ExploreSearchIndex } from '../components/Explore/ExplorePage.interface';
|
import { ExploreSearchIndex } from '../components/Explore/ExplorePage.interface';
|
||||||
import { PAGE_SIZE } from '../constants/constants';
|
import { PAGE_SIZE } from '../constants/constants';
|
||||||
|
import { AsyncDeleteJob } from '../context/AsyncDeleteProvider/AsyncDeleteProvider.interface';
|
||||||
import { SearchIndex } from '../enums/search.enum';
|
import { SearchIndex } from '../enums/search.enum';
|
||||||
import { AuthenticationConfiguration } from '../generated/configuration/authenticationConfiguration';
|
import { AuthenticationConfiguration } from '../generated/configuration/authenticationConfiguration';
|
||||||
import { AuthorizerConfiguration } from '../generated/configuration/authorizerConfiguration';
|
import { AuthorizerConfiguration } from '../generated/configuration/authorizerConfiguration';
|
||||||
@ -130,6 +131,27 @@ export const deleteEntity = async (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const deleteAsyncEntity = async (
|
||||||
|
entityType: string,
|
||||||
|
entityId: string,
|
||||||
|
isRecursive: boolean,
|
||||||
|
isHardDelete = true
|
||||||
|
) => {
|
||||||
|
const params = {
|
||||||
|
hardDelete: isHardDelete,
|
||||||
|
recursive: isRecursive,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await APIClient.delete<AsyncDeleteJob>(
|
||||||
|
`/${entityType}/async/${entityId}`,
|
||||||
|
{
|
||||||
|
params,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the aggregate field options based on the provided parameters.
|
* Retrieves the aggregate field options based on the provided parameters.
|
||||||
*
|
*
|
||||||
|
Loading…
x
Reference in New Issue
Block a user