From 102399920af682b2174c911edcaf3a626113d11c Mon Sep 17 00:00:00 2001 From: ironAiken2 <51399982+ironAiken2@users.noreply.github.com> Date: Fri, 10 Jan 2025 09:07:08 +0000 Subject: [PATCH] [FR-240] fix: show vfolder types in folder create modal based on user permission (#2992) ### This PR resolves [#FR-240](https://lablup.atlassian.net/browse/FR-240?atlOrigin=eyJpIjoiYTA1Mjk0NjJiNjFiNGI0MjhjYzJiNjEwZWI2MDlmZDYiLCJwIjoiaiJ9) issue Apply rules based on the `current user role` and `list_allowed_types` to the vfolder type selector in `FolderCreateModal`. **Changes:** - Only `super admin` or `admin` can create project type vfolder. - vfolder type in folder create modal is based on `list_allowed_types` comes from server - Show `max_vfolder_count` in `Project Resource Policy` if user role is only `Super Admin` or `Admin`. **Checklist:** (if applicable) - [ ] Mention to the original issue - [ ] 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 --- react/src/components/FolderCreateModal.tsx | 33 ++++++++++++++++++--- react/src/components/StorageStatusPanel.tsx | 28 +++++++++++++---- react/src/pages/VFolderListPage.tsx | 6 ++-- 3 files changed, 54 insertions(+), 13 deletions(-) diff --git a/react/src/components/FolderCreateModal.tsx b/react/src/components/FolderCreateModal.tsx index 1e8ed6aa3b..619146b3c4 100644 --- a/react/src/components/FolderCreateModal.tsx +++ b/react/src/components/FolderCreateModal.tsx @@ -1,6 +1,7 @@ import { useBaiSignedRequestWithPromise } from '../helper'; -import { useCurrentDomainValue } from '../hooks'; -import { useTanMutation } from '../hooks/reactQueryAlias'; +import { useCurrentDomainValue, useSuspendedBackendaiClient } from '../hooks'; +import { useCurrentUserRole } from '../hooks/backendai'; +import { useTanMutation, useTanQuery } from '../hooks/reactQueryAlias'; import { useCurrentProjectValue } from '../hooks/useCurrentProject'; import BAIModal, { BAIModalProps } from './BAIModal'; import Flex from './Flex'; @@ -9,6 +10,7 @@ import StorageSelect from './StorageSelect'; import { App, Button, Divider, Form, Input, Radio, Switch, theme } from 'antd'; import { createStyles } from 'antd-style'; import { FormInstance } from 'antd/lib'; +import _ from 'lodash'; import { Suspense, useRef } from 'react'; import { useTranslation } from 'react-i18next'; @@ -76,11 +78,21 @@ const FolderCreateModal: React.FC = ({ const { message } = App.useApp(); const formRef = useRef(null); + const baiClient = useSuspendedBackendaiClient(); + const userRole = useCurrentUserRole(); const currentDomain = useCurrentDomainValue(); const currentProject = useCurrentProjectValue(); const baiRequestWithPromise = useBaiSignedRequestWithPromise(); + const { data: allowedTypes, isFetching: isFetchingAllowedTypes } = + useTanQuery({ + queryKey: ['allowedTypes', modalProps.open], + enabled: modalProps.open, + queryFn: () => + modalProps.open ? baiClient.vfolder.list_allowed_types() : undefined, + }); + const mutationToCreateFolder = useTanMutation< FolderCreationResponse, { message?: string }, @@ -135,6 +147,7 @@ const FolderCreateModal: React.FC = ({ return ( = ({ } width={650} + okButtonProps={{ loading: mutationToCreateFolder.isPending }} onCancel={() => { onRequestClose(); }} @@ -224,8 +238,19 @@ const FolderCreateModal: React.FC = ({ style={{ flex: 1, marginBottom: 0 }} > - User - Project + {/* Both checks are required: + * - role check (admin/superadmin): Controls permission to create project folders + * - allowedTypes check: Ensures the 'group' type is registered in ETCD + * allowedTypes comes from ETCD and contains all registered types regardless of permissions, + * so we need both checks for proper access control + */} + {_.includes(allowedTypes, 'user') ? ( + User + ) : null} + {(userRole === 'admin' || userRole === 'superadmin') && + _.includes(allowedTypes, 'group') ? ( + Project + ) : null} diff --git a/react/src/components/StorageStatusPanel.tsx b/react/src/components/StorageStatusPanel.tsx index 6d3c65ddca..be05efa25f 100644 --- a/react/src/components/StorageStatusPanel.tsx +++ b/react/src/components/StorageStatusPanel.tsx @@ -1,5 +1,6 @@ import { addQuotaScopeTypePrefix, usageIndicatorColor } from '../helper'; import { useCurrentDomainValue, useSuspendedBackendaiClient } from '../hooks'; +import { useCurrentUserRole } from '../hooks/backendai'; import { useSuspenseTanQuery } from '../hooks/reactQueryAlias'; import { useCurrentProjectValue } from '../hooks/useCurrentProject'; import Flex from './Flex'; @@ -35,6 +36,7 @@ const StorageStatusPanel: React.FC<{ const { token } = theme.useToken(); const baiClient = useSuspendedBackendaiClient(); const currentProject = useCurrentProjectValue(); + const currentUserRole = useCurrentUserRole(); const [selectedVolumeInfo, setSelectedVolumeInfo] = useState(); const deferredSelectedVolumeInfo = useDeferredValue(selectedVolumeInfo); @@ -116,6 +118,7 @@ const StorageStatusPanel: React.FC<{ const { user_resource_policy, + project_resource_policy, keypair_resource_policy, project_quota_scope, user_quota_scope, @@ -123,7 +126,7 @@ const StorageStatusPanel: React.FC<{ graphql` query StorageStatusPanelQuery( $user_RP_name: String - # $project_RP_name: String! + $project_RP_name: String! $keypair_resource_policy_name: String $project_quota_scope_id: String! $user_quota_scope_id: String! @@ -133,9 +136,10 @@ const StorageStatusPanel: React.FC<{ user_resource_policy(name: $user_RP_name) @since(version: "23.09.6") { max_vfolder_count } - # project_resource_policy(name: $project_RP_name) @since(version: "23.09.1") { - # max_vfolder_count - # } + project_resource_policy(name: $project_RP_name) + @since(version: "23.09.1") { + max_vfolder_count + } keypair_resource_policy(name: $keypair_resource_policy_name) # use max_vfolder_count in keypair_resource_policy before adding max_vfolder_count in user_resource_policy @deprecatedSince(version: "23.09.4") { @@ -157,7 +161,7 @@ const StorageStatusPanel: React.FC<{ `, { user_RP_name: user?.resource_policy, - // project_RP_name: currentProjectDetail?.resource_policy || "", + project_RP_name: currentProject?.name ?? '', keypair_resource_policy_name: keypair?.resource_policy, project_quota_scope_id: addQuotaScopeTypePrefix( 'project', @@ -171,7 +175,6 @@ const StorageStatusPanel: React.FC<{ !deferredSelectedVolumeInfo?.id, }, ); - // Support version: // keypair resource policy < 23.09.4 // user resource policy, project resource policy >= 23.09.6 @@ -228,6 +231,19 @@ const StorageStatusPanel: React.FC<{ {t('data.ProjectFolder')}: {projectFolderCount} + {(currentUserRole === 'admin' || + currentUserRole === 'superadmin') && + (project_resource_policy?.max_vfolder_count ?? -1) >= 0 ? ( + <> + {' / '} + + {t('data.Limit')}: + + {project_resource_policy?.max_vfolder_count === 0 + ? '∞' + : project_resource_policy?.max_vfolder_count} + + ) : null} diff --git a/react/src/pages/VFolderListPage.tsx b/react/src/pages/VFolderListPage.tsx index f3d8f10069..eafc1ecb3c 100644 --- a/react/src/pages/VFolderListPage.tsx +++ b/react/src/pages/VFolderListPage.tsx @@ -40,12 +40,11 @@ const tabParam = withDefault(StringParam, 'general'); const VFolderListPage: React.FC = (props) => { const { t } = useTranslation(); + const { token } = theme.useToken(); const [curTabKey, setCurTabKey] = useQueryParam('tab', tabParam, { updateType: 'replace', }); - const baiClient = useSuspendedBackendaiClient(); const [fetchKey, updateFetchKey] = useUpdatableState('first'); - const dataViewRef = useRef(null); const [isOpenCreateModal, { toggle: openCreateModal }] = useToggle(false); const [inviteFolderId, setInviteFolderId] = useState(null); const [ @@ -53,7 +52,8 @@ const VFolderListPage: React.FC = (props) => { { toggle: toggleImportFromHuggingFaceModal }, ] = useToggle(false); - const { token } = theme.useToken(); + const dataViewRef = useRef(null); + const baiClient = useSuspendedBackendaiClient(); const enableImportFromHuggingFace = baiClient._config.enableImportFromHuggingFace;