Skip to content

Commit 0381546

Browse files
committed
feat(FR-2696): create project admin data page
Introduce the component-level V2 foundation for the upcoming project admin data page — a V2-backed VFolder list view. Migrates VFolderNodesV2 and its inner components from the legacy VirtualFolderNode fragment to the Strawberry V2 VFolder fragment. V1 VFolderNodes remains untouched for 26.4.x backward compatibility. Components migrated to V2: - react/src/components/VFolderNodesV2.tsx (fragment: VFolder) - react/src/components/DeleteVFolderModalV2.tsx - react/src/components/DeleteForeverVFolderModalV2.tsx - react/src/components/RestoreVFolderModalV2.tsx New sub-components on the V2 fragment: - react/src/components/VFolderNodeIdenticonV2.tsx - react/src/components/VFolderPermissionCellV2.tsx - react/src/components/SharedFolderPermissionInfoModalV2.tsx - packages/backend.ai-ui/src/components/fragments/BAIVFolderDeleteButtonV2.tsx Pages (VFolderNodeListPage, AdminVFolderNodeListPage) are temporarily switched back to V1 VFolderNodes / DeleteVFolderModal / RestoreVFolderModal to keep the V1 query path compatible with this PR's isolated compile. The page migration to V2 queries (myVfolders / adminVfoldersV2) is handled in the follow-up FR-2573 on top of this PR.
1 parent dabf732 commit 0381546

38 files changed

Lines changed: 1772 additions & 644 deletions

data/schema.graphql

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3948,6 +3948,28 @@ type CreateUserV2Payload
39483948
user: UserV2!
39493949
}
39503950

3951+
"""
3952+
Added in UNRELEASED. Scope-agnostic body for vfolder creation. The owning scope is supplied as a separate mutation argument.
3953+
"""
3954+
input CreateVFolderInScopeInput
3955+
@join__type(graph: STRAWBERRY)
3956+
{
3957+
"""VFolder name."""
3958+
name: String!
3959+
3960+
"""Storage host for the vfolder."""
3961+
host: String = null
3962+
3963+
"""Usage mode of the vfolder."""
3964+
usageMode: VFolderUsageMode! = GENERAL
3965+
3966+
"""Default mount permission of the vfolder."""
3967+
permission: VFolderMountPermission! = READ_WRITE
3968+
3969+
"""Whether the vfolder is cloneable."""
3970+
cloneable: Boolean! = false
3971+
}
3972+
39513973
"""Added in 26.4.2. Input for creating a new virtual folder."""
39523974
input CreateVFolderV2Input
39533975
@join__type(graph: STRAWBERRY)
@@ -10798,6 +10820,11 @@ type Mutation
1079810820
"""Added in 26.4.2. Create a new virtual folder."""
1079910821
createVfolderV2(input: CreateVFolderV2Input!): CreateVFolderV2Payload! @join__field(graph: STRAWBERRY)
1080010822

10823+
"""
10824+
Added in UNRELEASED. Create a virtual folder owned by the specified project. Requires project-scoped CREATE permission.
10825+
"""
10826+
createVFolderInProject(projectId: UUID!, input: CreateVFolderInScopeInput!): CreateVFolderV2Payload! @join__field(graph: STRAWBERRY)
10827+
1080110828
"""Added in 26.4.2. Soft-delete a virtual folder (move to trash)."""
1080210829
deleteVfolderV2(vfolderId: UUID!): DeleteVFolderV2Payload! @join__field(graph: STRAWBERRY)
1080310830

@@ -13818,6 +13845,7 @@ enum RBACElementType
1381813845
DEPLOYMENT_POLICY @join__enumValue(graph: STRAWBERRY)
1381913846
DEPLOYMENT_REVISION @join__enumValue(graph: STRAWBERRY)
1382013847
IMAGE_ALIAS @join__enumValue(graph: STRAWBERRY)
13848+
ROLE_ASSIGNMENT @join__enumValue(graph: STRAWBERRY)
1382113849
ARTIFACT_REVISION @join__enumValue(graph: STRAWBERRY)
1382213850
}
1382313851

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: 11 additions & 6 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,
@@ -63,7 +65,8 @@ const DeleteForeverVFolderModalV2: React.FC<
6365
// generic confirmation word since there is no single name to bind to.
6466
const confirmText =
6567
purgeable.length === 1
66-
? (purgeable[0]?.name ?? t('data.folders.DeleteForeverConfirmText'))
68+
? (purgeable[0]?.metadata?.name ??
69+
t('data.folders.DeleteForeverConfirmText'))
6770
: t('data.folders.DeleteForeverConfirmText');
6871

6972
return (
@@ -88,7 +91,7 @@ const DeleteForeverVFolderModalV2: React.FC<
8891
<Typography.Text>
8992
{purgeable.length === 1
9093
? t('data.folders.DeleteForeverDescription', {
91-
folderName: purgeable[0]?.name,
94+
folderName: purgeable[0]?.metadata?.name,
9295
})
9396
: t('data.folders.DeleteForeverMultipleDescription', {
9497
folderLength: purgeable.length,
@@ -120,15 +123,17 @@ const DeleteForeverVFolderModalV2: React.FC<
120123
if (purgedCount === 0) {
121124
message.error(
122125
t('data.folders.FailedToDeleteFolders', {
123-
folderNames: _.map(purgeable, 'name').join(', '),
126+
folderNames: _.map(purgeable, (v) => v?.metadata?.name).join(
127+
', ',
128+
),
124129
}),
125130
);
126131
return;
127132
}
128133
if (purgeable.length === 1) {
129134
message.success(
130135
t('data.folders.FolderDeletedForever', {
131-
folderName: purgeable[0]?.name,
136+
folderName: purgeable[0]?.metadata?.name,
132137
}),
133138
);
134139
} 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,
@@ -57,12 +55,13 @@ const DeleteVFolderModalV2: React.FC<DeleteVFolderModalV2Props> = ({
5755
}
5856
`);
5957

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

6766
return (
6867
<BAIModal
@@ -73,12 +72,11 @@ const DeleteVFolderModalV2: React.FC<DeleteVFolderModalV2Props> = ({
7372
confirmLoading={isInFlightBulkDelete}
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>

0 commit comments

Comments
 (0)