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. *