Skip to content

Commit 7d7a301

Browse files
committed
feat(FR-2573): migrate VFolder list queries to Strawberry V2 GraphQL API
Migrate AdminVFolderNodeListPage and VFolderNodeListPage, plus VFolderNodesV2 and its child components, from the legacy VirtualFolderNode query path to the V2 Strawberry VFolder type. AdminVFolderNodeListPage uses adminVfoldersV2, VFolderNodeListPage uses myVfolders (user-facing: folders accessible to the current user). Shared modal and button fragments (DeleteVFolderModalV2, DeleteForeverVFolderModalV2, RestoreVFolderModalV2, BAIVFolderDeleteButton) are also switched to V2 fields (metadata, accessControl, ownership). URL state now uses nuqs with structured VFolderFilter/VFolderOrderBy inputs composed via top-level AND, and convertToOrderBy for sort. isDeletedCategory accepts both V2 UPPERCASE and legacy kebab-case statuses for backwards compatibility with the remaining legacy call sites.
1 parent 16140ba commit 7d7a301

12 files changed

Lines changed: 854 additions & 496 deletions
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { BAIVFolderDeleteButtonV2Fragment$key } from '../../__generated__/BAIVFolderDeleteButtonV2Fragment.graphql';
2+
import { BAITrashBinIcon } from '../../icons';
3+
import BAIButton from '../BAIButton';
4+
import { theme, type ButtonProps } from 'antd';
5+
import * as _ from 'lodash-es';
6+
import { graphql, useFragment } from 'react-relay';
7+
8+
export interface BAIVFolderDeleteButtonV2Props extends ButtonProps {
9+
vfolderFrgmt: BAIVFolderDeleteButtonV2Fragment$key;
10+
}
11+
12+
const BAIVFolderDeleteButtonV2 = ({
13+
vfolderFrgmt,
14+
...buttonProps
15+
}: BAIVFolderDeleteButtonV2Props) => {
16+
const { token } = theme.useToken();
17+
useFragment<BAIVFolderDeleteButtonV2Fragment$key>(
18+
graphql`
19+
fragment BAIVFolderDeleteButtonV2Fragment on VFolder
20+
@relay(plural: true) {
21+
id
22+
}
23+
`,
24+
vfolderFrgmt,
25+
);
26+
27+
// TODO(needs-backend): V2 `VFolder` does not expose a per-user action
28+
// permission (legacy `VirtualFolderNode.permissions` had `delete_vfolder`).
29+
// `accessControl.permission` is a `VFolderMountPermission` enum (RO/RW/
30+
// RW_DELETE) describing the mount level, not a user's entity-level action
31+
// permission on the folder, so it cannot be used as a gate for delete.
32+
// Until a proper permission field is exposed, always treat the button as
33+
// enabled and let the backend reject unauthorized requests.
34+
const isEnabled = !buttonProps.disabled;
35+
36+
return (
37+
<BAIButton
38+
icon={<BAITrashBinIcon />}
39+
disabled={buttonProps.disabled}
40+
style={{
41+
color: isEnabled ? token.colorError : token.colorTextDisabled,
42+
backgroundColor: isEnabled
43+
? token.colorErrorBg
44+
: token.colorBgContainerDisabled,
45+
...buttonProps.style,
46+
}}
47+
{..._.omit(buttonProps, ['style', 'disabled'])}
48+
/>
49+
);
50+
};
51+
52+
export default BAIVFolderDeleteButtonV2;

packages/backend.ai-ui/src/components/fragments/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export type {
5454
BAIActivateArtifactsModalArtifactsFragmentKey,
5555
} from './BAIActivateArtifactsModal';
5656
export { default as BAIVFolderDeleteButton } from './BAIVFolderDeleteButton';
57+
export { default as BAIVFolderDeleteButtonV2 } from './BAIVFolderDeleteButtonV2';
5758
export { default as BAIAdminResourceGroupSelect } from './BAIAdminResourceGroupSelect';
5859
export type { BAIAdminResourceGroupSelectProps } from './BAIAdminResourceGroupSelect';
5960
export {

react/src/components/DeleteForeverVFolderModalV2.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,12 @@ const DeleteForeverVFolderModalV2: React.FC<
3636

3737
const vfolders = useFragment(
3838
graphql`
39-
fragment DeleteForeverVFolderModalV2Fragment on VirtualFolderNode
39+
fragment DeleteForeverVFolderModalV2Fragment on VFolder
4040
@relay(plural: true) {
4141
id
42-
name
42+
metadata {
43+
name
44+
}
4345
}
4446
`,
4547
vfolderFrgmts,
@@ -80,7 +82,7 @@ const DeleteForeverVFolderModalV2: React.FC<
8082
<Typography.Text>
8183
{purgeable.length === 1
8284
? t('data.folders.DeleteForeverDescription', {
83-
folderName: purgeable[0]?.name,
85+
folderName: purgeable[0]?.metadata?.name,
8486
})
8587
: t('data.folders.DeleteForeverMultipleDescription', {
8688
folderLength: purgeable.length,
@@ -112,15 +114,17 @@ const DeleteForeverVFolderModalV2: React.FC<
112114
if (purgedCount === 0) {
113115
message.error(
114116
t('data.folders.FailedToDeleteFolders', {
115-
folderNames: _.map(purgeable, 'name').join(', '),
117+
folderNames: _.map(purgeable, (v) => v?.metadata?.name).join(
118+
', ',
119+
),
116120
}),
117121
);
118122
return;
119123
}
120124
if (purgeable.length === 1) {
121125
message.success(
122126
t('data.folders.FolderDeletedForever', {
123-
folderName: purgeable[0]?.name,
127+
folderName: purgeable[0]?.metadata?.name,
124128
}),
125129
);
126130
} else {

react/src/components/DeleteVFolderModalV2.tsx

Lines changed: 23 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@
44
*/
55
import { DeleteVFolderModalV2Fragment$key } from '../__generated__/DeleteVFolderModalV2Fragment.graphql';
66
import { DeleteVFolderModalV2Mutation } from '../__generated__/DeleteVFolderModalV2Mutation.graphql';
7-
import { App, Typography, theme } from 'antd';
7+
import { App, Typography } from 'antd';
88
import {
9-
BAIAlert,
109
BAIFlex,
1110
BAIModal,
1211
BAIModalProps,
@@ -32,15 +31,14 @@ const DeleteVFolderModalV2: React.FC<DeleteVFolderModalV2Props> = ({
3231
const { t } = useTranslation();
3332
const { message } = App.useApp();
3433
const { getErrorMessage } = useErrorMessageResolver();
35-
const { token } = theme.useToken();
3634

3735
const vfolders = useFragment(
3836
graphql`
39-
fragment DeleteVFolderModalV2Fragment on VirtualFolderNode
40-
@relay(plural: true) {
37+
fragment DeleteVFolderModalV2Fragment on VFolder @relay(plural: true) {
4138
id
42-
name
43-
permissions
39+
metadata {
40+
name
41+
}
4442
}
4543
`,
4644
vfolderFrgmts,
@@ -58,12 +56,13 @@ const DeleteVFolderModalV2: React.FC<DeleteVFolderModalV2Props> = ({
5856
`,
5957
);
6058

61-
const foldersByPermission = _.groupBy(vfolders, (vfolder) => {
62-
if (vfolder.permissions?.includes('delete_vfolder')) {
63-
return 'deletable';
64-
}
65-
return 'undeletable';
66-
});
59+
// TODO(needs-backend): V2 `VFolder` does not expose a per-user action
60+
// permission (legacy `VirtualFolderNode.permissions` had `delete_vfolder`).
61+
// `accessControl.permission` is a mount-level enum (RO/RW/RW_DELETE), not
62+
// an entity-level action permission, so it cannot be used to filter out
63+
// undeletable folders here. Send all selected folders and let the backend
64+
// reject unauthorized ones until a proper permission field is exposed.
65+
const folders = vfolders ?? [];
6766

6867
return (
6968
<BAIModal
@@ -73,12 +72,11 @@ const DeleteVFolderModalV2: React.FC<DeleteVFolderModalV2Props> = ({
7372
okButtonProps={{ danger: true }}
7473
onCancel={() => onRequestClose?.(false)}
7574
onOk={() => {
76-
const deletable = foldersByPermission.deletable ?? [];
77-
if (deletable.length === 0) {
75+
if (folders.length === 0) {
7876
onRequestClose?.(false);
7977
return;
8078
}
81-
const ids = _.map(deletable, (vfolder) => toLocalId(vfolder.id));
79+
const ids = _.map(folders, (vfolder) => toLocalId(vfolder.id));
8280
commitBulkDeleteMutation({
8381
variables: { input: { ids } },
8482
onCompleted: (data, errors) => {
@@ -91,22 +89,24 @@ const DeleteVFolderModalV2: React.FC<DeleteVFolderModalV2Props> = ({
9189
if (deletedCount === 0) {
9290
message.error(
9391
t('data.folders.FailedToDeleteFolders', {
94-
folderNames: _.map(deletable, 'name').join(', '),
92+
folderNames: _.map(folders, (v) => v?.metadata?.name).join(
93+
', ',
94+
),
9595
}),
9696
);
9797
return;
9898
}
99-
if (deletable.length === 1) {
99+
if (folders.length === 1) {
100100
message.success(
101101
t('data.folders.FolderDeleted', {
102-
folderName: deletable[0]?.name,
102+
folderName: folders[0]?.metadata?.name,
103103
}),
104104
);
105105
} else {
106106
message.success(
107107
t('data.folders.MultipleFolderDeleted', {
108108
count: deletedCount,
109-
total: deletable.length,
109+
total: folders.length,
110110
}),
111111
);
112112
}
@@ -120,37 +120,13 @@ const DeleteVFolderModalV2: React.FC<DeleteVFolderModalV2Props> = ({
120120
{...baiModalProps}
121121
>
122122
<BAIFlex direction="column" gap={'sm'} align="stretch">
123-
{vfolders &&
124-
vfolders.length !== foldersByPermission.deletable?.length && (
125-
<BAIAlert
126-
showIcon
127-
ghostInfoBg={false}
128-
title={t('data.folders.ExcludedFolders', {
129-
count: foldersByPermission.undeletable?.length || 0,
130-
})}
131-
description={
132-
<ul
133-
style={{
134-
margin: 0,
135-
padding: 0,
136-
paddingTop: token.paddingXXS,
137-
listStyle: 'circle',
138-
}}
139-
>
140-
{_.map(foldersByPermission.undeletable, (vfolder) => (
141-
<li key={vfolder.id}>{vfolder.name}</li>
142-
))}
143-
</ul>
144-
}
145-
/>
146-
)}
147123
<Typography.Text>
148-
{foldersByPermission.deletable?.length === 1
124+
{folders.length === 1
149125
? t('data.folders.MoveToTrashDescription', {
150-
folderName: foldersByPermission.deletable?.[0]?.name,
126+
folderName: folders[0]?.metadata?.name,
151127
})
152128
: t('data.folders.MoveToTrashMultipleDescription', {
153-
folderLength: foldersByPermission.deletable?.length,
129+
folderLength: folders.length,
154130
})}
155131
</Typography.Text>
156132
</BAIFlex>

react/src/components/RestoreVFolderModalV2.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,11 @@ const RestoreVFolderModalV2: React.FC<RestoreVFolderModalV2Props> = ({
3535

3636
const vfolders = useFragment(
3737
graphql`
38-
fragment RestoreVFolderModalV2Fragment on VirtualFolderNode
39-
@relay(plural: true) {
38+
fragment RestoreVFolderModalV2Fragment on VFolder @relay(plural: true) {
4039
id
41-
name
40+
metadata {
41+
name
42+
}
4243
}
4344
`,
4445
vfolderFrgmts,
@@ -95,7 +96,7 @@ const RestoreVFolderModalV2: React.FC<RestoreVFolderModalV2Props> = ({
9596
if (vfolders?.length === 1) {
9697
message.success(
9798
t('data.folders.FolderRestored', {
98-
folderName: vfolders?.[0]?.name,
99+
folderName: vfolders?.[0]?.metadata?.name,
99100
}),
100101
);
101102
} else {
@@ -114,7 +115,7 @@ const RestoreVFolderModalV2: React.FC<RestoreVFolderModalV2Props> = ({
114115
<Typography.Text>
115116
{vfolders?.length === 1
116117
? t('data.folders.RestoreDescription', {
117-
folderName: vfolders?.[0]?.name,
118+
folderName: vfolders?.[0]?.metadata?.name,
118119
})
119120
: t('data.folders.RestoreMultipleDescription', {
120121
folderLength: vfolders?.length,

0 commit comments

Comments
 (0)