Skip to content

Commit e7540db

Browse files
committed
feat(FR-1296): Improve error message for deleting in-use vfolder (#4706)
resolves ([FR-1296](https://lablup.atlassian.net/browse/FR-1296)) ### Improve vfolder notification and error handling This PR adds support for virtual folder notifications in the UI, allowing users to see detailed information about folder operations and errors. Key changes: `BAIVirtualFolderNodeNotification` provides a common interface for notifications targeting vfolders. Currently, actions for the vfolder explorer are not included; only the message, description, progress, and extraDescription features are provided. - Created a new `BAIVirtualFolderNodeNotificationItem` component to display folder-specific notifications - Added a reusable `BAINotificationBackgroundItem` component to show progress indicators - Enhanced error handling in folder operations to provide more context - Added support for showing mounted sessions when a folder cannot be deleted - Improved the notification system to handle both string and React node content in `extraDescription` ![image.png](https://app.graphite.com/user-attachments/assets/e38a00cd-aef2-4f2c-a052-75b29adcd3d4.png) ![image.png](https://app.graphite.com/user-attachments/assets/3b9622e3-c218-4aaa-9d57-9004c7a22691.png) **Checklist:** - [x] Documentation - [ ] Minium required manager version - [ ] Specific setting for review (eg., KB link, endpoint or how to setup) - [ ] Minimum requirements to check during review - [ ] Test case(s) to demonstrate the difference of before/after [FR-1296]: https://lablup.atlassian.net/browse/FR-1296?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
1 parent e63258d commit e7540db

27 files changed

+287
-29
lines changed

react/src/components/BAIGeneralNotificationItem.tsx

Lines changed: 14 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { NotificationState } from '../hooks/useBAINotification';
2+
import BAINotificationBackgroundProgress from './BAINotificationBackgroundProgress';
23
import {
34
CheckCircleOutlined,
45
ClockCircleOutlined,
56
CloseCircleOutlined,
67
} from '@ant-design/icons';
7-
import { Button, Card, List, Progress, Typography, theme } from 'antd';
8+
import { Button, Card, List, Typography, theme } from 'antd';
89
import { BAIFlex } from 'backend.ai-ui';
910
import dayjs from 'dayjs';
1011
import _ from 'lodash';
@@ -117,36 +118,23 @@ const BAIGeneralNotificationItem: React.FC<{
117118
marginTop: token.marginSM,
118119
}}
119120
>
120-
<Typography.Text type="secondary" copyable>
121-
{notification.extraDescription}
122-
</Typography.Text>
121+
{_.isString(notification.extraDescription) ? (
122+
<Typography.Text type="secondary" copyable>
123+
{notification.extraDescription}
124+
</Typography.Text>
125+
) : (
126+
notification.extraDescription
127+
)}
123128
</Card>
124129
) : null}
125130

126131
<BAIFlex direction="row" align="center" justify="end" gap={'sm'}>
127-
{notification.backgroundTask &&
128-
_.isNumber(notification.backgroundTask.percent) ? (
129-
<Progress
130-
size="small"
131-
showInfo={false}
132-
percent={notification.backgroundTask.percent}
133-
strokeColor={
134-
notification.backgroundTask.status === 'rejected'
135-
? token.colorTextDisabled
136-
: undefined
137-
}
138-
style={{
139-
margin: 0,
140-
opacity:
141-
notification.backgroundTask.status === 'resolved' &&
142-
showDate
143-
? 0
144-
: 1,
145-
}}
146-
147-
// status={item.progressStatus}
132+
{notification.backgroundTask && (
133+
<BAINotificationBackgroundProgress
134+
backgroundTask={notification.backgroundTask}
135+
showDate={showDate}
148136
/>
149-
) : null}
137+
)}
150138
{showDate ? (
151139
<BAIFlex>
152140
<Typography.Text type="secondary">

react/src/components/BAINodeNotificationItem.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { NotificationState } from '../hooks/useBAINotification';
22
import BAIComputeSessionNodeNotificationItem from './BAIComputeSessionNodeNotificationItem';
3+
import BAIVirtualFolderNodeNotificationItem from './BAIVirtualFolderNodeNotificationItem';
34
import React from 'react';
45
import { graphql, useRefetchableFragment } from 'react-relay';
56
import { BAINodeNotificationItemFragment$key } from 'src/__generated__/BAINodeNotificationItemFragment.graphql';
@@ -18,6 +19,8 @@ const nodeFragmentOperation = graphql`
1819
... on VirtualFolderNode {
1920
__typename
2021
status
22+
...BAIVirtualFolderNodeNotificationItemFragment
23+
@alias(as: "virtualFolderNodeFrgmt")
2124
}
2225
}
2326
`;
@@ -39,6 +42,13 @@ const BAINodeNotificationItem: React.FC<{
3942
/>
4043
);
4144
} else if (node?.__typename === 'VirtualFolderNode') {
45+
return (
46+
<BAIVirtualFolderNodeNotificationItem
47+
notification={notification}
48+
virtualFolderNodeFrgmt={node.virtualFolderNodeFrgmt || null}
49+
showDate={showDate}
50+
/>
51+
);
4252
} else {
4353
// console.warn('Unknown node type in BAINodeNotificationItem:', node);
4454
return null;
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Progress, theme } from 'antd';
2+
import _ from 'lodash';
3+
import { NotificationState } from 'src/hooks/useBAINotification';
4+
5+
interface BAINotificationBackgroundProgressProps {
6+
backgroundTask: NotificationState['backgroundTask'];
7+
showDate?: boolean;
8+
}
9+
10+
const BAINotificationBackgroundProgress: React.FC<
11+
BAINotificationBackgroundProgressProps
12+
> = ({ backgroundTask, showDate }) => {
13+
'use memo';
14+
15+
const { token } = theme.useToken();
16+
17+
return _.isNumber(backgroundTask?.percent) ? (
18+
<Progress
19+
size="small"
20+
showInfo={false}
21+
percent={backgroundTask.percent}
22+
strokeColor={
23+
backgroundTask.status === 'rejected'
24+
? token.colorTextDisabled
25+
: undefined
26+
}
27+
style={{
28+
margin: 0,
29+
opacity: backgroundTask.status === 'resolved' && showDate ? 0 : 1,
30+
}}
31+
/>
32+
) : null;
33+
};
34+
35+
export default BAINotificationBackgroundProgress;
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import BAINotificationBackgroundProgress from './BAINotificationBackgroundProgress';
2+
import { useToggle } from 'ahooks';
3+
import { Card, List, theme, Typography } from 'antd';
4+
import { BAIFlex, BAILink, BAINotificationItem, BAIText } from 'backend.ai-ui';
5+
import dayjs from 'dayjs';
6+
import _ from 'lodash';
7+
import { useTranslation } from 'react-i18next';
8+
import { graphql, useFragment } from 'react-relay';
9+
import { useNavigate } from 'react-router-dom';
10+
import { BAIVirtualFolderNodeNotificationItemFragment$key } from 'src/__generated__/BAIVirtualFolderNodeNotificationItemFragment.graphql';
11+
import {
12+
NotificationState,
13+
useSetBAINotification,
14+
} from 'src/hooks/useBAINotification';
15+
16+
interface BAIVirtualFolderNodeNotificationItemProps {
17+
notification: NotificationState;
18+
virtualFolderNodeFrgmt: BAIVirtualFolderNodeNotificationItemFragment$key | null;
19+
showDate?: boolean;
20+
}
21+
22+
const BAIVirtualFolderNodeNotificationItem: React.FC<
23+
BAIVirtualFolderNodeNotificationItemProps
24+
> = ({ notification, virtualFolderNodeFrgmt, showDate }) => {
25+
'use memo';
26+
27+
const navigate = useNavigate();
28+
const { t } = useTranslation();
29+
const { token } = theme.useToken();
30+
const { closeNotification } = useSetBAINotification();
31+
const [showExtraDescription, { toggle: toggleShowExtraDescription }] =
32+
useToggle(false);
33+
34+
const node = useFragment(
35+
graphql`
36+
fragment BAIVirtualFolderNodeNotificationItemFragment on VirtualFolderNode {
37+
row_id
38+
id
39+
name
40+
status
41+
}
42+
`,
43+
virtualFolderNodeFrgmt,
44+
);
45+
46+
return (
47+
node && (
48+
<BAINotificationItem
49+
title={
50+
<BAIText ellipsis>
51+
{t('general.Folder')}:&nbsp;
52+
<BAILink
53+
style={{
54+
fontWeight: 'normal',
55+
}}
56+
title={node.name || ''}
57+
onClick={() => {
58+
navigate(
59+
`/data${node.row_id ? `?${new URLSearchParams({ folder: node.row_id }).toString()}` : ''}`,
60+
);
61+
closeNotification(notification.key);
62+
}}
63+
>
64+
{node.name}
65+
</BAILink>
66+
</BAIText>
67+
}
68+
description={
69+
<List.Item>
70+
<BAIFlex direction="column" align="stretch" gap={'xxs'}>
71+
<BAIFlex
72+
direction="row"
73+
align="end"
74+
gap={'xxs'}
75+
justify="between"
76+
>
77+
{_.isString(notification.description) ? (
78+
<BAIText>
79+
{_.truncate(notification.description, { length: 300 })}
80+
</BAIText>
81+
) : (
82+
notification.description
83+
)}
84+
85+
{notification.extraDescription && !notification?.onCancel ? (
86+
<BAIFlex>
87+
<Typography.Link
88+
onClick={() => {
89+
toggleShowExtraDescription();
90+
}}
91+
>
92+
{t('notification.SeeDetail')}
93+
</Typography.Link>
94+
</BAIFlex>
95+
) : null}
96+
</BAIFlex>
97+
98+
{notification.extraDescription && showExtraDescription ? (
99+
<Card
100+
size="small"
101+
style={{
102+
maxHeight: '300px',
103+
overflow: 'auto',
104+
overflowX: 'hidden',
105+
marginTop: token.marginSM,
106+
}}
107+
>
108+
{_.isString(notification.extraDescription) ? (
109+
<Typography.Text type="secondary" copyable>
110+
{notification.extraDescription}
111+
</Typography.Text>
112+
) : (
113+
notification.extraDescription
114+
)}
115+
</Card>
116+
) : null}
117+
118+
{notification.backgroundTask && (
119+
<BAINotificationBackgroundProgress
120+
backgroundTask={notification.backgroundTask}
121+
showDate={showDate}
122+
/>
123+
)}
124+
</BAIFlex>
125+
</List.Item>
126+
}
127+
footer={
128+
showDate ? dayjs(notification.created).format('lll') : undefined
129+
}
130+
/>
131+
)
132+
);
133+
};
134+
135+
export default BAIVirtualFolderNodeNotificationItem;

react/src/components/VFolderNodes.tsx

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {
22
VFolderNodesFragment$data,
33
VFolderNodesFragment$key,
44
} from '../__generated__/VFolderNodesFragment.graphql';
5-
import { useSuspendedBackendaiClient } from '../hooks';
5+
import { useSuspendedBackendaiClient, useWebUINavigate } from '../hooks';
66
import { useCurrentUserInfo } from '../hooks/backendai';
77
import { useTanMutation } from '../hooks/reactQueryAlias';
88
import { useSetBAINotification } from '../hooks/useBAINotification';
@@ -84,6 +84,7 @@ const VFolderNodes: React.FC<VFolderNodesProps> = ({
8484
const { upsertNotification } = useSetBAINotification();
8585
const { generateFolderPath } = useFolderExplorerOpener();
8686
const { getErrorMessage } = useErrorMessageResolver();
87+
const navigate = useWebUINavigate();
8788

8889
const [deletingVFolder, setDeletingVFolder] =
8990
useState<VFolderNodeInList | null>(null);
@@ -107,6 +108,7 @@ const VFolderNodes: React.FC<VFolderNodesProps> = ({
107108
...VFolderNodeIdenticonFragment
108109
...SharedFolderPermissionInfoModalFragment
109110
...BAIVFolderDeleteButtonFragment
111+
...BAINodeNotificationItemFragment
110112
}
111113
`,
112114
vfoldersFrgmt,
@@ -274,6 +276,8 @@ const VFolderNodes: React.FC<VFolderNodesProps> = ({
274276
},
275277
onError: (error) => {
276278
upsertNotification({
279+
key: `vfolder-error-${vfolder?.id}`,
280+
node: vfolder,
277281
description: getErrorMessage(error),
278282
open: true,
279283
});
@@ -298,9 +302,49 @@ const VFolderNodes: React.FC<VFolderNodesProps> = ({
298302
);
299303
},
300304
onError: (error) => {
305+
const matchString = error?.message.match(
306+
/sessions\(ids: (\[.*?\])\)/,
307+
)?.[1];
308+
const occupiedSession = JSON.parse(
309+
matchString?.replace(/'/g, '"') || '[]',
310+
);
301311
upsertNotification({
302-
description: getErrorMessage(error),
303312
open: true,
313+
key: `vfolder-error-${vfolder?.id}`,
314+
node: vfolder,
315+
description: getErrorMessage(error).replace(
316+
/\(ids[\s\S]*$/,
317+
'',
318+
),
319+
extraDescription: !_.isEmpty(occupiedSession) ? (
320+
<BAIFlex direction="column" align="stretch">
321+
<Typography.Text
322+
style={{
323+
color: token.colorTextDescription,
324+
}}
325+
>
326+
{t('data.folders.MountedSessions')}
327+
</Typography.Text>
328+
{_.map(occupiedSession, (sessionId) => (
329+
<BAILink
330+
key={sessionId}
331+
style={{
332+
fontWeight: 'normal',
333+
}}
334+
onClick={() => {
335+
navigate({
336+
pathname: '/session',
337+
search: new URLSearchParams({
338+
sessionDetail: sessionId,
339+
}).toString(),
340+
});
341+
}}
342+
>
343+
{sessionId}
344+
</BAILink>
345+
))}
346+
</BAIFlex>
347+
) : null,
304348
});
305349
},
306350
});
@@ -427,6 +471,8 @@ const VFolderNodes: React.FC<VFolderNodesProps> = ({
427471
},
428472
onError: (error) => {
429473
upsertNotification({
474+
key: `vfolder-error-${deletingVFolder?.id}`,
475+
...(deletingVFolder && { node: deletingVFolder }),
430476
description: getErrorMessage(error),
431477
open: true,
432478
});

react/src/hooks/useBAINotification.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export interface NotificationState
6464
renderDataMessage?: (message?: string) => React.ReactNode;
6565
promise?: Promise<unknown> | null;
6666
};
67-
extraDescription?: string | null;
67+
extraDescription?: ReactNode | null;
6868
onCancel?: (() => void) | null;
6969
skipDesktopNotification?: boolean;
7070
extraData: any;
@@ -279,6 +279,8 @@ export const useBAINotificationEffect = () => {
279279
* @returns An object containing functions for manipulating notifications.
280280
*/
281281
export const useSetBAINotification = () => {
282+
'use memo';
283+
282284
// Don't use _notifications carefully when you need to mutate it.
283285
const setNotifications = useSetAtom(notificationListState);
284286
const [desktopNotification] = useBAISettingUserState('desktop_notification');

resources/i18n/de.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,7 @@
402402
"MaxSize": "Maximale Größe",
403403
"ModifyPermissions": "Berechtigungen ändern",
404404
"MountPermission": "Erlaubnis montieren",
405+
"MountedSessions": "Gemountete Sitzungen",
405406
"MoveToTrash": "Ziehen Sie zu Müllbehälter",
406407
"MoveToTrashDescription": "Sind Sie sicher, dass Sie \"{{folderName}}\" in den Müll verschieben möchten?",
407408
"MoveToTrashMultipleDescription": "Sind Sie sicher, dass Sie {{folderLength}} Ordner in Müll bin verschieben möchten?",
@@ -693,6 +694,7 @@
693694
"Enabled": "aktiviert",
694695
"ErrorOccurred": "Etwas lief schief. \nBitte versuchen Sie es später erneut.",
695696
"ExtendLoginSession": "Eine Anmeldesitzung verlängern",
697+
"Folder": "Ordner",
696698
"Folders": "Ordner",
697699
"General": "Allgemein",
698700
"Image": "Bild",

0 commit comments

Comments
 (0)