Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions react/src/hooks/useWebUIMenuItems.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export const VALID_MENU_KEYS = [
'admin-dashboard',
'admin-data',
'project-admin-users',
'project-admin-vfolders',
'agent',
'project',
'settings',
Expand All @@ -130,6 +131,7 @@ const ALL_ADMIN_PAGE_KEYS: ReadonlySet<string> = new Set([
'admin-dashboard',
'admin-data',
'project-admin-users',
'project-admin-vfolders',
'agent',
'project',
'settings',
Expand All @@ -141,15 +143,17 @@ const ALL_ADMIN_PAGE_KEYS: ReadonlySet<string> = new Set([
]);

// Admin-category page keys reachable by a project admin (3-tier admin gating).
// Project admins see Sessions, Serving, Data (vfolders) and Members within the
// admin category. Other admin pages remain visible only to domain admins or
// Project admins see Sessions, Members, and Folders within the admin category.
// The Folders page (`project-admin-vfolders`) reuses `AdminVFolderNodeListPage`,
// which auto-scopes the `vfolder_nodes` query to the current project for
// project admins. Other admin pages remain visible only to domain admins or
// superadmins. Kept as a plain array so it can be exported and reused (e.g. for
// per-page route gating in follow-up PRs).
export const PROJECT_ADMIN_PAGE_KEYS = [
// 'admin-session',
'admin-session',
// 'admin-serving',
// 'admin-data',
'project-admin-users',
'project-admin-vfolders',
] as const;

const PROJECT_ADMIN_PAGE_KEY_SET: ReadonlySet<string> = new Set(
Expand Down Expand Up @@ -357,6 +361,16 @@ export const useWebUIMenuItems = (props?: UseWebUIMenuItemsProps) => {
key: 'project-admin-users' as MenuKeys,
group: 'admin-operations' as AdminMenuGroupName,
},
{
label: (
<WebUILink to="/project-admin-vfolders">
{t('webui.menu.ProjectFolders')}
</WebUILink>
),
icon: <CloudUploadOutlined style={{ color: token.colorInfo }} />,
key: 'project-admin-vfolders' as MenuKeys,
group: 'admin-operations' as AdminMenuGroupName,
},
isSuperAdmin && {
label: <WebUILink to="/project">{t('webui.menu.Projects')}</WebUILink>,
icon: <TeamOutlined style={{ color: token.colorInfo }} />,
Expand Down
92 changes: 72 additions & 20 deletions react/src/pages/AdminVFolderNodeListPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@ import type {
import BAIRadioGroup from '../components/BAIRadioGroup';
import BAITabs from '../components/BAITabs';
import DeleteVFolderModal from '../components/DeleteVFolderModal';
import FolderCreateModal from '../components/FolderCreateModal';
import RestoreVFolderModal from '../components/RestoreVFolderModal';
import VFolderNodes, { VFolderNodeInList } from '../components/VFolderNodes';
import { handleRowSelectionChange } from '../helper';
import { useSuspendedBackendaiClient } from '../hooks';
import { useCurrentProjectValue } from '../hooks/useCurrentProject';
import { useEffectiveAdminRole } from '../hooks/useCurrentUserProjectRoles';
import { isDeletedCategory } from './VFolderNodeListPage';
import { useToggle } from 'ahooks';
import { Badge, Button, theme, Tooltip } from 'antd';
Expand Down Expand Up @@ -66,6 +69,8 @@ const AdminVFolderNodeListPage: React.FC = (props) => {
const { t } = useTranslation();
const { token } = theme.useToken();
const baiClient = useSuspendedBackendaiClient();
const effectiveAdminRole = useEffectiveAdminRole();
const currentProject = useCurrentProjectValue();

const [columnOverrides, setColumnOverrides] = useBAISettingUserState(
'table_column_overrides.AdminVFolderNodeListPage',
Expand All @@ -75,6 +80,7 @@ const AdminVFolderNodeListPage: React.FC = (props) => {
Array<VFolderNodesType>
>([]);

const [isOpenCreateModal, { toggle: toggleCreateModal }] = useToggle(false);
const [isOpenDeleteModal, { toggle: toggleDeleteModal }] = useToggle(false);
const [isOpenRestoreModal, { toggle: toggleRestoreModal }] = useToggle(false);
Comment on lines 69 to 85
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that the admin folder list can be scoped by currentProject (for project admins), selected rows can become stale when the user switches projects. Without clearing selectedFolderList on currentProject.id change, bulk actions (delete/restore) may target folders from a previous project. Add an effect similar to VFolderNodeListPage to reset selection (and optionally pagination) when currentProject.id changes.

Copilot uses AI. Check for mistakes.

Expand Down Expand Up @@ -123,22 +129,48 @@ const AdminVFolderNodeListPage: React.FC = (props) => {

const [fetchKey, updateFetchKey] = useUpdatableState('initial-fetch');

// scope_id is intentionally omitted so superadmin sees all vfolders across all projects/domains
// Determine the `scope_id` to apply to the `vfolder_nodes` query based on the
// user's effective admin role:
// - superadmin: no scope (sees all projects/domains)
// - currentProjectAdmin: scope to the currently selected project
// - domainAdmin: TODO(needs-backend) — requires a `domain:<name>` scope argument
// that the core does not yet accept. For now, fall back to no scope, which
// matches the pre-FR-2556 behavior for domain admins.
// Personal (user-type) folders are always filtered out via `ownership_type == "group"`
// so that admins never see other users' private folders.
const scopeId: string | undefined =
effectiveAdminRole === 'currentProjectAdmin' && currentProject?.id
? `project:${currentProject.id}`
: undefined;
Comment on lines +132 to +144
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For effectiveAdminRole === 'projectAdmin', scopeId falls back to undefined when currentProject.id is missing, which makes the vfolder_nodes query unscoped (potentially showing folders outside the intended project scope). Consider making currentProject.id mandatory in this role (e.g., block rendering/query until available or surface an error) and avoid issuing an unscoped admin query for project admins.

Copilot uses AI. Check for mistakes.
// TODO(needs-backend): FR-2556 — domainAdmin scope requires `domain:<name>`
// scope support on the `vfolder_nodes` query. Remove this note once the
// backend contract is finalized.

const ownershipTypeFilter = 'ownership_type == "group"';

const queryVariables: AdminVFolderNodeListPageQuery$variables = {
offset: baiPaginationOption.offset,
first: baiPaginationOption.first,
scope_id: scopeId,
filter: mergeFilterValues([
queryParams.statusCategory === 'active' ||
queryParams.statusCategory === undefined
? FILTER_BY_STATUS_CATEGORY['active']
: FILTER_BY_STATUS_CATEGORY['deleted'],
queryParams.filter,
usageModeFilter,
ownershipTypeFilter,
]),
order: queryParams.order,
permission: 'read_attribute',
filterForActiveCount: FILTER_BY_STATUS_CATEGORY['active'],
filterForDeletedCount: FILTER_BY_STATUS_CATEGORY['deleted'],
filterForActiveCount: mergeFilterValues([
FILTER_BY_STATUS_CATEGORY['active'],
ownershipTypeFilter,
]),
filterForDeletedCount: mergeFilterValues([
FILTER_BY_STATUS_CATEGORY['deleted'],
ownershipTypeFilter,
]),
};
const deferredQueryVariables = useDeferredValue(queryVariables);
const deferredFetchKey = useDeferredValue(fetchKey);
Expand All @@ -149,6 +181,7 @@ const AdminVFolderNodeListPage: React.FC = (props) => {
query AdminVFolderNodeListPageQuery(
$offset: Int
$first: Int
$scope_id: ScopeField
$filter: String
$order: String
$permission: VFolderPermissionValueField
Expand All @@ -158,6 +191,7 @@ const AdminVFolderNodeListPage: React.FC = (props) => {
vfolder_nodes(
offset: $offset
first: $first
scope_id: $scope_id
filter: $filter
order: $order
permission: $permission
Expand All @@ -181,6 +215,7 @@ const AdminVFolderNodeListPage: React.FC = (props) => {
active: vfolder_nodes(
first: 0
offset: 0
scope_id: $scope_id
filter: $filterForActiveCount
permission: $permission
) {
Expand All @@ -189,6 +224,7 @@ const AdminVFolderNodeListPage: React.FC = (props) => {
deleted: vfolder_nodes(
first: 0
offset: 0
scope_id: $scope_id
filter: $filterForDeletedCount
permission: $permission
) {
Expand All @@ -212,6 +248,16 @@ const AdminVFolderNodeListPage: React.FC = (props) => {
<BAICard
variant="borderless"
title={t('data.Folders')}
extra={
<Button
type="primary"
onClick={() => {
toggleCreateModal();
}}
>
{t('data.CreateFolder')}
</Button>
}
styles={{
header: {
borderBottom: 'none',
Expand Down Expand Up @@ -341,23 +387,9 @@ const AdminVFolderNodeListPage: React.FC = (props) => {
propertyLabel: t('data.folders.Location'),
type: 'string',
},
{
key: 'ownership_type',
propertyLabel: t('data.Type'),
type: 'string',
strictSelection: true,
defaultOperator: '==',
options: [
{
label: t('data.User'),
value: 'user',
},
{
label: t('data.Project'),
value: 'group',
},
],
},
// `ownership_type` filter removed: admin view is pinned to
// project-type folders (user-type folders are hidden for
// all admin roles per the FR-2209 spec audit).
{
key: 'permission',
propertyLabel: t('data.Permission'),
Expand Down Expand Up @@ -498,6 +530,26 @@ const AdminVFolderNodeListPage: React.FC = (props) => {
/>
</BAIFlex>
</BAICard>
<FolderCreateModal
open={isOpenCreateModal}
// Admin view creates folders for the currently-scoped project only:
// the type radio group ("User"/"Project") and the project selector
// are hidden so an admin cannot create a personal folder or target a
// different project. The `group` field defaults to
// `currentProject.id` inside `FolderCreateModal`, so simply hiding
// the fields locks the folder to the current project.
initialValues={{
type: 'project',
group: currentProject?.id || undefined,
}}
hiddenFormItems={['type', 'group']}
Comment on lines +535 to +545
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

initialValues always includes group, even when currentProject?.id is undefined. In FolderCreateModal, mergedInitialValues is {...INITIAL_FORM_VALUES, ...initialValuesFromProps}, so an explicit group: undefined from props overrides the modal's own default group value and can lead to submitting an invalid project folder (especially since the group field is hidden). Consider omitting the group key entirely when there is no currentProject.id, and/or disabling the Create button/modal until a project is selected.

Copilot uses AI. Check for mistakes.
onRequestClose={(success) => {
if (success) {
updateFetchKey();
}
toggleCreateModal();
}}
/>
<DeleteVFolderModal
vfolderFrgmts={selectedFolderList}
open={isOpenDeleteModal}
Expand Down
12 changes: 12 additions & 0 deletions react/src/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,18 @@ export const mainLayoutChildRoutes: RouteObject[] = [
);
},
},
{
path: '/project-admin-vfolders',
handle: { labelKey: 'webui.menu.ProjectFolders' },
Component: () => {
useSuspendedBackendaiClient();
return (
<Suspense fallback={<Skeleton active />}>
<AdminVFolderNodeListPage />
</Suspense>
);
},
},
{
path: '/environment',
handle: { labelKey: 'webui.menu.Environments' },
Expand Down
1 change: 1 addition & 0 deletions resources/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2973,6 +2973,7 @@
"PrivacyPolicy": "Privacy Policy",
"ProfileUpdated": "Profile has been successfully updated.",
"Project": "Project",
"ProjectFolders": "Folders",
"ProjectMembers": "Users",
"Projects": "Projects",
"RBACManagement": "RBAC Management",
Expand Down
1 change: 1 addition & 0 deletions resources/i18n/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -2975,6 +2975,7 @@
"PrivacyPolicy": "개인정보 보호",
"ProfileUpdated": "프로필이 성공적으로 업데이트되었습니다.",
"Project": "프로젝트",
"ProjectFolders": "폴더",
"ProjectMembers": "사용자",
"Projects": "프로젝트",
"RBACManagement": "RBAC 관리",
Expand Down
Loading