diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/ApiServiceRest.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/ApiServiceRest.spec.ts
index c465aad711c..b83b72d866f 100644
--- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/ApiServiceRest.spec.ts
+++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/ApiServiceRest.spec.ts
@@ -97,13 +97,13 @@ test.describe('API service', () => {
await page.fill('[data-testid="confirmation-text-input"]', 'DELETE');
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 deleteResponse;
- await toastNotification(page, /deleted successfully!/);
+ await toastNotification(page, /Delete operation initiated for/);
});
});
diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/VersionPages/EntityVersionPages.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/VersionPages/EntityVersionPages.spec.ts
index e6cf4d18aaf..c03d756e1a1 100644
--- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/VersionPages/EntityVersionPages.spec.ts
+++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/VersionPages/EntityVersionPages.spec.ts
@@ -248,13 +248,13 @@ test.describe('Entity Version pages', () => {
await page.fill('[data-testid="confirmation-text-input"]', 'DELETE');
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 deleteResponse;
- await toastNotification(page, /deleted successfully!/);
+ await toastNotification(page, /Delete operation initiated for/);
await page.reload();
diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/VersionPages/ServiceEntityVersionPage.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/VersionPages/ServiceEntityVersionPage.spec.ts
index 662ed1df02b..a31e8ac4390 100644
--- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/VersionPages/ServiceEntityVersionPage.spec.ts
+++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/VersionPages/ServiceEntityVersionPage.spec.ts
@@ -208,13 +208,13 @@ test.describe('Service Version pages', () => {
await page.fill('[data-testid="confirmation-text-input"]', 'DELETE');
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 deleteResponse;
- await toastNotification(page, /deleted successfully!/);
+ await toastNotification(page, /Delete operation initiated for/);
await page.reload();
diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts
index 59eee880e84..332c4e6a9db 100644
--- a/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts
+++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts
@@ -1221,13 +1221,13 @@ export const softDeleteEntity = async (
await page.fill('[data-testid="confirmation-text-input"]', 'DELETE');
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 deleteResponse;
- await toastNotification(page, /deleted successfully!/);
+ await toastNotification(page, /Delete operation initiated for/);
await page.reload();
@@ -1287,12 +1287,12 @@ export const hardDeleteEntity = async (
await page.check('[data-testid="hard-delete"]');
await page.fill('[data-testid="confirmation-text-input"]', 'DELETE');
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 deleteResponse;
- await toastNotification(page, /deleted successfully!/);
+ await toastNotification(page, /Delete operation initiated for/);
};
export const checkDataAssetWidget = async (page: Page, serviceType: string) => {
diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/serviceIngestion.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/serviceIngestion.ts
index 56fe71a9f66..3140e0ea437 100644
--- a/openmetadata-ui/src/main/resources/ui/playwright/utils/serviceIngestion.ts
+++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/serviceIngestion.ts
@@ -111,7 +111,9 @@ export const deleteService = async (
response
.url()
.includes(
- `/api/v1/services/${getServiceCategoryFromService(typeOfService)}`
+ `/api/v1/services/${getServiceCategoryFromService(
+ typeOfService
+ )}s/async`
)
);
@@ -120,7 +122,10 @@ export const deleteService = async (
await deleteResponse;
// 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}"]`, {
state: 'hidden',
diff --git a/openmetadata-ui/src/main/resources/ui/src/App.tsx b/openmetadata-ui/src/main/resources/ui/src/App.tsx
index 95884d6edcb..7fd9474fe80 100644
--- a/openmetadata-ui/src/main/resources/ui/src/App.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/App.tsx
@@ -23,6 +23,7 @@ import { EntityExportModalProvider } from './components/Entity/EntityExportModal
import ApplicationsProvider from './components/Settings/Applications/ApplicationsProvider/ApplicationsProvider';
import WebAnalyticsProvider from './components/WebAnalytics/WebAnalyticsProvider';
import AntDConfigProvider from './context/AntDConfigProvider/AntDConfigProvider';
+import AsyncDeleteProvider from './context/AsyncDeleteProvider/AsyncDeleteProvider';
import PermissionProvider from './context/PermissionProvider/PermissionProvider';
import TourProvider from './context/TourProvider/TourProvider';
import WebSocketProvider from './context/WebSocketProvider/WebSocketProvider';
@@ -83,9 +84,11 @@ const App: FC = () => {
-
-
-
+
+
+
+
+
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DataAssetsHeader/DataAssetsHeader.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DataAssetsHeader/DataAssetsHeader.component.tsx
index 5bb80d68176..09d8d90381e 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DataAssetsHeader/DataAssetsHeader.component.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/DataAssetsHeader/DataAssetsHeader.component.tsx
@@ -536,6 +536,7 @@ export const DataAssetsHeader = ({
{
const { onUpdateCSVExportJob } = useEntityExportModalProvider();
+ const { handleDeleteEntityWebsocketResponse } = useAsyncDeleteProvider();
const { searchCriteria, updateSearchCriteria } = useApplicationStore();
const searchContainerRef = useRef(null);
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 () => {
socket && socket.off(SOCKET_EVENTS.TASK_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.BACKGROUND_JOB_CHANNEL);
+ socket && socket.off(SOCKET_EVENTS.DELETE_ENTITY_CHANNEL);
};
}, [socket, onUpdateCSVExportJob]);
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/PageLayoutV1/PageLayoutV1.tsx b/openmetadata-ui/src/main/resources/ui/src/components/PageLayoutV1/PageLayoutV1.tsx
index 3a89b1482cb..e50c5be5f7a 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/PageLayoutV1/PageLayoutV1.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/PageLayoutV1/PageLayoutV1.tsx
@@ -106,6 +106,7 @@ const PageLayoutV1: FC = ({
)}
+
{children}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/DeleteWidget/DeleteWidget.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/common/DeleteWidget/DeleteWidget.interface.ts
index 1e8f86b2e43..fdc5fd01d3a 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/common/DeleteWidget/DeleteWidget.interface.ts
+++ b/openmetadata-ui/src/main/resources/ui/src/components/common/DeleteWidget/DeleteWidget.interface.ts
@@ -32,6 +32,7 @@ export interface DeleteWidgetModalProps {
entityType: EntityType;
isAdminUser?: boolean;
entityId?: string;
+ isAsyncDelete?: boolean;
prepareType?: boolean;
isRecursiveDelete?: boolean;
successMessage?: string;
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/DeleteWidget/DeleteWidgetModal.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/DeleteWidget/DeleteWidgetModal.tsx
index ef68ca53171..dcf779fc736 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/common/DeleteWidget/DeleteWidgetModal.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/common/DeleteWidget/DeleteWidgetModal.tsx
@@ -31,6 +31,7 @@ import React, {
useState,
} from 'react';
import { useTranslation } from 'react-i18next';
+import { useAsyncDeleteProvider } from '../../../context/AsyncDeleteProvider/AsyncDeleteProvider';
import { EntityType } from '../../../enums/entity.enum';
import { useApplicationStore } from '../../../hooks/useApplicationStore';
import { deleteEntity } from '../../../rest/miscAPI';
@@ -57,6 +58,7 @@ const DeleteWidgetModal = ({
onCancel,
entityId,
prepareType = true,
+ isAsyncDelete = false,
isRecursiveDelete,
afterDeleteAction,
successMessage,
@@ -67,6 +69,7 @@ const DeleteWidgetModal = ({
const { t } = useTranslation();
const [form] = Form.useForm();
const { currentUser, onLogoutHandler } = useApplicationStore();
+ const { handleOnAsyncEntityDeleteConfirm } = useAsyncDeleteProvider();
const [deleteConfirmationText, setDeleteConfirmationText] =
useState('');
const [deletionType, setDeletionType] = useState(
@@ -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 value = e.target.value;
setDeletionType(value);
@@ -278,7 +311,7 @@ const DeleteWidgetModal = ({
open={visible}
title={`${t('label.delete')} ${entityType} "${entityName}"`}
onCancel={handleOnEntityDeleteCancel}>
- className="m-0" name="deleteType">
{(deleteOptions ?? DELETE_OPTION).map(
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/EntityPageInfos/ManageButton/ManageButton.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/common/EntityPageInfos/ManageButton/ManageButton.interface.ts
index 28222e946b1..72e9e7af139 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/common/EntityPageInfos/ManageButton/ManageButton.interface.ts
+++ b/openmetadata-ui/src/main/resources/ui/src/components/common/EntityPageInfos/ManageButton/ManageButton.interface.ts
@@ -29,6 +29,7 @@ export interface ManageButtonProps {
softDeleteMessagePostFix?: string;
hardDeleteMessagePostFix?: string;
canDelete?: boolean;
+ isAsyncDelete?: boolean;
extraDropdownContent?: ItemType[];
onAnnouncementClick?: () => void;
onRestoreEntity?: () => Promise;
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/EntityPageInfos/ManageButton/ManageButton.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/EntityPageInfos/ManageButton/ManageButton.tsx
index fe859d8fc1b..2c197414a77 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/common/EntityPageInfos/ManageButton/ManageButton.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/common/EntityPageInfos/ManageButton/ManageButton.tsx
@@ -47,6 +47,7 @@ const ManageButton: FC = ({
entityType,
canDelete,
entityId,
+ isAsyncDelete = false,
isRecursiveDelete,
extraDropdownContent,
onAnnouncementClick,
@@ -278,6 +279,7 @@ const ManageButton: FC = ({
entityName={displayName ?? entityName}
entityType={entityType}
hardDeleteMessagePostFix={hardDeleteMessagePostFix}
+ isAsyncDelete={isAsyncDelete}
isRecursiveDelete={isRecursiveDelete}
prepareType={prepareType}
softDeleteMessagePostFix={softDeleteMessagePostFix}
diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts
index 54914de1ed6..e3784942c67 100644
--- a/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts
+++ b/openmetadata-ui/src/main/resources/ui/src/constants/constants.ts
@@ -299,6 +299,7 @@ export const SOCKET_EVENTS = {
BULK_ASSETS_CHANNEL: 'bulkAssetsChannel',
CSV_IMPORT_CHANNEL: 'csvImportChannel',
BACKGROUND_JOB_CHANNEL: 'backgroundJobStatus',
+ DELETE_ENTITY_CHANNEL: 'deleteEntityChannel',
};
export const IN_PAGE_SEARCH_ROUTES: Record> = {
diff --git a/openmetadata-ui/src/main/resources/ui/src/context/AsyncDeleteProvider/AsyncDeleteProvider.interface.ts b/openmetadata-ui/src/main/resources/ui/src/context/AsyncDeleteProvider/AsyncDeleteProvider.interface.ts
new file mode 100644
index 00000000000..50092298a3a
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/context/AsyncDeleteProvider/AsyncDeleteProvider.interface.ts
@@ -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;
+ handleOnAsyncEntityDeleteConfirm: ({
+ deleteType,
+ }: DeleteWidgetAsyncFormFields) => Promise;
+ 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 &
+ AsyncDeleteResponse;
diff --git a/openmetadata-ui/src/main/resources/ui/src/context/AsyncDeleteProvider/AsyncDeleteProvider.test.tsx b/openmetadata-ui/src/main/resources/ui/src/context/AsyncDeleteProvider/AsyncDeleteProvider.test.tsx
new file mode 100644
index 00000000000..eaf31a9a98a
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/context/AsyncDeleteProvider/AsyncDeleteProvider.test.tsx
@@ -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 }) => (
+ {children}
+ );
+
+ 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
+ );
+ });
+});
diff --git a/openmetadata-ui/src/main/resources/ui/src/context/AsyncDeleteProvider/AsyncDeleteProvider.tsx b/openmetadata-ui/src/main/resources/ui/src/context/AsyncDeleteProvider/AsyncDeleteProvider.tsx
new file mode 100644
index 00000000000..fb058c3f379
--- /dev/null
+++ b/openmetadata-ui/src/main/resources/ui/src/context/AsyncDeleteProvider/AsyncDeleteProvider.tsx
@@ -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>();
+ const asyncDeleteJobRef = useRef>();
+
+ 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 = {
+ ...response,
+ ...asyncDeleteJob,
+ };
+ setAsyncDeleteJob(updatedAsyncDeleteJob);
+ asyncDeleteJobRef.current = updatedAsyncDeleteJob;
+ };
+
+ const activityFeedContextValues = useMemo(() => {
+ return {
+ asyncDeleteJob,
+ handleOnAsyncEntityDeleteConfirm,
+ handleDeleteEntityWebsocketResponse,
+ };
+ }, [handleOnAsyncEntityDeleteConfirm, handleDeleteEntityWebsocketResponse]);
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useAsyncDeleteProvider = () => useContext(AsyncDeleteContext);
+
+export default AsyncDeleteProvider;
diff --git a/openmetadata-ui/src/main/resources/ui/src/rest/miscAPI.ts b/openmetadata-ui/src/main/resources/ui/src/rest/miscAPI.ts
index 1786ccb8ac0..7b8171dc59a 100644
--- a/openmetadata-ui/src/main/resources/ui/src/rest/miscAPI.ts
+++ b/openmetadata-ui/src/main/resources/ui/src/rest/miscAPI.ts
@@ -15,6 +15,7 @@ import { AxiosResponse } from 'axios';
import { Edge } from '../components/Entity/EntityLineage/EntityLineage.interface';
import { ExploreSearchIndex } from '../components/Explore/ExplorePage.interface';
import { PAGE_SIZE } from '../constants/constants';
+import { AsyncDeleteJob } from '../context/AsyncDeleteProvider/AsyncDeleteProvider.interface';
import { SearchIndex } from '../enums/search.enum';
import { AuthenticationConfiguration } from '../generated/configuration/authenticationConfiguration';
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(
+ `/${entityType}/async/${entityId}`,
+ {
+ params,
+ }
+ );
+
+ return response.data;
+};
+
/**
* Retrieves the aggregate field options based on the provided parameters.
*