Skip to content

Commit 2f368e6

Browse files
committed
feat(FR-1719): use async file deletion api to handle timeout error (#4691)
resolves #4688 ([FR-1719](https://lablup.atlassian.net/browse/FR-1719)) ### Implement Background File Deletion in File Explorer using Background API This PR enhances the file deletion functionality in the File Explorer by implementing background file deletion. When users delete files, especially large ones or directories, the operation now runs asynchronously in the background, providing a better user experience. Key changes: - Added support for the new `delete-files-async` API endpoint when available - Implemented visual indicators for files being deleted (loading state on delete buttons) - Added notification system to track background deletion progress - Added translations for deletion status messages across all supported languages - Made the confirmation input field disabled during deletion to prevent multiple submissions **Checklist:** - [x] Documentation - [x] Minium required manager version: 25.17.0 for background file deletion - [x] Specific setting for review: Test with large file/folder deletion - [x] Minimum requirements to check during review: Verify loading indicators appear and notifications show progress - [x] Test case(s): Delete a large folder and verify the UI remains responsive [FR-1719]: https://lablup.atlassian.net/browse/FR-1719?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
1 parent 8bf1f55 commit 2f368e6

File tree

29 files changed

+192
-17
lines changed

29 files changed

+192
-17
lines changed

packages/backend.ai-ui/src/components/BAIConfirmModalWithInput.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import BAIFlex from './BAIFlex';
22
import BAIModal, { BAIModalProps } from './BAIModal';
33
import { ExclamationCircleFilled } from '@ant-design/icons';
4-
import { Form, Input, Typography } from 'antd';
4+
import { Form, Input, InputProps, theme, Typography } from 'antd';
55
import React from 'react';
66
import { useTranslation } from 'react-i18next';
77

@@ -14,6 +14,7 @@ export interface BAIConfirmModalWithInputProps
1414
title: React.ReactNode;
1515
icon?: React.ReactNode;
1616
okButtonProps?: Omit<BAIModalProps['okButtonProps'], 'disabled' | 'danger'>;
17+
inputProps?: InputProps;
1718
}
1819

1920
const BAIConfirmModalWithInput: React.FC<BAIConfirmModalWithInputProps> = ({
@@ -23,9 +24,11 @@ const BAIConfirmModalWithInput: React.FC<BAIConfirmModalWithInputProps> = ({
2324
icon,
2425
onOk,
2526
onCancel,
26-
...props
27+
inputProps,
28+
...modalProps
2729
}) => {
2830
const { t } = useTranslation();
31+
const { token } = theme.useToken();
2932
const [form] = Form.useForm();
3033
const typedText = Form.useWatch('confirmText', form);
3134

@@ -37,7 +40,7 @@ const BAIConfirmModalWithInput: React.FC<BAIConfirmModalWithInputProps> = ({
3740
<Text strong>
3841
{icon ?? (
3942
<ExclamationCircleFilled
40-
style={{ color: '#faad14', marginRight: 5 }}
43+
style={{ color: token.colorWarning, marginRight: 5 }}
4144
/>
4245
)}
4346
{title}
@@ -52,9 +55,9 @@ const BAIConfirmModalWithInput: React.FC<BAIConfirmModalWithInputProps> = ({
5255
form.resetFields();
5356
onCancel?.(e);
5457
}}
55-
{...props}
58+
{...modalProps}
5659
okButtonProps={{
57-
...props.okButtonProps,
60+
...modalProps.okButtonProps,
5861
disabled: confirmText !== typedText,
5962
danger: true,
6063
}}
@@ -83,9 +86,11 @@ const BAIConfirmModalWithInput: React.FC<BAIConfirmModalWithInputProps> = ({
8386
autoFocus
8487
autoComplete="off"
8588
allowClear
89+
{...inputProps}
8690
onClick={(e) => {
8791
e.preventDefault();
8892
e.stopPropagation();
93+
inputProps?.onClick?.(e);
8994
}}
9095
/>
9196
</Form.Item>

packages/backend.ai-ui/src/components/baiClient/FileExplorer/BAIFileExplorer.tsx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,13 @@ export interface BAIFileExplorerProps {
5757
enableWrite?: boolean;
5858
onChangeFetchKey?: (fetchKey: string) => void;
5959
ref?: React.Ref<BAIFileExplorerRef>;
60+
onDeleteFilesInBackground?: (
61+
bgTaskId: string,
62+
targetVFolderId: string,
63+
deletingFilePaths: Array<string>,
64+
) => void;
65+
// FIXME: need to delete when `delete-file-async` API returns deleting file paths
66+
deletingFilePaths?: Array<string>;
6067
}
6168

6269
const BAIFileExplorer: React.FC<BAIFileExplorerProps> = ({
@@ -68,6 +75,8 @@ const BAIFileExplorer: React.FC<BAIFileExplorerProps> = ({
6875
enableDownload = false,
6976
enableDelete = false,
7077
enableWrite = false,
78+
onDeleteFilesInBackground,
79+
deletingFilePaths,
7180
style,
7281
ref,
7382
}) => {
@@ -187,7 +196,17 @@ const BAIFileExplorer: React.FC<BAIFileExplorerProps> = ({
187196
{
188197
title: t('comp:FileExplorer.Controls'),
189198
width: 80,
190-
render: (_, record) => {
199+
render: (_controls, record) => {
200+
// true if the file is being deleted or its parent directory is being deleted
201+
const isPendingDelete =
202+
_.includes(deletingFilePaths, `${currentPath}/${record.name}`) ||
203+
_.some(deletingFilePaths, (path) =>
204+
_.startsWith(
205+
currentPath,
206+
_.endsWith(path, '/') ? path : `${path}/`,
207+
),
208+
);
209+
191210
return (
192211
<Suspense fallback={<Skeleton.Button size="small" active />}>
193212
<FileItemControls
@@ -197,6 +216,7 @@ const BAIFileExplorer: React.FC<BAIFileExplorerProps> = ({
197216
}}
198217
enableDownload={enableDownload}
199218
enableDelete={enableDelete}
219+
deleteButtonProps={{ loading: isPendingDelete }}
200220
/>
201221
</Suspense>
202222
);
@@ -288,6 +308,7 @@ const BAIFileExplorer: React.FC<BAIFileExplorerProps> = ({
288308
enableDelete={enableDelete}
289309
enableWrite={enableWrite}
290310
onUpload={(files, currentPath) => onUpload(files, currentPath)}
311+
onDeleteFilesInBackground={onDeleteFilesInBackground}
291312
onRequestClose={(
292313
success: boolean,
293314
modifiedItems?: Array<VFolderFile>,
@@ -364,6 +385,7 @@ const BAIFileExplorer: React.FC<BAIFileExplorerProps> = ({
364385
<DeleteSelectedItemsModal
365386
open={!!selectedSingleItem}
366387
selectedFiles={selectedSingleItem ? [selectedSingleItem] : []}
388+
onDeleteFilesInBackground={onDeleteFilesInBackground}
367389
onRequestClose={(success: boolean) => {
368390
if (success) {
369391
setSelectedItems((prev) =>

packages/backend.ai-ui/src/components/baiClient/FileExplorer/DeleteSelectedItemsModal.tsx

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,19 @@ import _ from 'lodash';
99
import { use } from 'react';
1010
import { useTranslation } from 'react-i18next';
1111

12-
interface DeleteSelectedItemsModalProps extends ModalProps {
13-
onRequestClose: (success: boolean) => void;
12+
export interface DeleteSelectedItemsModalProps extends ModalProps {
13+
onRequestClose: (success: boolean, deletingFilePaths?: Array<string>) => void;
14+
onDeleteFilesInBackground?: (
15+
bgTaskId: string,
16+
targetVFolderId: string,
17+
deletingFilePaths: Array<string>,
18+
) => void;
1419
selectedFiles: Array<VFolderFile>;
1520
}
1621

1722
const DeleteSelectedItemsModal: React.FC<DeleteSelectedItemsModalProps> = ({
1823
onRequestClose,
24+
onDeleteFilesInBackground,
1925
selectedFiles,
2026
...modalProps
2127
}) => {
@@ -49,11 +55,19 @@ const DeleteSelectedItemsModal: React.FC<DeleteSelectedItemsModalProps> = ({
4955
recursive: true,
5056
name: targetVFolderId,
5157
})
52-
.then(() => {
53-
onRequestClose(true);
54-
message.success(
55-
t('comp:FileExplorer.SelectedItemsDeletedSuccessfully'),
56-
);
58+
.then(({ bgtask_id }: { bgtask_id: string }) => {
59+
onRequestClose(true, selectedFileNames);
60+
if (bgtask_id) {
61+
onDeleteFilesInBackground?.(
62+
bgtask_id,
63+
targetVFolderId,
64+
selectedFileNames,
65+
);
66+
} else {
67+
message.success(
68+
t('comp:FileExplorer.SelectedItemsDeletedSuccessfully'),
69+
);
70+
}
5771
})
5872
.catch((err) => {
5973
if (err && err.message) {
@@ -69,6 +83,7 @@ const DeleteSelectedItemsModal: React.FC<DeleteSelectedItemsModalProps> = ({
6983
title={t('comp:FileExplorer.DeleteSelectedItemsDialog')}
7084
okText={t('general.button.Delete')}
7185
okButtonProps={{ danger: true, loading: deleteFilesMutation.isPending }}
86+
inputProps={{ disabled: deleteFilesMutation.isPending }}
7287
onOk={handleDelete}
7388
onCancel={() => onRequestClose(false)}
7489
confirmText={

packages/backend.ai-ui/src/components/baiClient/FileExplorer/ExplorerActionControls.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import { BAITrashBinIcon } from '../../../icons';
22
import BAIFlex from '../../BAIFlex';
33
import { VFolderFile } from '../../provider/BAIClientProvider/types';
44
import CreateDirectoryModal from './CreateDirectoryModal';
5-
import DeleteSelectedItemsModal from './DeleteSelectedItemsModal';
5+
import DeleteSelectedItemsModal, {
6+
DeleteSelectedItemsModalProps,
7+
} from './DeleteSelectedItemsModal';
68
import { useUploadVFolderFiles } from './hooks';
79
import {
810
FileAddOutlined,
@@ -36,6 +38,7 @@ interface ExplorerActionControlsProps {
3638
modifiedItems?: Array<VFolderFile>,
3739
) => void;
3840
onUpload: (files: Array<RcFile>, currentPath: string) => void;
41+
onDeleteFilesInBackground: DeleteSelectedItemsModalProps['onDeleteFilesInBackground'];
3942
enableDelete?: boolean;
4043
enableWrite?: boolean;
4144
// onClickRefresh?: (key: string) => void;
@@ -46,6 +49,7 @@ const ExplorerActionControls: React.FC<ExplorerActionControlsProps> = ({
4649
selectedFiles,
4750
onRequestClose,
4851
onUpload,
52+
onDeleteFilesInBackground,
4953
enableDelete = false,
5054
enableWrite = false,
5155
extra,
@@ -149,6 +153,7 @@ const ExplorerActionControls: React.FC<ExplorerActionControlsProps> = ({
149153
destroyOnHidden
150154
open={openDeleteModal}
151155
selectedFiles={selectedFiles}
156+
onDeleteFilesInBackground={onDeleteFilesInBackground}
152157
onRequestClose={(success: boolean) => {
153158
if (success) {
154159
onRequestClose(true, selectedFiles);

packages/backend.ai-ui/src/components/baiClient/FileExplorer/FileItemControls.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { BAITrashBinIcon } from '../../../icons';
2+
import { BAIButtonProps } from '../../BAIButton';
23
import BAIFlex from '../../BAIFlex';
34
import useConnectedBAIClient from '../../provider/BAIClientProvider/hooks/useConnectedBAIClient';
45
import { VFolderFile } from '../../provider/BAIClientProvider/types';
@@ -14,13 +15,17 @@ interface FileItemControlsProps {
1415
onClickDelete: () => void;
1516
enableDownload?: boolean;
1617
enableDelete?: boolean;
18+
downloadButtonProps?: BAIButtonProps;
19+
deleteButtonProps?: BAIButtonProps;
1720
}
1821

1922
const FileItemControls: React.FC<FileItemControlsProps> = ({
2023
selectedItem,
2124
onClickDelete,
2225
enableDownload = false,
2326
enableDelete = false,
27+
downloadButtonProps,
28+
deleteButtonProps,
2429
}) => {
2530
const { t } = useTranslation();
2631
const { token } = theme.useToken();
@@ -90,6 +95,7 @@ const FileItemControls: React.FC<FileItemControlsProps> = ({
9095
e.stopPropagation();
9196
handleDownload();
9297
}}
98+
{...downloadButtonProps}
9399
/>
94100
<Button
95101
type="text"
@@ -100,6 +106,7 @@ const FileItemControls: React.FC<FileItemControlsProps> = ({
100106
e.stopPropagation();
101107
onClickDelete();
102108
}}
109+
{...deleteButtonProps}
103110
/>
104111
</BAIFlex>
105112
);

packages/backend.ai-ui/src/components/provider/BAIClientProvider/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export interface BAIClient {
4747
email: string;
4848
accessKey: string;
4949
_config: BackendAIConfig;
50+
supports: (feature: string) => boolean;
5051
}
5152

5253
export type BackendAIConfig = {

react/src/components/FolderExplorerModal.tsx

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useFileUploadManager } from './FileUploadManager';
22
import FolderExplorerHeader from './FolderExplorerHeader';
3+
import { useFolderExplorerOpener } from './FolderExplorerOpener';
34
import VFolderNodeDescription from './VFolderNodeDescription';
45
import { Alert, Divider, Grid, Skeleton, Splitter, theme } from 'antd';
56
import { createStyles } from 'antd-style';
@@ -8,13 +9,14 @@ import {
89
BAIFileExplorer,
910
BAIFileExplorerRef,
1011
BAIFlex,
12+
BAILink,
1113
BAIModal,
1214
BAIModalProps,
1315
toGlobalId,
1416
useInterval,
1517
} from 'backend.ai-ui';
1618
import _ from 'lodash';
17-
import { Suspense, useDeferredValue, useEffect, useRef } from 'react';
19+
import { Suspense, useDeferredValue, useEffect, useRef, useState } from 'react';
1820
import { useTranslation } from 'react-i18next';
1921
import { graphql, useLazyLoadQuery } from 'react-relay';
2022
import { FolderExplorerModalQuery } from 'src/__generated__/FolderExplorerModalQuery.graphql';
@@ -23,6 +25,7 @@ import {
2325
useFetchKey,
2426
useSuspendedBackendaiClient,
2527
} from 'src/hooks';
28+
import { useSetBAINotification } from 'src/hooks/useBAINotification';
2629
import { useCurrentProjectValue } from 'src/hooks/useCurrentProject';
2730
import { useMergedAllowedStorageHostPermission } from 'src/hooks/useMergedAllowedStorageHostPermission';
2831

@@ -94,6 +97,11 @@ const FolderExplorerModal: React.FC<FolderExplorerProps> = ({
9497
deferredOpen && modalProps.open ? 'network-only' : 'store-only',
9598
},
9699
);
100+
101+
// FIXME: This is a temporary workaround to notify file deletion to use WebUI Notification.
102+
const { upsertNotification, closeNotification } = useSetBAINotification();
103+
const { generateFolderPath } = useFolderExplorerOpener();
104+
const [deletingFilePaths, setDeletingFilePaths] = useState<Array<string>>([]);
97105
const { uploadStatus, uploadFiles } = useFileUploadManager(
98106
vfolder_node?.id,
99107
vfolder_node?.name || undefined,
@@ -136,10 +144,53 @@ const FolderExplorerModal: React.FC<FolderExplorerProps> = ({
136144
<BAIFileExplorer
137145
ref={fileExplorerRef}
138146
targetVFolderId={vfolderID}
147+
deletingFilePaths={deletingFilePaths}
139148
fetchKey={fetchKey}
140149
onUpload={(files: RcFile[], currentPath: string) => {
141150
uploadFiles(files, vfolderID, currentPath);
142151
}}
152+
onDeleteFilesInBackground={(
153+
bgTaskId,
154+
targetVFolderId,
155+
deletingFilePaths,
156+
) => {
157+
setDeletingFilePaths(deletingFilePaths);
158+
upsertNotification({
159+
key: `delete:${bgTaskId}`,
160+
open: true,
161+
message: (
162+
<span>
163+
{t('explorer.VFolder')}:&nbsp;
164+
<BAILink
165+
style={{
166+
fontWeight: 'normal',
167+
}}
168+
to={generateFolderPath(targetVFolderId)}
169+
onClick={() => {
170+
closeNotification(`delete:${bgTaskId}`);
171+
}}
172+
>{`${vfolder_node.name}`}</BAILink>
173+
</span>
174+
),
175+
backgroundTask: {
176+
status: 'pending',
177+
taskId: bgTaskId,
178+
promise: null,
179+
percent: 0,
180+
onChange: {
181+
pending: t('explorer.DeletingSelectedItems'),
182+
resolved: () => {
183+
setDeletingFilePaths([]);
184+
return t('explorer.SelectedItemsDeletedSuccessfully');
185+
},
186+
rejected: () => {
187+
setDeletingFilePaths([]);
188+
return t('explorer.SelectedItemsDeletionFailed');
189+
},
190+
},
191+
},
192+
});
193+
}}
143194
enableDownload={hasDownloadContentPermission}
144195
enableDelete={hasDeleteContentPermission}
145196
enableWrite={hasWriteContentPermission}

resources/i18n/de.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -659,6 +659,7 @@
659659
"Title": "Es ist ein Fehler aufgetreten."
660660
},
661661
"explorer": {
662+
"DeletingSelectedItems": "Die ausgewählten Dateien werden gelöscht. Bitte warten.",
662663
"FileUploadCancelled": "Das Datei -Upload wurde durch Benutzeranforderung storniert.",
663664
"FileUploadFailed": "Es wurden einige Dateien nicht in Ordner '{{folderName}}' hochgeladen.",
664665
"Filename": "Dateiname",
@@ -667,6 +668,8 @@
667668
"NoExplorerSupportForUnmanagedFolder": "Nicht verwaltete Ordner unterstützen den Datei -Explorer nicht.",
668669
"NoPermissions": "Keine Berechtigungen für den Zugriff auf diesen Ordner",
669670
"ProcessingUpload": "Ihre Datei wird hochgeladen. \nBitte warten.",
671+
"SelectedItemsDeletedSuccessfully": "Die ausgewählte Datei wurde erfolgreich gelöscht.",
672+
"SelectedItemsDeletionFailed": "Beim Löschen der Dateien ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.",
670673
"SuccessfullyUploadedToFolder": "Dateien erfolgreich hochgeladen.",
671674
"UploadFailed": "Upload fehlgeschlagen in '{{folderName}}' '",
672675
"UploadingFiles": "Dateien werden hochgeladen...",

resources/i18n/el.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,7 @@
656656
"Title": "Εμφανίστηκε σφάλμα."
657657
},
658658
"explorer": {
659+
"DeletingSelectedItems": "Διαγράφονται τα επιλεγμένα αρχεία. Παρακαλώ περιμένετε.",
659660
"FileUploadCancelled": "Η μεταφόρτωση αρχείων ακυρώθηκε με αίτημα χρήστη.",
660661
"FileUploadFailed": "Απέτυχε να ανεβάσει κάποια αρχεία στο φάκελο '{{folderName}}'.",
661662
"Filename": "Όνομα αρχείου",
@@ -664,6 +665,8 @@
664665
"NoExplorerSupportForUnmanagedFolder": "Οι μη διαχειριζόμενοι φάκελοι δεν υποστηρίζουν τον εξερευνητή αρχείων.",
665666
"NoPermissions": "Δεν υπάρχουν δικαιώματα πρόσβασης σε αυτόν τον φάκελο",
666667
"ProcessingUpload": "Το αρχείο σας μεταφορτώνεται. \nΠεριμένετε.",
668+
"SelectedItemsDeletedSuccessfully": "Το επιλεγμένο αρχείο διαγράφηκε με επιτυχία.",
669+
"SelectedItemsDeletionFailed": "Προέκυψε σφάλμα κατά τη διαγραφή των αρχείων. Παρακαλώ δοκιμάστε ξανά.",
667670
"SuccessfullyUploadedToFolder": "Τα αρχεία μεταφορτώθηκαν με επιτυχία.",
668671
"UploadFailed": "Η μεταφόρτωση απέτυχε στο '{{folderName}}'",
669672
"UploadingFiles": "Μεταφόρτωση αρχείων...",

0 commit comments

Comments
 (0)