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:
Ashish Gupta 2025-03-18 10:50:40 +05:30 committed by GitHub
parent 20086f71ee
commit b61c6d6269
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 428 additions and 17 deletions

View File

@ -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/);
}); });
}); });

View File

@ -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();

View File

@ -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();

View File

@ -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) => {

View File

@ -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',

View File

@ -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>

View File

@ -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}

View File

@ -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]);

View File

@ -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>

View File

@ -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;

View File

@ -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(

View File

@ -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>;

View File

@ -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}

View File

@ -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>> = {

View File

@ -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;

View File

@ -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
);
});
});

View File

@ -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;

View File

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