Browse Source

feat: enhanced rbac GUI (#758)

* add collection selectors

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* add database selector

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* fix some logics

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* WIP

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* fix autocomplete selector

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* update

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* finish rbac UI

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* show privilege count on collection option

Signed-off-by: shanghaikid <jiangruiyi@gmail.com>

* better performance

Signed-off-by: shanghaikid <jiangruiyi@gmail.com>

* bug fix

Signed-off-by: shanghaikid <jiangruiyi@gmail.com>

* bug fix

Signed-off-by: shanghaikid <jiangruiyi@gmail.com>

* fix server select role api

Signed-off-by: shanghaikid <jiangruiyi@gmail.com>

* part

Signed-off-by: shanghaikid <jiangruiyi@gmail.com>

* support indeterminate for privilege cate

Signed-off-by: shanghaikid <jiangruiyi@gmail.com>

* show privileges count for collection opitons

Signed-off-by: shanghaikid <jiangruiyi@gmail.com>

* show privileges count for db options

Signed-off-by: shanghaikid <jiangruiyi@gmail.com>

* part

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* part

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* fix user page

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* add privileges on role page

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* fix user page

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* rename instance to cluster

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* pv group part1

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* pv group part2

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* finish user and role pages and dialog

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* update style

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* fix home page

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* clean

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* adjust styles

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* fix tips

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* support duplicate role

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

* better permission info

Signed-off-by: ryjiang <jiangruiyi@gmail.com>

---------

Signed-off-by: ryjiang <jiangruiyi@gmail.com>
Signed-off-by: shanghaikid <jiangruiyi@gmail.com>
ryjiang 2 months ago
parent
commit
2214806c6b
39 changed files with 1612 additions and 822 deletions
  1. 8 3
      client/src/components/layout/GlobalEffect.tsx
  2. 62 0
      client/src/components/layout/Wrapper.tsx
  3. 0 191
      client/src/components/menu/CommunityBtn.tsx
  4. 2 2
      client/src/context/System.tsx
  5. 1 1
      client/src/context/Types.ts
  6. 1 1
      client/src/hooks/Navigation.tsx
  7. 6 0
      client/src/http/Collection.service.ts
  8. 9 11
      client/src/http/User.service.ts
  9. 2 0
      client/src/i18n/cn/common.ts
  10. 1 1
      client/src/i18n/cn/nav.ts
  11. 27 4
      client/src/i18n/cn/user.ts
  12. 2 0
      client/src/i18n/en/common.ts
  13. 1 1
      client/src/i18n/en/nav.ts
  14. 28 3
      client/src/i18n/en/user.ts
  15. 13 28
      client/src/pages/databases/collections/search/PartitionsSelector.tsx
  16. 16 6
      client/src/pages/user/PrivilegeGroups.tsx
  17. 154 49
      client/src/pages/user/Roles.tsx
  18. 33 37
      client/src/pages/user/Types.ts
  19. 70 64
      client/src/pages/user/User.tsx
  20. 545 0
      client/src/pages/user/dialogs/DBCollectionSelector.tsx
  21. 1 0
      client/src/pages/user/dialogs/PrivilegeGroupOptions.tsx
  22. 0 82
      client/src/pages/user/dialogs/PrivilegeOptions.tsx
  23. 0 1
      client/src/pages/user/dialogs/UpdatePrivilegeGroupDialog.tsx
  24. 107 106
      client/src/pages/user/dialogs/UpdateRoleDialog.tsx
  25. 2 2
      client/src/pages/user/dialogs/UpdateUserRole.tsx
  26. 77 0
      client/src/pages/user/dialogs/styles.ts
  27. 1 1
      server/package.json
  28. 27 0
      server/src/collections/collections.controller.ts
  29. 1 32
      server/src/collections/collections.service.ts
  30. 2 8
      server/src/database/databases.service.ts
  31. 10 4
      server/src/middleware/index.ts
  32. 8 2
      server/src/milvus/milvus.service.ts
  33. 0 7
      server/src/partitions/partitions.service.ts
  34. 40 0
      server/src/types/users.type.ts
  35. 230 35
      server/src/users/users.controller.ts
  36. 73 72
      server/src/users/users.service.ts
  37. 40 62
      server/src/utils/Const.ts
  38. 2 2
      server/src/utils/Error.ts
  39. 10 4
      server/yarn.lock

+ 8 - 3
client/src/components/layout/GlobalEffect.tsx

@@ -41,23 +41,28 @@ const GlobalEffect = ({ children }: { children: React.ReactNode }) => {
         },
         error => {
           const { response } = error;
+          let messageType = 'error';
+
           if (response) {
             switch (response.status) {
               case HTTP_STATUS_CODE.UNAUTHORIZED:
-              case HTTP_STATUS_CODE.FORBIDDEN:
                 setTimeout(() => logout(true), 1000);
                 break;
+
+              case HTTP_STATUS_CODE.FORBIDDEN:
+                messageType = 'warning';
+                break;
               default:
                 break;
             }
             const errorMessage = response.data?.message;
             if (errorMessage) {
-              openSnackBar(errorMessage, 'error');
+              openSnackBar(errorMessage, messageType);
               return Promise.reject(error);
             }
           }
           // Handle other error cases
-          openSnackBar(error.message, 'error');
+          openSnackBar(error.message, messageType);
           return Promise.reject(error);
         }
       );

+ 62 - 0
client/src/components/layout/Wrapper.tsx

@@ -0,0 +1,62 @@
+import { makeStyles } from '@mui/styles';
+import { Theme } from '@mui/material';
+import { useTranslation } from 'react-i18next';
+
+interface WrapperProps {
+  hasPermission?: boolean;
+  children?: React.ReactNode;
+  className?: string;
+}
+
+const useStyles = makeStyles((theme: Theme) => ({
+  wrapper: {
+    width: '100%',
+    height: '100%',
+    position: 'relative',
+  },
+  overlay: {
+    position: 'absolute',
+    top: 0,
+    left: 0,
+    right: 0,
+    bottom: 0,
+    display: 'flex',
+    alignItems: 'center',
+    justifyContent: 'center',
+    backgroundColor: theme.palette.background.default,
+    zIndex: 1000,
+  },
+  message: {
+    fontSize: 14,
+    color: theme.palette.text.primary,
+    textAlign: 'center',
+    fontStyle: 'italic',
+  },
+}));
+
+const Wrapper = ({
+  hasPermission = true,
+  children,
+  className,
+}: WrapperProps) => {
+  // styles
+  const classes = useStyles();
+
+  // i18n
+  const { t: commonTrans } = useTranslation();
+
+  return (
+    <div className={`${classes.wrapper} ${className}`}>
+      {children}
+      {!hasPermission && (
+        <div className={classes.overlay}>
+          <div className={classes.message}>
+            {commonTrans('noPermissionTip')}
+          </div>
+        </div>
+      )}
+    </div>
+  );
+};
+
+export default Wrapper;

+ 0 - 191
client/src/components/menu/CommunityBtn.tsx

@@ -1,191 +0,0 @@
-import React from 'react';
-import { useTranslation } from 'react-i18next';
-import Button from '@mui/material/Button';
-import SvgIcon from '@mui/material/SvgIcon';
-import { Theme, Link } from '@mui/material';
-import ChevronRightIcon from '@mui/icons-material/ChevronRight';
-import GitHubIcon from '@mui/icons-material/GitHub';
-import peopleIcon from '@/assets/icons/people.svg?react';
-import qrcodePath from '@/assets/imgs/wechat_qrcode.jpeg';
-import discordIcon from '@/assets/icons/discord.svg?react';
-import { makeStyles } from '@mui/styles';
-
-const GITHUB_LINK = 'https://github.com/milvus-io/milvus/discussions';
-const DISCORD_LINK = 'https://discord.com/invite/8uyFbECzPX';
-
-const getStyles = makeStyles((theme: Theme) => ({
-  root: {
-    bottom: theme.spacing(2),
-    position: 'absolute',
-    right: theme.spacing(1),
-    width: theme.spacing(5),
-    zIndex: 3,
-  },
-  menuBtn: {
-    border: '1px solid #E9E9ED',
-    borderRadius: '50%',
-    bottom: 0,
-    boxShadow: '3px 3px 10px rgba(0, 0, 0, 0.05)',
-    height: theme.spacing(5),
-    minWidth: 'auto',
-    padding: 0,
-    position: 'absolute',
-    width: theme.spacing(5),
-  },
-  chevronIcon: {
-    transform: 'rotateZ(90deg)',
-    fill: theme.palette.primary.main,
-  },
-  container: {
-    bottom: theme.spacing(7),
-    position: 'absolute',
-    width: '360px',
-    overflow: 'hidden',
-  },
-  head: {
-    backgroundColor: theme.palette.primary.main,
-    padding: theme.spacing(1.5, 2.5),
-    color: '#fff',
-    borderTopLeftRadius: theme.spacing(1),
-    borderTopRightRadius: theme.spacing(1),
-  },
-  title: {
-    fontWeight: 700,
-    fontSize: theme.spacing(2),
-    lineHeight: theme.spacing(3),
-    letterSpacing: '-0.01em',
-  },
-  titleDesc: {
-    color: '#f0f4f9',
-    fontSize: theme.spacing(1.5),
-    lineHeight: theme.spacing(2),
-  },
-  body: {
-    backgroundColor: '#fff',
-    border: '1px solid #e9e9e9',
-    borderTop: 0,
-    borderBottomRightRadius: theme.spacing(1),
-    borderBottomLeftRadius: theme.spacing(1),
-    padding: theme.spacing(3),
-  },
-  block: {
-    border: '1px solid #f4f4f4',
-    boxShadow: '3px 3px 10px rgba(0, 0, 0, 0.05)',
-    marginBottom: theme.spacing(3),
-    padding: theme.spacing(2),
-  },
-  contentTitle: {
-    fontWeight: 500,
-    fontSize: theme.spacing(1.75),
-    lineHeight: theme.spacing(2.5),
-  },
-  contentDesc: {
-    fontSize: theme.spacing(1.5),
-    lineHeight: theme.spacing(2.5),
-    color: theme.palette.text.secondary,
-    marginBottom: theme.spacing(1),
-  },
-  contentLink: {
-    display: 'block',
-    fontSize: theme.spacing(1.5),
-    lineHeight: theme.spacing(2.5),
-    letterSpacing: '-0.01em',
-    color: theme.palette.primary.main,
-  },
-  qrImg: {
-    display: 'block',
-    margin: '0 auto',
-    width: theme.spacing(10),
-  },
-  textCenter: {
-    textAlign: 'center',
-  },
-  icon: {
-    marginTop: theme.spacing(2),
-    width: theme.spacing(2.5),
-    height: theme.spacing(2.5),
-  },
-}));
-
-const CommunityBtn = (props: any) => {
-  const [open, setOpen] = React.useState<boolean>(false);
-  const classes = getStyles();
-  const { t } = useTranslation();
-  const communityTrans: { [key in string]: string } = t('community');
-
-  return (
-    <div className={classes.root}>
-      {open && (
-        <div className={classes.container}>
-          <div className={classes.head}>
-            <div className={classes.title}>{communityTrans.hi}</div>
-            <div className={classes.titleDesc}>{communityTrans.growing}</div>
-          </div>
-          <div className={classes.body}>
-            <div className={classes.block}>
-              <div className={`${classes.contentTitle} ${classes.textCenter}`}>
-                {communityTrans.question}
-              </div>
-              <div className={`${classes.contentDesc} ${classes.textCenter}`}>
-                {communityTrans.qr}
-              </div>
-              <img className={classes.qrImg} src={qrcodePath} alt="qrcode" />
-            </div>
-            <div className={classes.block}>
-              <div className={`${classes.contentTitle} ${classes.textCenter}`}>
-                {communityTrans.more}
-              </div>
-
-              <SvgIcon
-                viewBox="0 0 24 24"
-                component={discordIcon}
-                className={classes.icon}
-              />
-              <div className={classes.contentDesc}>{communityTrans.join}</div>
-              <Link
-                classes={{ root: classes.contentLink }}
-                href={DISCORD_LINK}
-                underline="always"
-                target="_blank"
-                rel="noopener"
-              >
-                {DISCORD_LINK}
-              </Link>
-
-              <SvgIcon
-                viewBox="0 0 24 24"
-                component={GitHubIcon}
-                className={classes.icon}
-              />
-              <div className={classes.contentDesc}>{communityTrans.get}</div>
-              <Link
-                classes={{ root: classes.contentLink }}
-                href={GITHUB_LINK}
-                underline="always"
-                target="_blank"
-                rel="noopener"
-              >
-                {GITHUB_LINK}
-              </Link>
-            </div>
-          </div>
-        </div>
-      )}
-      <Button
-        className={classes.menuBtn}
-        aria-haspopup="true"
-        onClick={() => {
-          setOpen(!open);
-        }}
-      >
-        {open ? (
-          <ChevronRightIcon className={classes.chevronIcon} />
-        ) : (
-          <SvgIcon viewBox="0 0 24 24" component={peopleIcon} />
-        )}
-      </Button>
-    </div>
-  );
-};
-
-export default CommunityBtn;

+ 2 - 2
client/src/context/System.tsx

@@ -54,8 +54,8 @@ export const SystemProvider = (props: { children: React.ReactNode }) => {
       const systemInfo = rootCoord.infos.system_info;
 
       const data = {
-        users: users.usernames,
-        roles: roles.results,
+        users: users,
+        roles: roles,
         queryNodes,
         dataNodes,
         indexNodes,

+ 1 - 1
client/src/context/Types.ts

@@ -60,7 +60,7 @@ export type SnackBarType = {
 
 export type OpenSnackBarType = (
   message: string | ReactElement,
-  type?: 'error' | 'info' | 'success' | 'warning',
+  type?: 'error' | 'info' | 'success' | 'warning' | string,
   autoHideDuration?: number | null,
   position?: {
     horizontal: 'center' | 'left' | 'right';

+ 1 - 1
client/src/hooks/Navigation.tsx

@@ -71,7 +71,7 @@ export const useNavigationHook = (
         const navInfo: NavInfo = {
           navTitle: navTrans('user'),
           backPath: '',
-          showDatabaseSelector: true,
+          showDatabaseSelector: false,
         };
         setNavInfo(navInfo);
         break;

+ 6 - 0
client/src/http/Collection.service.ts

@@ -24,6 +24,12 @@ export class CollectionService extends BaseModel {
     return super.findAll({ path: `/collections`, params: data || {} });
   }
 
+  static getCollectionsNames(data?: {
+    db_name: string;
+  }): Promise<string[]> {
+    return super.findAll({ path: `/collections/names`, params: data || {} });
+  }
+
   static describeCollectionUnformatted(collectionName: string) {
     return super.search({
       path: `/collections/${collectionName}/unformatted`,

+ 9 - 11
client/src/http/User.service.ts

@@ -1,9 +1,10 @@
 import BaseModel from './BaseModel';
 import type {
-  Users,
-  UsersWithRoles,
+  RolesWithPrivileges,
   PrivilegeGroup,
   PrivilegeGroupsRes,
+  UserWithRoles,
+  RBACOptions,
 } from '@server/types';
 import type {
   CreateUserParams,
@@ -19,12 +20,15 @@ import type {
 export class UserService extends BaseModel {
   // get user data
   static getUsers() {
-    return super.search<Users>({ path: `/users`, params: {} });
+    return super.search<UserWithRoles[]>({ path: `/users`, params: {} });
   }
 
   // get all roles
   static getRoles() {
-    return super.search<UsersWithRoles>({ path: `/users/roles`, params: {} });
+    return super.search<RolesWithPrivileges[]>({
+      path: `/users/roles`,
+      params: {},
+    });
   }
 
   // create user
@@ -89,13 +93,7 @@ export class UserService extends BaseModel {
     return super.search({
       path: `/users/rbac`,
       params: {},
-    }) as Promise<{
-      GlobalPrivileges: Record<string, unknown>;
-      CollectionPrivileges: Record<string, unknown>;
-      RbacObjects: Record<string, unknown>;
-      UserPrivileges: Record<string, unknown>;
-      Privileges: Record<string, unknown>;
-    }>;
+    }) as Promise<RBACOptions>;
   }
   // get privilege groups
   static getPrivilegeGroups() {

+ 2 - 0
client/src/i18n/cn/common.ts

@@ -85,6 +85,8 @@ const commonTrans = {
   memory: '内存',
   yes: '是',
   no: '否',
+
+  noPermissionTip: '您没有权限访问此页面。',
 };
 
 export default commonTrans;

+ 1 - 1
client/src/i18n/cn/nav.ts

@@ -5,7 +5,7 @@ const navTrans = {
   console: '搜索控制台',
   search: '向量搜索',
   system: '系统视图',
-  user: '用户',
+  user: '用户和角色',
   dbAdmin: '数据库管理',
   database: '数据库',
 };

+ 27 - 4
client/src/i18n/cn/user.ts

@@ -1,5 +1,3 @@
-import PrivilegeGroups from '@/pages/user/PrivilegeGroups';
-
 const userTrans = {
   createTitle: '创建用户',
   updateTitle: '更新Milvus用户',
@@ -7,21 +5,25 @@ const userTrans = {
   user: '用户',
   users: '用户们',
   deleteWarning: '您正在尝试删除用户。此操作无法撤销。',
+  deleteRoleWarning: '您正在尝试删除角色。此操作无法撤销。',
   oldPassword: '当前密码',
   newPassword: '新密码',
   confirmPassword: '确认密码',
   update: '更新密码',
   isNotSame: '与新密码不同',
-  deleteTip: '请至少选择一个要删除的项目,不能删除root用户。',
+  deleteTip: '请至少选择一个要删除的用户,不能删除root用户。',
+  deleteRoleTip: '请至少选择一个要删除的角色,不能删除admin/public 角色。',
+  editPassword: '修改密码',
 
   // role
-  deleteEditRoleTip: 'root角色不可编辑。',
+  deleteEditRoleTip: '请选择一个角色,并且root角色不可编辑。',
   disableEditRolePrivilegeTip: 'admin和public角色不可编辑。',
 
   role: '角色',
   editRole: '编辑角色',
   roles: '角色',
   createRoleTitle: '创建角色',
+  dupicateRoleTitle: '复制角色',
   updateRolePrivilegeTitle: '更新角色',
   updateRoleSuccess: '用户角色',
   type: '类型',
@@ -42,6 +44,27 @@ const userTrans = {
   deletePrivilegGroupWarning: '您正在尝试删除权限组,请确保没有角色与其绑定。',
   createPrivilegeGroupTitle: '创建权限组',
   updatePrivilegeGroupTitle: '更新权限组',
+  allCollections: '所有集合',
+  allDatabases: '所有Database 及所有集合',
+
+  collections: 'Collection',
+  collection: 'Collection',
+  databases: 'Database',
+  database: 'Database',
+  cluster: '集群',
+
+  DatabasePrivileges: 'Database 相关权限',
+  CollectionPrivileges: 'Collection 相关权限',
+  PartitionPrivileges: 'Partition 相关权限',
+  IndexPrivileges: 'Index 相关权限',
+  EntityPrivileges: 'Entity 相关权限',
+  ResourceManagementPrivileges: '资源管理相关权限',
+  RBACPrivileges: 'RBAC相关权限',
+
+  CollectionPrivilegeGroups: '内置权限组',
+  DatabasePrivilegeGroups: '内置权限组',
+  ClusterPrivilegeGroups: '内置权限组',
+  CustomPrivilegeGroups: '用户定义权限组',
 };
 
 export default userTrans;

+ 2 - 0
client/src/i18n/en/common.ts

@@ -86,6 +86,8 @@ const commonTrans = {
   memory: 'Memory',
   yes: 'Yes',
   no: 'No',
+
+  noPermissionTip: `You don't have permission to access this content.`,
 };
 
 export default commonTrans;

+ 1 - 1
client/src/i18n/en/nav.ts

@@ -5,7 +5,7 @@ const navTrans = {
   console: 'Search Console',
   search: 'Vector Search',
   system: 'System View',
-  user: 'User',
+  user: 'User and Role',
   dbAdmin: 'DB Management',
   database: 'Database',
 };

+ 28 - 3
client/src/i18n/en/user.ts

@@ -1,5 +1,3 @@
-import { create } from "domain";
-
 const userTrans = {
   createTitle: 'Create User',
   updateTitle: 'Update Milvus User',
@@ -7,6 +5,8 @@ const userTrans = {
   user: 'User',
   users: 'Users',
   deleteWarning: 'You are trying to drop user. This action cannot be undone.',
+  deleteRoleWarning:
+    'You are trying to drop role. This action cannot be undone.',
   oldPassword: 'Current Password',
   newPassword: 'New Password',
   confirmPassword: 'Confirm Password',
@@ -14,15 +14,19 @@ const userTrans = {
   isNotSame: 'Not same as new password',
   deleteTip:
     'Please select at least one item to drop and the root user can not be dropped.',
+  deleteRoleTip:
+    'Please select at least one item to drop and the admin/public role can not be dropped.',
+  editPassword: 'Edit Password',
 
   // role
-  deleteEditRoleTip: 'root role is not editable.',
+  deleteEditRoleTip: 'Please select one user to edit, root is not editable.',
   disableEditRolePrivilegeTip: 'admin and public role are not editable.',
 
   role: 'Role',
   editRole: 'Edit Role',
   roles: 'Roles',
   createRoleTitle: 'Create Role',
+  dupicateRoleTitle: 'Duplicate Role',
   updateRolePrivilegeTitle: 'Update Role',
   updateRoleSuccess: 'User Role',
   type: 'Type',
@@ -44,6 +48,27 @@ const userTrans = {
     'You are trying to drop the privilege group, please make sure no role is bound to it.',
   createPrivilegeGroupTitle: 'Create Privilege Group',
   updatePrivilegeGroupTitle: 'Update Privilege Group',
+  allCollections: 'All Collections',
+  allDatabases: 'All Databases',
+
+  collections: 'Collections',
+  collection: 'Collection',
+  databases: 'Databases',
+  database: 'Database',
+  cluster: 'Cluster',
+
+  DatabasePrivileges: 'Database Privileges',
+  CollectionPrivileges: 'Collection Privileges',
+  PartitionPrivileges: 'Partition Privileges',
+  IndexPrivileges: 'Index Privileges',
+  EntityPrivileges: 'Entity Privileges',
+  ResourceManagementPrivileges: 'Resource Management Privileges',
+  RBACPrivileges: 'RBAC Privileges',
+
+  CollectionPrivilegeGroups: 'Built-in Groups',
+  DatabasePrivilegeGroups: 'Built-in Groups',
+  ClusterPrivilegeGroups: 'Built-in Groups',
+  CustomPrivilegeGroups: 'User-defined Groups',
 };
 
 export default userTrans;

+ 13 - 28
client/src/pages/databases/collections/search/PartitionsSelector.tsx

@@ -1,9 +1,8 @@
 import { useState } from 'react';
+import { TextField } from '@mui/material';
 import Autocomplete from '@mui/material/Autocomplete';
-import CircularProgress from '@mui/material/CircularProgress';
 import { PartitionService } from '@/http';
 import type { PartitionData } from '@server/types';
-import CustomInput from '@/components/customInput/CustomInput';
 import { useTranslation } from 'react-i18next';
 
 interface PartitionsSelectorProps {
@@ -15,28 +14,24 @@ interface PartitionsSelectorProps {
 export default function PartitionsSelector(props: PartitionsSelectorProps) {
   // i18n
   const { t: searchTrans } = useTranslation('search');
-  // default loading
-  const DEFAULT_LOADING_OPTIONS: readonly PartitionData[] = [
-    { name: searchTrans('loading'), id: -1, rowCount: -1, createdTime: '' },
-  ];
 
   // props
   const { collectionName, selected, setSelected } = props;
   // state
   const [open, setOpen] = useState(false);
-  const [options, setOptions] = useState<readonly PartitionData[]>(
-    DEFAULT_LOADING_OPTIONS
-  );
+  const [options, setOptions] = useState<readonly PartitionData[]>([]);
   const [loading, setLoading] = useState(false);
 
   const handleOpen = () => {
     setOpen(true);
+    setLoading(true);
     (async () => {
       try {
         const res = await PartitionService.getPartitions(collectionName);
-        setLoading(false);
         setOptions([...res]);
       } catch (err) {
+        console.error(err);
+      } finally {
         setLoading(false);
       }
     })();
@@ -44,7 +39,6 @@ export default function PartitionsSelector(props: PartitionsSelectorProps) {
 
   const handleClose = () => {
     setOpen(false);
-    setOptions(DEFAULT_LOADING_OPTIONS);
   };
 
   return (
@@ -66,24 +60,15 @@ export default function PartitionsSelector(props: PartitionsSelectorProps) {
       getOptionLabel={option => (option && option.name) || ''}
       options={options}
       loading={loading}
+      noOptionsText={
+        loading ? searchTrans('loading') : searchTrans('noOptions')
+      }
       renderInput={params => {
-        return loading ? (
-          <CircularProgress color="inherit" size={20} />
-        ) : (
-          <CustomInput
-            textConfig={{
-              ...params,
-              label: searchTrans('partitionFilter'),
-              key: 'partitionFilter',
-              className: 'input',
-              value: params.inputProps.value,
-              disabled: false,
-              variant: 'filled',
-              required: false,
-              InputLabelProps: { shrink: true },
-            }}
-            checkValid={() => true}
-            type="text"
+        return (
+          <TextField
+            {...params}
+            label={searchTrans('partitionFilter')}
+            variant="filled"
           />
         );
       }}

+ 16 - 6
client/src/pages/user/PrivilegeGroups.tsx

@@ -6,6 +6,7 @@ import { rootContext } from '@/context';
 import { useNavigationHook, usePaginationHook } from '@/hooks';
 import AttuGrid from '@/components/grid/Grid';
 import { ColDefinitionsType, ToolBarConfig } from '@/components/grid/Types';
+import Wrapper from '@/components/layout/Wrapper';
 import DeleteTemplate from '@/components/customDialog/DeleteDialogTemplate';
 import { ALL_ROUTER_TYPES } from '@/router/consts';
 import UpdatePrivilegeGroupDialog from './dialogs/UpdatePrivilegeGroupDialog';
@@ -24,13 +25,18 @@ const useStyles = makeStyles((theme: Theme) => ({
 
 const PrivilegeGroups = () => {
   useNavigationHook(ALL_ROUTER_TYPES.USER);
+  // styles
   const classes = useStyles();
 
+  // ui states
   const [groups, setGroups] = useState<PrivilegeGroup[]>([]);
   const [loading, setLoading] = useState(false);
   const [selectedGroups, setSelectedGroups] = useState<PrivilegeGroup[]>([]);
+  const [hasPermission, setHasPermission] = useState(true);
+  // context
   const { setDialog, handleCloseDialog, openSnackBar } =
     useContext(rootContext);
+  // i18n
   const { t: successTrans } = useTranslation('success');
   const { t: userTrans } = useTranslation('user');
   const { t: btnTrans } = useTranslation('btn');
@@ -38,10 +44,14 @@ const PrivilegeGroups = () => {
 
   const fetchGroups = async () => {
     setLoading(true);
-    const res = await UserService.getPrivilegeGroups();
-    setGroups(res.privilege_groups);
-
-    setLoading(false);
+    try {
+      const res = await UserService.getPrivilegeGroups();
+      setGroups(res.privilege_groups);
+    } catch (error) {
+      setHasPermission(false);
+    } finally {
+      setLoading(false);
+    }
   };
 
   const onUpdate = async (data: { isEditing: boolean }) => {
@@ -198,7 +208,7 @@ const PrivilegeGroups = () => {
   };
 
   return (
-    <div className={classes.wrapper}>
+    <Wrapper className={classes.wrapper} hasPermission={hasPermission}>
       <AttuGrid
         toolbarConfigs={[]}
         colDefinitions={colDefinitions}
@@ -220,7 +230,7 @@ const PrivilegeGroups = () => {
         handleSort={handleGridSort}
         labelDisplayedRows={getLabelDisplayedRows(userTrans('privilegeGroups'))}
       />
-    </div>
+    </Wrapper>
   );
 };
 

+ 154 - 49
client/src/pages/user/Roles.tsx

@@ -13,8 +13,14 @@ import type {
   ColDefinitionsType,
   ToolBarConfig,
 } from '@/components/grid/Types';
-import type { DeleteRoleParams, RoleData } from './Types';
+import Wrapper from '@/components/layout/Wrapper';
+import type { DeleteRoleParams } from './Types';
 import { getLabelDisplayedRows } from '@/pages/search/Utils';
+import type {
+  RolesWithPrivileges,
+  RBACOptions,
+  DBCollectionsPrivileges,
+} from '@server/types';
 
 const useStyles = makeStyles((theme: Theme) => ({
   wrapper: {
@@ -27,35 +33,45 @@ const useStyles = makeStyles((theme: Theme) => ({
 
 const Roles = () => {
   useNavigationHook(ALL_ROUTER_TYPES.USER);
+  // styles
   const classes = useStyles();
+  // context
   const { database } = useContext(dataContext);
-  const [loading, setLoading] = useState(false);
-
-  const [roles, setRoles] = useState<RoleData[]>([]);
-  const [selectedRole, setSelectedRole] = useState<RoleData[]>([]);
   const { setDialog, handleCloseDialog, openSnackBar } =
     useContext(rootContext);
+  // ui states
+  const [loading, setLoading] = useState(false);
+  const [roles, setRoles] = useState<RolesWithPrivileges[]>([]);
+  const [rbacOptions, setRbacOptions] = useState<RBACOptions>(
+    {} as RBACOptions
+  );
+  const [selectedRole, setSelectedRole] = useState<RolesWithPrivileges[]>([]);
+  const [hasPermission, setHasPermission] = useState(true);
+
+  // i18n
   const { t: successTrans } = useTranslation('success');
   const { t: userTrans } = useTranslation('user');
   const { t: btnTrans } = useTranslation('btn');
   const { t: dialogTrans } = useTranslation('dialog');
 
   const fetchRoles = async () => {
-    const roles = await UserService.getRoles();
-    setSelectedRole([]);
-
-    setRoles(
-      roles.results.map(v => ({
-        name: v.role.name,
-        privilegeContent: v,
-        privileges: v.entities.map(e => ({
-          roleName: v.role.name,
-          object: e.object.name,
-          objectName: e.object_name,
-          privilegeName: e.grantor.privilege.name,
-        })),
-      }))
-    );
+    setLoading(true);
+
+    try {
+      const [roles, rbacs] = await Promise.all([
+        UserService.getRoles(),
+        UserService.getRBAC(),
+      ]);
+
+      setSelectedRole([]);
+      setRbacOptions(rbacs);
+
+      setRoles(roles as any);
+    } catch (error) {
+      setHasPermission(false);
+    } finally {
+      setLoading(false);
+    }
   };
 
   const onUpdate = async (data: { isEditing: boolean }) => {
@@ -71,7 +87,7 @@ const Roles = () => {
   const handleDelete = async (force?: boolean) => {
     for (const role of selectedRole) {
       const param: DeleteRoleParams = {
-        roleName: role.name,
+        roleName: role.roleName,
         force,
       };
       await UserService.deleteRole(param);
@@ -92,6 +108,7 @@ const Roles = () => {
           params: {
             component: (
               <UpdateRoleDialog
+                role={{ roleName: '', privileges: {} }}
                 onUpdate={onUpdate}
                 handleClose={handleCloseDialog}
               />
@@ -106,7 +123,7 @@ const Roles = () => {
       type: 'button',
       btnVariant: 'text',
       btnColor: 'secondary',
-      label: userTrans('editRole'),
+      label: btnTrans('edit'),
       onClick: async () => {
         setDialog({
           open: true,
@@ -126,11 +143,42 @@ const Roles = () => {
       disabled: () =>
         selectedRole.length === 0 ||
         selectedRole.length > 1 ||
-        selectedRole.findIndex(v => v.name === 'admin') > -1 ||
-        selectedRole.findIndex(v => v.name === 'public') > -1,
+        selectedRole.findIndex(v => v.roleName === 'admin') > -1 ||
+        selectedRole.findIndex(v => v.roleName === 'public') > -1,
+      disabledTooltip: userTrans('disableEditRolePrivilegeTip'),
+    },
+    {
+      type: 'button',
+      btnVariant: 'text',
+      btnColor: 'secondary',
+      label: btnTrans('duplicate'),
+      onClick: async () => {
+        setDialog({
+          open: true,
+          type: 'custom',
+          params: {
+            component: (
+              <UpdateRoleDialog
+                role={{
+                  ...selectedRole[0],
+                  roleName: selectedRole[0].roleName + '_copy',
+                }}
+                onUpdate={onUpdate}
+                handleClose={handleCloseDialog}
+                sameAs={true}
+              />
+            ),
+          },
+        });
+      },
+      icon: 'copy',
+      disabled: () =>
+        selectedRole.length === 0 ||
+        selectedRole.length > 1 ||
+        selectedRole.findIndex(v => v.roleName === 'admin') > -1 ||
+        selectedRole.findIndex(v => v.roleName === 'public') > -1,
       disabledTooltip: userTrans('disableEditRolePrivilegeTip'),
     },
-
     {
       type: 'button',
       btnVariant: 'text',
@@ -144,7 +192,7 @@ const Roles = () => {
               <DeleteTemplate
                 label={btnTrans('drop')}
                 title={dialogTrans('deleteTitle', { type: userTrans('role') })}
-                text={userTrans('deleteWarning')}
+                text={userTrans('deleteRoleWarning')}
                 handleDelete={handleDelete}
                 forceDelLabel={userTrans('forceDelLabel')}
               />
@@ -155,47 +203,103 @@ const Roles = () => {
       label: btnTrans('drop'),
       disabled: () =>
         selectedRole.length === 0 ||
-        selectedRole.findIndex(v => v.name === 'admin') > -1 ||
-        selectedRole.findIndex(v => v.name === 'public') > -1,
-      disabledTooltip: userTrans('deleteTip'),
+        selectedRole.findIndex(v => v.roleName === 'admin') > -1 ||
+        selectedRole.findIndex(v => v.roleName === 'public') > -1,
+      disabledTooltip: userTrans('deleteRoleTip'),
       icon: 'delete',
     },
   ];
 
   const colDefinitions: ColDefinitionsType[] = [
     {
-      id: 'name',
+      id: 'roleName',
       align: 'left',
       disablePadding: false,
       label: userTrans('role'),
+      sortType: 'string',
     },
 
     {
-      id: 'privilegeContent',
+      id: 'privileges',
       align: 'left',
       disablePadding: false,
-      formatter({ privilegeContent }) {
+      formatter({ privileges, roleName }) {
+        const isAdmin = roleName === 'admin';
+
+        // Derive the options arrays as in DBCollectionSelector.
+        const rbacEntries = Object.entries(rbacOptions) as [
+          string,
+          Record<string, string>
+        ][];
+
+        // privileges of the privilege groups
+        const privilegeGroups = rbacEntries.filter(([key]) =>
+          key.endsWith('PrivilegeGroups')
+        );
+
+        const groupPrivileges = new Set(
+          privilegeGroups.reduce(
+            (acc, [_, group]) => acc.concat(Object.values(group)),
+            [] as string[]
+          )
+        );
+
+        let groupCount = 0;
+        let privilegeCount = 0;
+
+        Object.values(privileges as DBCollectionsPrivileges).forEach(
+          dbPrivileges => {
+            Object.values(dbPrivileges.collections).forEach(
+              collectionPrivileges => {
+                Object.keys(collectionPrivileges).forEach(privilege => {
+                  if (groupPrivileges.has(privilege)) {
+                    groupCount++;
+                  } else {
+                    privilegeCount++;
+                  }
+                });
+              }
+            );
+          }
+        );
+
         return (
-          <>
-            {privilegeContent.entities.map((e: any) => {
-              return (
-                <Chip
-                  key={`${e.object.name}-${e.grantor.privilege.name}`}
-                  className={classes.chip}
-                  size="small"
-                  label={e.grantor.privilege.name}
-                  variant="outlined"
-                />
-              );
-            })}
-          </>
+          <div>
+            {
+              <>
+                <div style={{ marginBottom: 2 }}>
+                  <Chip
+                    label={`${userTrans('Group')} (${
+                      isAdmin ? '*' : groupCount
+                    })`}
+                    size="small"
+                    style={{ marginRight: 2 }}
+                  />
+                </div>
+                <div style={{ marginBottom: 2 }}>
+                  <Chip
+                    label={`${userTrans('privileges')} (${
+                      isAdmin ? '*' : privilegeCount
+                    })`}
+                    size="small"
+                    style={{ marginRight: 2 }}
+                  />
+                </div>
+              </>
+            }
+          </div>
         );
       },
       label: userTrans('privileges'),
+      getStyle: () => {
+        return {
+          width: '80%',
+        };
+      },
     },
   ];
 
-  const handleSelectChange = (value: RoleData[]) => {
+  const handleSelectChange = (value: any[]) => {
     setSelectedRole(value);
   };
 
@@ -220,19 +324,20 @@ const Roles = () => {
   };
 
   return (
-    <div className={classes.wrapper}>
+    <Wrapper className={classes.wrapper} hasPermission={hasPermission}>
       <AttuGrid
         toolbarConfigs={toolbarConfigs}
         colDefinitions={colDefinitions}
         rows={result}
         rowCount={total}
-        primaryKey="name"
+        primaryKey="roleName"
         showPagination={true}
         selected={selectedRole}
         setSelected={handleSelectChange}
         page={currentPage}
         onPageChange={handlePageChange}
         rowsPerPage={pageSize}
+        rowHeight={69}
         setRowsPerPage={handlePageSize}
         isLoading={loading}
         order={order}
@@ -240,7 +345,7 @@ const Roles = () => {
         handleSort={handleGridSort}
         labelDisplayedRows={getLabelDisplayedRows(userTrans('roles'))}
       />
-    </div>
+    </Wrapper>
   );
 };
 

+ 33 - 37
client/src/pages/user/Types.ts

@@ -1,10 +1,8 @@
 import { Option as RoleOption } from '@/components/customSelector/Types';
-
-export interface UserData {
-  name: string;
-  roleName?: string;
-  roles: string[];
-}
+import type {
+  DBCollectionsPrivileges,
+  RBACOptions,
+} from '@server/types/users.type';
 
 export interface CreateUserParams {
   username: string;
@@ -46,16 +44,9 @@ export interface DeleteUserParams {
   username: string;
 }
 
-export interface Privilege {
-  roleName: string;
-  object: string;
-  objectName: string;
-  privilegeName: string;
-}
-
 export interface CreateRoleParams {
   roleName: string;
-  privileges: Privilege[];
+  privileges: DBCollectionsPrivileges;
 }
 
 export interface CreatePrivilegeGroupParams {
@@ -63,15 +54,16 @@ export interface CreatePrivilegeGroupParams {
   privileges: string[];
 }
 
-export interface RoleData {
-  name: string;
-  privileges: Privilege[];
-}
+export type RoleData = {
+  roleName: string;
+  privileges: DBCollectionsPrivileges;
+};
 
 export interface CreateRoleProps {
   onUpdate: (data: { data: CreateRoleParams; isEditing: boolean }) => void;
   handleClose: () => void;
-  role?: RoleData;
+  role: RoleData;
+  sameAs?: boolean;
 }
 
 export interface DeleteRoleParams {
@@ -86,29 +78,33 @@ export interface AssignRoleParams {
 
 export interface UnassignRoleParams extends AssignRoleParams {}
 
-export type RBACObject = 'Global' | 'Collection' | 'User';
+export type CollectionOption = {
+  name: string;
+  value: string;
+};
 
-export interface PrivilegeOptionsProps {
-  options: string[];
-  selection: Privilege[];
-  onChange: (selection: Privilege[]) => void;
-  roleName: string;
-  object: RBACObject;
-  objectName?: string;
-  title: string;
+export type DBOption = {
+  name: string;
+  value: string;
+};
+
+export interface DBCollectionsSelectorProps {
+  selected: DBCollectionsPrivileges; // Current selected DBs and their collections with privileges
+  setSelected: (
+    value:
+      | DBCollectionsPrivileges
+      | ((prev: DBCollectionsPrivileges) => DBCollectionsPrivileges)
+  ) => void;
+  // Callback to update selected state
+  options: {
+    rbacOptions: RBACOptions; // Available RBAC options (privileges)
+    dbOptions: DBOption[]; // Available databases
+  };
 }
 
 export interface PrivilegeGrpOptionsProps {
   options: string[];
   selection: string[];
-  onChange: (selection: string[]) => void;
+  onChange: (data: string[]) => void;
   group_name: string;
 }
-
-export type RBACOptions = {
-  GlobalPrivileges: Record<string, unknown>;
-  CollectionPrivileges: Record<string, unknown>;
-  RbacObjects: Record<string, unknown>;
-  UserPrivileges: Record<string, unknown>;
-  Privileges: Record<string, unknown>;
-};

+ 70 - 64
client/src/pages/user/User.tsx

@@ -8,10 +8,10 @@ import {
   CreateUserParams,
   DeleteUserParams,
   UpdateUserParams,
-  UserData,
   UpdateUserRoleParams,
 } from './Types';
 import DeleteTemplate from '@/components/customDialog/DeleteDialogTemplate';
+import Wrapper from '@/components/layout/Wrapper';
 import { rootContext } from '@/context';
 import { useNavigationHook, usePaginationHook } from '@/hooks';
 import CreateUser from './dialogs/CreateUserDialog';
@@ -19,6 +19,7 @@ import UpdateUserRole from './dialogs/UpdateUserRole';
 import UpdateUser from './dialogs/UpdateUserPassDialog';
 import { ALL_ROUTER_TYPES } from '@/router/consts';
 import { makeStyles } from '@mui/styles';
+import type { UserWithRoles } from '@server/types';
 
 const useStyles = makeStyles((theme: Theme) => ({
   wrapper: {
@@ -28,36 +29,34 @@ const useStyles = makeStyles((theme: Theme) => ({
 
 const Users = () => {
   useNavigationHook(ALL_ROUTER_TYPES.USER);
+  // styles
   const classes = useStyles();
 
-  const [users, setUsers] = useState<UserData[]>([]);
-  const [selectedUser, setSelectedUser] = useState<UserData[]>([]);
+  // ui states
+  const [users, setUsers] = useState<UserWithRoles[]>([]);
+  const [selectedUser, setSelectedUser] = useState<UserWithRoles[]>([]);
+  const [loading, setLoading] = useState(true);
+  const [hasPermission, setHasPermission] = useState(true);
+  // context
   const { setDialog, handleCloseDialog, openSnackBar } =
     useContext(rootContext);
+  // i18n
   const { t: successTrans } = useTranslation('success');
   const { t: userTrans } = useTranslation('user');
   const { t: btnTrans } = useTranslation('btn');
   const { t: dialogTrans } = useTranslation('dialog');
 
   const fetchUsers = async () => {
-    const res = await UserService.getUsers();
-    const roles = await UserService.getRoles();
+    setLoading(true);
+    try {
+      const res = await UserService.getUsers();
 
-    setUsers(
-      res.usernames.map((v: string) => {
-        const name = v;
-        const rolesByName = roles.results.filter(r =>
-          r.users.map((u: any) => u.name).includes(name)
-        );
-        const originRoles =
-          v === 'root' ? ['admin'] : rolesByName.map(r => r.role.name);
-        return {
-          name: v,
-          role: originRoles.join(' , '),
-          roles: originRoles,
-        };
-      })
-    );
+      setUsers(res);
+    } catch (error) {
+      setHasPermission(false);
+    } finally {
+      setLoading(false);
+    }
   };
 
   const {
@@ -103,7 +102,7 @@ const Users = () => {
   const handleDelete = async () => {
     for (const user of selectedUser) {
       const param: DeleteUserParams = {
-        username: user.name,
+        username: user.username,
       };
       await UserService.deleteUser(param);
     }
@@ -126,8 +125,8 @@ const Users = () => {
               <CreateUser
                 handleCreate={handleCreate}
                 handleClose={handleCloseDialog}
-                roleOptions={roles.results.map((r: any) => {
-                  return { label: r.role.name, value: r.role.name };
+                roleOptions={roles.map(r => {
+                  return { label: r.roleName, value: r.roleName };
                 })}
               />
             ),
@@ -137,6 +136,34 @@ const Users = () => {
       icon: 'add',
     },
 
+    {
+      type: 'button',
+      btnVariant: 'text',
+      btnColor: 'secondary',
+      label: userTrans('editPassword'),
+      onClick: async () => {
+        setDialog({
+          open: true,
+          type: 'custom',
+          params: {
+            component: (
+              <UpdateUser
+                username={selectedUser[0]!.username}
+                handleUpdate={handleUpdate}
+                handleClose={handleCloseDialog}
+              />
+            ),
+          },
+        });
+      },
+      icon: 'edit',
+      disabled: () =>
+        selectedUser.length === 0 ||
+        selectedUser.length > 1 ||
+        selectedUser.findIndex(v => v.username === 'root') > -1,
+      disabledTooltip: userTrans('deleteEditRoleTip'),
+    },
+
     {
       type: 'button',
       btnVariant: 'text',
@@ -149,11 +176,12 @@ const Users = () => {
           params: {
             component: (
               <UpdateUserRole
-                username={selectedUser[0]!.name}
+                username={selectedUser[0]!.username}
                 onUpdate={onUpdate}
                 handleClose={handleCloseDialog}
                 roles={
-                  users.filter(u => u.name === selectedUser[0].name)[0].roles
+                  users.filter(u => u.username === selectedUser[0].username)[0]
+                    .roles
                 }
               />
             ),
@@ -164,7 +192,7 @@ const Users = () => {
       disabled: () =>
         selectedUser.length === 0 ||
         selectedUser.length > 1 ||
-        selectedUser.findIndex(v => v.name === 'root') > -1,
+        selectedUser.findIndex(v => v.username === 'root') > -1,
       disabledTooltip: userTrans('deleteEditRoleTip'),
     },
 
@@ -191,7 +219,7 @@ const Users = () => {
       label: btnTrans('drop'),
       disabled: () =>
         selectedUser.length === 0 ||
-        selectedUser.findIndex(v => v.name === 'root') > -1,
+        selectedUser.findIndex(v => v.username === 'root') > -1,
       disabledTooltip: userTrans('deleteTip'),
       icon: 'delete',
     },
@@ -199,50 +227,28 @@ const Users = () => {
 
   const colDefinitions: ColDefinitionsType[] = [
     {
-      id: 'name',
+      id: 'username',
       align: 'left',
       sortType: 'string',
       disablePadding: false,
       label: userTrans('user'),
     },
     {
-      id: 'role',
+      id: 'roles',
       align: 'left',
-      sortType: 'string',
-      disablePadding: false,
+      notSort: true,
+      disablePadding: true,
       label: userTrans('role'),
-    },
-    {
-      id: 'action',
-      disablePadding: false,
-      label: 'Action',
-      showActionCell: true,
-      sortBy: 'action',
-      actionBarConfigs: [
-        {
-          onClick: (e: React.MouseEvent, row: UserData) => {
-            setDialog({
-              open: true,
-              type: 'custom',
-              params: {
-                component: (
-                  <UpdateUser
-                    username={row.name}
-                    handleUpdate={handleUpdate}
-                    handleClose={handleCloseDialog}
-                  />
-                ),
-              },
-            });
-          },
-          linkButton: true,
-          text: 'Update password',
-        },
-      ],
+      formatter(rowData, cellData) {
+        return rowData.username === 'root' ? 'admin' : cellData.join(', ');
+      },
+      getStyle: () => {
+        return { width: '80%' };
+      },
     },
   ];
 
-  const handleSelectChange = (value: UserData[]) => {
+  const handleSelectChange = (value: UserWithRoles[]) => {
     setSelectedUser(value);
   };
 
@@ -255,13 +261,13 @@ const Users = () => {
   };
 
   return (
-    <div className={classes.wrapper}>
+    <Wrapper className={classes.wrapper} hasPermission={hasPermission}>
       <AttuGrid
         toolbarConfigs={toolbarConfigs}
         colDefinitions={colDefinitions}
         rows={result}
         rowCount={total}
-        primaryKey="name"
+        primaryKey="username"
         showPagination={true}
         selected={selectedUser}
         setSelected={handleSelectChange}
@@ -269,12 +275,12 @@ const Users = () => {
         onPageChange={handlePageChange}
         rowsPerPage={pageSize}
         setRowsPerPage={handlePageSize}
-        // isLoading={loading}
+        isLoading={loading}
         order={order}
         orderBy={orderBy}
         handleSort={handleGridSort}
       />
-    </div>
+    </Wrapper>
   );
 };
 

+ 545 - 0
client/src/pages/user/dialogs/DBCollectionSelector.tsx

@@ -0,0 +1,545 @@
+import { useState, useCallback, useEffect } from 'react';
+import {
+  TextField,
+  Typography,
+  FormControlLabel,
+  Checkbox,
+  Tabs,
+  Tab,
+  Radio,
+} from '@mui/material';
+import Autocomplete from '@mui/material/Autocomplete';
+import { CollectionService } from '@/http';
+import { useTranslation } from 'react-i18next';
+import { useDBCollectionSelectorStyle } from './styles';
+import type {
+  DBOption,
+  CollectionOption,
+  DBCollectionsSelectorProps,
+} from '../Types';
+import type { DBCollectionsPrivileges, RBACOptions } from '@server/types';
+
+export default function DBCollectionsSelector(
+  props: DBCollectionsSelectorProps
+) {
+  // Props
+  const { selected, setSelected, options } = props;
+  const { rbacOptions, dbOptions } = options;
+  // Styles
+  const classes = useDBCollectionSelectorStyle();
+  // i18n
+  const { t: searchTrans } = useTranslation('search');
+  const { t: userTrans } = useTranslation('user');
+
+  // UI states
+  const [selectedDB, setSelectedDB] = useState<DBOption | null>(null);
+  const [selectedCollection, setSelectedCollection] = useState<string>('*');
+  const [collectionOptions, setCollectionOptions] = useState<
+    CollectionOption[]
+  >([]);
+  const [loading, setLoading] = useState(false);
+  const [tabValue, setTabValue] = useState(0); // Tab index
+  const [privilegeOptionType, setPrivilegeOptionType] = useState<
+    'group' | 'custom'
+  >('group'); // Type of privilege options
+
+  // Fetch collections when selected DB changes
+  const fetchCollections = useCallback(async (dbName: string) => {
+    // const
+    const ALL_COLLECTIONS = { name: '*', value: '*' };
+    setLoading(true);
+    try {
+      let options: CollectionOption[] = [];
+      if (dbName === '*') {
+        options = [ALL_COLLECTIONS];
+      } else {
+        const res = await CollectionService.getCollectionsNames({
+          db_name: dbName,
+        });
+        options = res.map(c => ({ name: c, value: c }));
+        options.unshift(ALL_COLLECTIONS);
+      }
+      setCollectionOptions(options);
+    } catch (err) {
+      console.error(err);
+    } finally {
+      setLoading(false);
+    }
+  }, []);
+
+  // Initialize selectedDB and fetch collections when dbOptions changes
+  useEffect(() => {
+    if (dbOptions.length > 0 && selectedDB === null) {
+      const initialDB = dbOptions[0];
+      setSelectedDB(initialDB);
+      fetchCollections(initialDB.value);
+    }
+  }, [dbOptions, selectedDB, fetchCollections]);
+
+  // Handle DB selection
+  const handleDBChange = (db: DBOption) => {
+    setSelectedDB(db);
+    setSelected(prevSelected => {
+      const newSelected = { ...prevSelected } as DBCollectionsPrivileges;
+      if (!newSelected[db.value]) {
+        newSelected[db.value] = { collections: {} };
+      }
+      setSelectedCollection('*');
+      return newSelected;
+    });
+    fetchCollections(db.value);
+  };
+
+  // Handle collection selection
+  const handleCollectionChange = (collection: CollectionOption | null) => {
+    if (collection) {
+      setSelectedCollection(collection.value);
+    }
+  };
+
+  // Handle privilege change for a collection
+  const handlePrivilegeChange = (
+    collectionValue: string,
+    privilegeName: string,
+    checked: boolean,
+    dbValue: string
+  ) => {
+    const selectedDBValue = dbValue;
+    if (!selectedDBValue) return;
+
+    const newSelected = { ...selected };
+    if (!newSelected[selectedDBValue]) {
+      newSelected[selectedDBValue] = { collections: {} };
+    }
+    if (!newSelected[selectedDBValue].collections[collectionValue]) {
+      newSelected[selectedDBValue].collections[collectionValue] = {};
+    }
+    newSelected[selectedDBValue].collections[collectionValue][privilegeName] =
+      checked;
+    setSelected(newSelected);
+  };
+
+  // Handle select all privileges in a category
+  const handleSelectAll = (
+    category: keyof RBACOptions,
+    collectionValue: string,
+    checked: boolean,
+    dbValue: string
+  ) => {
+    const selectedDBValue = dbValue;
+    if (!selectedDBValue) return;
+
+    const newSelected = { ...selected };
+    if (!newSelected[selectedDBValue]) {
+      newSelected[selectedDBValue] = { collections: {} };
+    }
+    if (!newSelected[selectedDBValue].collections[collectionValue]) {
+      newSelected[selectedDBValue].collections[collectionValue] = {};
+    }
+    Object.keys(rbacOptions[category]).forEach(privilegeName => {
+      newSelected[selectedDBValue].collections[collectionValue][privilegeName] =
+        checked;
+    });
+    setSelected(newSelected);
+  };
+
+  // Check if all privileges in a category are selected
+  const isCategoryAllSelected = (
+    category: keyof RBACOptions,
+    collectionValue: string,
+    dbValue: string
+  ) => {
+    const selectedDBValue = dbValue || selectedDB?.value;
+    if (!selectedDBValue) return false;
+
+    const categoryPrivileges = rbacOptions[category];
+    return Object.keys(categoryPrivileges).every(
+      privilegeName =>
+        selected[selectedDBValue] &&
+        selected[selectedDBValue].collections &&
+        selected[selectedDBValue].collections[collectionValue]?.[privilegeName]
+    );
+  };
+
+  // Check if some privileges in a category are selected
+  const isCategorySomeSelected = (
+    category: keyof RBACOptions,
+    collectionValue: string,
+    dbValue: string
+  ) => {
+    const selectedDBValue = dbValue;
+    if (!selectedDBValue) return false;
+
+    const categoryPrivileges = rbacOptions[category];
+    const someSelected = Object.keys(categoryPrivileges).some(
+      privilegeName =>
+        selected[selectedDBValue] &&
+        selected[selectedDBValue].collections &&
+        selected[selectedDBValue].collections[collectionValue]?.[privilegeName]
+    );
+    const allSelected = isCategoryAllSelected(
+      category,
+      collectionValue,
+      dbValue
+    );
+    return someSelected && !allSelected;
+  };
+
+  // Calculate the number of selected privileges for a collection
+  const getSelectedPrivilegesCount = (
+    collectionValue: string,
+    privileges: [string, Record<string, string>][]
+  ): number => {
+    const selectedDBValue = selectedDB?.value;
+    if (!selectedDBValue) return 0;
+
+    const collectionPrivileges =
+      selected[selectedDBValue]?.collections?.[collectionValue] || {};
+
+    // Count the number of privileges that are selected for the collection
+    const selectedCount = privileges.reduce(
+      (total, [_, categoryPrivileges]) => {
+        return (
+          total +
+          Object.keys(categoryPrivileges).reduce((total, privilegeName) => {
+            return total + (collectionPrivileges[privilegeName] ? 1 : 0);
+          }, 0)
+        );
+      },
+      0
+    );
+
+    return selectedCount;
+  };
+
+  // Calculate the total number of privileges for a collection
+  const getTotalPrivilegesCount = (
+    privileges: [string, Record<string, string>][]
+  ) => {
+    const total = privileges.reduce((total, [_, categoryPrivileges]) => {
+      return total + Object.keys(categoryPrivileges).length;
+    }, 0);
+
+    return total;
+  };
+
+  // extract privileges options
+  const globalPrivileges = [
+    'DatabasePrivileges',
+    'ResourceManagementPrivileges',
+    'RBACPrivileges',
+  ];
+
+  const rbacEntries = Object.entries(rbacOptions) as [
+    keyof RBACOptions,
+    Record<string, string>
+  ][];
+  const databasePrivilegeOptions = rbacEntries.filter(([category]) => {
+    return (
+      category === 'DatabasePrivileges' || category == 'DatabasePrivilegeGroups'
+    );
+  });
+  const collectionPrivilegeOptions = rbacEntries.filter(([category]) => {
+    return (
+      !globalPrivileges.includes(category) &&
+      category !== 'ClusterPrivilegeGroups' &&
+      category !== 'DatabasePrivilegeGroups'
+    );
+  });
+  const clusterPrivileges = rbacEntries.filter(([category]) => {
+    return (
+      category === 'ResourceManagementPrivileges' ||
+      category === 'RBACPrivileges' ||
+      category === 'ClusterPrivilegeGroups' ||
+      category === 'CustomPrivilegeGroups'
+    );
+  });
+
+  const collectionTypeOptions = collectionPrivilegeOptions.filter(c => {
+    return privilegeOptionType === 'group'
+      ? c[0].includes('Groups')
+      : !c[0].includes('Groups');
+  });
+
+  return (
+    <div className={classes.root}>
+      {/* Privilege type toggle */}
+      <div className={classes.toggle}>
+        <label className="toggle-label">
+          <Radio
+            checked={privilegeOptionType === 'group'}
+            onChange={() => setPrivilegeOptionType('group')}
+            value="group"
+            name="group"
+            size="small"
+            inputProps={{ 'aria-label': 'group' }}
+          />
+          {userTrans('privilegeGroups')}
+        </label>
+        <label className="toggle-label">
+          <Radio
+            checked={privilegeOptionType === 'custom'}
+            onChange={() => setPrivilegeOptionType('custom')}
+            value="custom"
+            name="custom"
+            size="small"
+            inputProps={{ 'aria-label': 'custom' }}
+          />
+          {userTrans('privileges')}
+        </label>
+      </div>
+      {/* Tabs for cluster, Database, Collection */}
+      <Tabs
+        value={tabValue}
+        onChange={(event, newValue) => setTabValue(newValue)}
+        aria-label="tabs"
+      >
+        <Tab label={userTrans('collection')} sx={{ textTransform: 'none' }} />
+        <Tab label={userTrans('database')} sx={{ textTransform: 'none' }} />
+        <Tab label={userTrans('cluster')} sx={{ textTransform: 'none' }} />
+      </Tabs>
+
+      {tabValue === 2 && (
+        <PrivilegeSelector
+          privilegeOptions={clusterPrivileges}
+          selected={selected}
+          selectedDB={{ name: userTrans('allDatabases'), value: '*' }}
+          selectedCollection={'*'}
+          handlePrivilegeChange={handlePrivilegeChange}
+          isCategoryAllSelected={isCategoryAllSelected}
+          isCategorySomeSelected={isCategorySomeSelected}
+          handleSelectAll={handleSelectAll}
+          privilegeOptionType={privilegeOptionType}
+        />
+      )}
+
+      {tabValue === 1 && (
+        <PrivilegeSelector
+          privilegeOptions={databasePrivilegeOptions}
+          selected={selected}
+          selectedDB={{ name: userTrans('allDatabases'), value: '*' }}
+          selectedCollection={'*'}
+          handlePrivilegeChange={handlePrivilegeChange}
+          isCategoryAllSelected={isCategoryAllSelected}
+          isCategorySomeSelected={isCategorySomeSelected}
+          handleSelectAll={handleSelectAll}
+          privilegeOptionType={privilegeOptionType}
+        />
+      )}
+
+      {tabValue === 0 && (
+        <div>
+          <div className={classes.dbCollections}>
+            <Autocomplete
+              className={classes.selectorDB}
+              options={dbOptions}
+              loading={loading}
+              value={selectedDB || null}
+              onChange={(_, value) => {
+                if (!value) return;
+                handleDBChange(value);
+              }}
+              getOptionLabel={option => option.name}
+              isOptionEqualToValue={(option, value) =>
+                option.value === value.value
+              }
+              renderInput={params => (
+                <TextField
+                  {...params}
+                  label={userTrans('databases')}
+                  variant="filled"
+                />
+              )}
+              noOptionsText={
+                loading ? searchTrans('loading') : searchTrans('noOptions')
+              }
+            />
+            <Autocomplete
+              className={classes.selectorCollection}
+              options={collectionOptions}
+              loading={loading}
+              value={
+                collectionOptions.find(
+                  option => option.value === selectedCollection
+                ) || null
+              }
+              onChange={(_, value) => {
+                if (!value) return;
+                handleCollectionChange(value);
+              }}
+              getOptionLabel={option => {
+                const selectedCount = getSelectedPrivilegesCount(
+                  option.value,
+                  collectionTypeOptions
+                );
+                const totalCount = getTotalPrivilegesCount(
+                  collectionTypeOptions
+                );
+                return `${option.name} (${selectedCount}/${totalCount})`;
+              }}
+              isOptionEqualToValue={(option, value) =>
+                option.value === value.value
+              }
+              renderInput={params => {
+                return (
+                  <TextField
+                    {...params}
+                    label={userTrans('collections')}
+                    variant="filled"
+                  />
+                );
+              }}
+              noOptionsText={
+                loading ? searchTrans('loading') : searchTrans('noOptions')
+              }
+            />
+          </div>
+
+          <PrivilegeSelector
+            privilegeOptions={collectionPrivilegeOptions}
+            selected={selected}
+            selectedDB={selectedDB}
+            selectedCollection={selectedCollection}
+            handlePrivilegeChange={handlePrivilegeChange}
+            isCategoryAllSelected={isCategoryAllSelected}
+            isCategorySomeSelected={isCategorySomeSelected}
+            handleSelectAll={handleSelectAll}
+            privilegeOptionType={privilegeOptionType}
+          />
+        </div>
+      )}
+    </div>
+  );
+}
+
+// PriviligeSelector
+const PrivilegeSelector = (props: {
+  privilegeOptions: [keyof RBACOptions, Record<string, string>][];
+  selected: DBCollectionsPrivileges;
+  selectedDB: DBOption | null;
+  selectedCollection: string;
+  privilegeOptionType: 'group' | 'custom';
+  handlePrivilegeChange: (
+    collectionValue: string,
+    privilegeName: string,
+    checked: boolean,
+    dbValue: string
+  ) => void;
+  isCategoryAllSelected: (
+    category: keyof RBACOptions,
+    collectionValue: string,
+    dbValue: string
+  ) => boolean;
+  isCategorySomeSelected: (
+    category: keyof RBACOptions,
+    collectionValue: string,
+    dbValue: string
+  ) => boolean;
+  handleSelectAll: (
+    category: keyof RBACOptions,
+    collectionValue: string,
+    checked: boolean,
+    dbValue: string
+  ) => void;
+}) => {
+  // props
+  const {
+    selected,
+    selectedDB,
+    selectedCollection,
+    privilegeOptions,
+    handlePrivilegeChange,
+    handleSelectAll,
+    isCategoryAllSelected,
+    isCategorySomeSelected,
+    privilegeOptionType,
+  } = props;
+
+  // style
+  const classes = useDBCollectionSelectorStyle();
+
+  // i18n
+  const { t: userTrans } = useTranslation('user');
+
+  return (
+    <div className={classes.privileges}>
+      {selectedDB && selectedCollection && (
+        <div>
+          {privilegeOptions
+            .filter(v => {
+              if (privilegeOptionType === 'group') {
+                return v[0].includes('Groups');
+              } else {
+                return !v[0].includes('Groups');
+              }
+            })
+            .map(([category, categoryPrivileges]) => (
+              <div key={category}>
+                <div className={classes.categoryHeader}>
+                  <FormControlLabel
+                    control={
+                      <Checkbox
+                        checked={isCategoryAllSelected(
+                          category,
+                          selectedCollection,
+                          selectedDB.value
+                        )}
+                        indeterminate={isCategorySomeSelected(
+                          category,
+                          selectedCollection,
+                          selectedDB.value
+                        )}
+                        onChange={e =>
+                          handleSelectAll(
+                            category,
+                            selectedCollection,
+                            e.target.checked,
+                            selectedDB.value
+                          )
+                        }
+                        size="small"
+                        className={classes.selectAllCheckbox}
+                        title={userTrans('selectAll')}
+                      />
+                    }
+                    label={userTrans(category)}
+                  />
+                </div>
+                <div className={classes.categoryBody}>
+                  {Object.entries(categoryPrivileges).map(([privilegeName]) => (
+                    <FormControlLabel
+                      className={classes.privilegeBody}
+                      key={privilegeName}
+                      control={
+                        <Checkbox
+                          className={classes.checkbox}
+                          checked={
+                            (selected[selectedDB.value] &&
+                              selected[selectedDB.value].collections &&
+                              selected[selectedDB.value].collections[
+                                selectedCollection
+                              ]?.[privilegeName]) ||
+                            false
+                          }
+                          size="small"
+                          onChange={e =>
+                            handlePrivilegeChange(
+                              selectedCollection,
+                              privilegeName,
+                              e.target.checked,
+                              selectedDB.value
+                            )
+                          }
+                        />
+                      }
+                      label={privilegeName}
+                    />
+                  ))}
+                </div>
+              </div>
+            ))}
+        </div>
+      )}
+    </div>
+  );
+};

+ 1 - 0
client/src/pages/user/dialogs/PrivilegeGroupOptions.tsx

@@ -25,6 +25,7 @@ const PrivilegeGroupOptions: FC<PrivilegeGrpOptionsProps> = ({
   options,
   selection,
   onChange,
+  group_name,
 }) => {
   const classes = useStyles();
 

+ 0 - 82
client/src/pages/user/dialogs/PrivilegeOptions.tsx

@@ -1,82 +0,0 @@
-import {
-  Theme,
-  Typography,
-  Checkbox,
-  FormGroup,
-  FormControlLabel,
-} from '@mui/material';
-import { FC } from 'react';
-import { makeStyles } from '@mui/styles';
-import type { Privilege, PrivilegeOptionsProps } from '../Types';
-
-const useStyles = makeStyles((theme: Theme) => ({
-  checkBox: {
-    width: theme.spacing(24),
-  },
-  formGrp: {
-    marginBottom: theme.spacing(2),
-  },
-  subTitle: {
-    marginBottom: theme.spacing(0.5),
-  },
-}));
-
-const PrivilegeOptions: FC<PrivilegeOptionsProps> = ({
-  options,
-  selection,
-  onChange,
-  title,
-  roleName,
-  object,
-  objectName = '*',
-}) => {
-  const classes = useStyles();
-
-  return (
-    <>
-      <Typography variant="h6" component="h6" className={classes.subTitle}>
-        {title}
-      </Typography>
-      <FormGroup row className={classes.formGrp}>
-        {options.map((r: string) => (
-          <FormControlLabel
-            control={
-              <Checkbox
-                onChange={(
-                  e: React.ChangeEvent<HTMLInputElement>,
-                  checked: boolean
-                ) => {
-                  let newSelection = [...selection];
-
-                  if (!checked) {
-                    newSelection = newSelection.filter(
-                      (n: Privilege) => n.privilegeName !== r
-                    );
-                  } else {
-                    newSelection.push({
-                      privilegeName: r,
-                      object: object,
-                      objectName: objectName,
-                      roleName: roleName,
-                    });
-                  }
-                  onChange(newSelection);
-                }}
-              />
-            }
-            key={r}
-            label={r}
-            value={r}
-            checked={
-              selection.filter((s: Privilege) => s.privilegeName === r).length >
-              0
-            }
-            className={classes.checkBox}
-          />
-        ))}
-      </FormGroup>
-    </>
-  );
-};
-
-export default PrivilegeOptions;

+ 0 - 1
client/src/pages/user/dialogs/UpdatePrivilegeGroupDialog.tsx

@@ -19,7 +19,6 @@ import { CreatePrivilegeGroupParams } from '../Types';
 import PrivilegeGroupOptions from './PrivilegeGroupOptions';
 import { makeStyles } from '@mui/styles';
 import { PrivilegeGroup } from '@server/types';
-import { Opacity } from '@mui/icons-material';
 
 const useStyles = makeStyles((theme: Theme) => ({
   input: {

+ 107 - 106
client/src/pages/user/dialogs/UpdateRoleDialog.tsx

@@ -1,90 +1,141 @@
-import { Theme, Typography } from '@mui/material';
+import { Theme } from '@mui/material';
 import { FC, useMemo, useState, useEffect } from 'react';
 import { useTranslation } from 'react-i18next';
 import DialogTemplate from '@/components/customDialog/DialogTemplate';
 import CustomInput from '@/components/customInput/CustomInput';
 import { useFormValidation } from '@/hooks';
 import { formatForm } from '@/utils';
-import { UserService } from '@/http';
+import { UserService, DatabaseService } from '@/http';
 import { makeStyles } from '@mui/styles';
-import PrivilegeOptions from './PrivilegeOptions';
+import DBCollectionSelector from './DBCollectionSelector';
 import type { ITextfieldConfig } from '@/components/customInput/Types';
-import type {
-  CreateRoleProps,
-  CreateRoleParams,
-  PrivilegeOptionsProps,
-  RBACOptions,
-} from '../Types';
+import type { CreateRoleProps, CreateRoleParams, DBOption } from '../Types';
+import type { DBCollectionsPrivileges, RBACOptions } from '@server/types';
 
 const useStyles = makeStyles((theme: Theme) => ({
   input: {
     margin: theme.spacing(1, 0, 0.5),
   },
   dialogWrapper: {
-    maxWidth: theme.spacing(88),
-  },
-  checkBox: {
-    width: theme.spacing(24),
-  },
-  formGrp: {
-    marginBottom: theme.spacing(2),
+    width: '66vw',
+    maxWidth: '66vw',
   },
   subTitle: {
     marginBottom: theme.spacing(0.5),
   },
 }));
 
-const UpdateRoleDialog: FC<CreateRoleProps> = ({
-  onUpdate,
-  handleClose,
-  role = { name: '', privileges: [] },
-}) => {
+const DEFAULT_DB_Privileges: DBCollectionsPrivileges = {
+  '*': {
+    collections: {
+      '*': {},
+    },
+  },
+};
+
+const UpdateRoleDialog: FC<CreateRoleProps> = props => {
+  const {
+    role = {
+      roleName: '',
+      privileges: DEFAULT_DB_Privileges,
+    },
+    handleClose,
+    onUpdate,
+    sameAs,
+  } = props;
+  // i18n
   const { t: userTrans } = useTranslation('user');
   const { t: btnTrans } = useTranslation('btn');
   const { t: warningTrans } = useTranslation('warning');
-  const [rbacOptions, setRbacOptions] = useState<RBACOptions>({
-    GlobalPrivileges: {},
-    CollectionPrivileges: {},
-    RbacObjects: {},
-    UserPrivileges: {},
-    Privileges: {},
+
+  // const
+  const ALL_DB = { name: userTrans('allDatabases'), value: '*' };
+
+  // UI states
+  const [options, setOptions] = useState<{
+    rbacOptions: RBACOptions; // Available RBAC options (privileges)
+    dbOptions: DBOption[]; // Available databases
+  }>({
+    rbacOptions: {} as RBACOptions,
+    dbOptions: [],
   });
 
-  const fetchRBAC = async () => {
-    const rbacOptions = await UserService.getRBAC();
+  const [selected, setSelected] = useState<DBCollectionsPrivileges>(
+    role.privileges
+  );
 
-    setRbacOptions(rbacOptions);
-  };
+  // Form state
+  const [form, setForm] = useState<CreateRoleParams>({
+    roleName: role.roleName,
+    privileges: selected,
+  });
 
-  const isEditing = role.name !== '';
+  // update form if selected changes
+  useEffect(() => {
+    setForm(v => ({
+      ...v,
+      privileges: selected,
+    }));
+  }, [selected]);
 
   useEffect(() => {
-    fetchRBAC();
+    const fetchData = async () => {
+      try {
+        const [dbResponse, rbacResponse] = await Promise.all([
+          DatabaseService.listDatabases(),
+          UserService.getRBAC(),
+        ]);
+
+        const dbOptions = dbResponse.map(db => ({
+          name: db.name,
+          value: db.name,
+        }));
+        dbOptions.unshift(ALL_DB);
+
+        setOptions({
+          rbacOptions: rbacResponse,
+          dbOptions,
+        });
+      } catch (err) {
+        console.error('Failed to fetch data:', err);
+      }
+    };
+
+    fetchData();
   }, []);
 
-  const [form, setForm] = useState<CreateRoleParams>({
-    roleName: role.name,
-    privileges: JSON.parse(JSON.stringify(role.privileges)),
-  });
+  // Check if editing an existing role
+  const isEditing = role.roleName !== '' && !sameAs;
 
+  // Form validation
   const checkedForm = useMemo(() => {
     return formatForm(form);
   }, [form]);
   const { validation, checkIsValid, disabled } = useFormValidation(checkedForm);
 
-  const classes = useStyles();
-
+  // Handle input change
   const handleInputChange = (key: 'roleName', value: string) => {
-    setForm(v => {
-      const newFrom = { ...v, [key]: value };
+    setForm(v => ({
+      ...v,
+      [key]: value,
+    }));
+  };
 
-      // update roleName
-      newFrom.privileges.forEach(p => (p.roleName = value));
+  // Handle create/update role
+  const handleCreateRole = async () => {
+    try {
+      const res = await UserService.updateRolePrivileges(form);
 
-      return newFrom;
-    });
+      onUpdate({ data: form, isEditing });
+    } catch (error) {
+      console.error('Error creating/updating role:', error);
+    }
   };
 
+  // styles
+  const classes = useStyles();
+
+  // Input configurations
   const createConfigs: ITextfieldConfig[] = [
     {
       label: userTrans('role'),
@@ -107,55 +158,14 @@ const UpdateRoleDialog: FC<CreateRoleProps> = ({
     },
   ];
 
-  const handleCreateRole = async () => {
-    if (!isEditing) {
-      await UserService.createRole(form);
-    }
-
-    await UserService.updateRolePrivileges(form);
-
-    onUpdate({ data: form, isEditing: isEditing });
-  };
-
-  const onChange = (newSelection: any) => {
-    setForm(v => {
-      return { ...v, privileges: [...newSelection] };
-    });
-  };
-
-  const optionGroups: PrivilegeOptionsProps[] = [
-    {
-      options: Object.values(rbacOptions.GlobalPrivileges) as string[],
-      object: 'Global',
-      title: userTrans('objectGlobal'),
-      selection: form.privileges,
-      roleName: form.roleName,
-      onChange: onChange,
-    },
-
-    {
-      options: Object.values(rbacOptions.CollectionPrivileges) as string[],
-      title: userTrans('objectCollection'),
-      object: 'Collection',
-      selection: form.privileges,
-      roleName: form.roleName,
-      onChange: onChange,
-    },
-
-    {
-      options: Object.values(rbacOptions.UserPrivileges) as string[],
-      title: userTrans('objectUser'),
-      object: 'User',
-      selection: form.privileges,
-      roleName: form.roleName,
-      onChange: onChange,
-    },
-  ];
-
   return (
     <DialogTemplate
       title={userTrans(
-        isEditing ? 'updateRolePrivilegeTitle' : 'createRoleTitle'
+        isEditing
+          ? 'updateRolePrivilegeTitle'
+          : sameAs
+          ? 'dupicateRoleTitle'
+          : 'createRoleTitle'
       )}
       handleClose={handleClose}
       confirmLabel={btnTrans(isEditing ? 'update' : 'create')}
@@ -173,21 +183,12 @@ const UpdateRoleDialog: FC<CreateRoleProps> = ({
             key={v.label}
           />
         ))}
-        <Typography variant="h5" component="h5" className={classes.subTitle}>
-          {userTrans('privileges')}
-        </Typography>
-
-        {optionGroups.map(o => (
-          <PrivilegeOptions
-            key={o.object}
-            title={o.title}
-            object={o.object}
-            options={o.options}
-            selection={o.selection}
-            roleName={o.roleName}
-            onChange={o.onChange}
-          />
-        ))}
+
+        <DBCollectionSelector
+          selected={selected}
+          setSelected={setSelected}
+          options={options}
+        />
       </>
     </DialogTemplate>
   );

+ 2 - 2
client/src/pages/user/dialogs/UpdateUserRole.tsx

@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
 import DialogTemplate from '@/components/customDialog/DialogTemplate';
 import { UserService } from '@/http';
 import { makeStyles } from '@mui/styles';
-import type { UpdateUserRoleProps, UpdateUserRoleParams } from './Types';
+import type { UpdateUserRoleProps, UpdateUserRoleParams } from '../Types';
 
 const useStyles = makeStyles((theme: Theme) => ({
   input: {
@@ -43,7 +43,7 @@ const UpdateUserRole: FC<UpdateUserRoleProps> = ({
   const fetchAllRoles = async () => {
     const roles = await UserService.getRoles();
 
-    setRoleOptions(roles.results.map(r => r.role.name));
+    setRoleOptions(roles.map(role => role.roleName));
   };
 
   useEffect(() => {

+ 77 - 0
client/src/pages/user/dialogs/styles.ts

@@ -0,0 +1,77 @@
+import { makeStyles } from '@mui/styles';
+import { Theme } from '@mui/material';
+
+export const useDBCollectionSelectorStyle = makeStyles((theme: Theme) => ({
+  root: {
+    display: 'flex',
+    flexDirection: 'column',
+    gap: theme.spacing(1),
+    backgroundColor: theme.palette.background.paper,
+  },
+  dbCollections: {
+    display: 'flex',
+    flexDirection: 'row',
+    gap: theme.spacing(1),
+  },
+  selectorDB: {
+    flex: 1,
+    marginBottom: theme.spacing(2),
+  },
+  selectorCollection: {
+    flex: 1,
+  },
+  categoryHeader: {
+    display: 'flex',
+    alignItems: 'center',
+    padding: theme.spacing(0.5),
+    backgroundColor: theme.palette.action.hover,
+    borderRadius: theme.shape.borderRadius,
+    marginBottom: 0,
+    '& .MuiTypography-root': {
+      fontSize: 14,
+      fontWeight: 600,
+      color: theme.palette.text.primary,
+    },
+  },
+  categoryBody: {
+    padding: theme.spacing(0.5, 1.5),
+    borderRadius: theme.shape.borderRadius,
+    backgroundColor: theme.palette.background.paper,
+  },
+  privilegeTitle: {
+    fontWeight: 600,
+    fontSize: 14,
+    color: theme.palette.text.primary,
+    margin: 0,
+    marginLeft: theme.spacing(-2),
+  },
+  privileges: {
+    display: 'flex',
+    flex: 1,
+    flexDirection: 'column',
+    height: 'auto',
+    minHeight: 200,
+    width: '100%',
+    borderRadius: theme.shape.borderRadius,
+  },
+
+  privilegeBody: {
+    minWidth: 200,
+  },
+
+  selectAllCheckbox: {
+    marginLeft: 8,
+  },
+  checkbox: {},
+  toggle: {
+    fontSize: 13,
+    '& .toggle-label': {
+      padding: 0,
+      marginRight: theme.spacing(1),
+      fontWeight: '400',
+    },
+    '& .MuiRadio-root': {
+      paddingRight: 8,
+    },
+  },
+}));

+ 1 - 1
server/package.json

@@ -13,7 +13,7 @@
   },
   "dependencies": {
     "@json2csv/plainjs": "^7.0.3",
-    "@zilliz/milvus2-sdk-node": "2.5.3",
+    "@zilliz/milvus2-sdk-node": "2.5.5",
     "axios": "^1.7.7",
     "chalk": "4.1.2",
     "class-sanitizer": "^1.0.1",

+ 27 - 0
server/src/collections/collections.controller.ts

@@ -34,6 +34,7 @@ export class CollectionController {
   generateRoutes() {
     // get all collections
     this.router.get('/', this.showCollections.bind(this));
+    this.router.get('/names', this.getCollectionNames.bind(this));
     // get all collections statistics
     this.router.get('/statistics', this.getStatistics.bind(this));
     // index
@@ -158,6 +159,32 @@ export class CollectionController {
     }
   }
 
+  async getCollectionNames(req: Request, res: Response, next: NextFunction) {
+    try {
+      const db_name = req.query?.db_name;
+      const request = {} as any;
+
+      if (db_name) {
+        request.db_name = db_name;
+      }
+
+      const result = await this.collectionsService.showCollections(
+        req.clientId,
+        request
+      );
+      res.send(
+        result.data
+          .sort((a, b) => {
+             // sort by name
+            return a.name.localeCompare(b.name);
+          })
+          .map((item: any) => item.name)
+      );
+    } catch (error) {
+      next(error);
+    }
+  }
+
   async getStatistics(req: Request, res: Response, next: NextFunction) {
     try {
       const result = await this.collectionsService.getStatistics(

+ 1 - 32
server/src/collections/collections.service.ts

@@ -9,7 +9,6 @@ import {
   RenameCollectionReq,
   AlterAliasReq,
   CreateAliasReq,
-  DropAliasReq,
   ShowCollectionsReq,
   ShowCollectionsType,
   DeleteEntitiesReq,
@@ -32,7 +31,6 @@ import {
 } from '@zilliz/milvus2-sdk-node';
 import { Parser } from '@json2csv/plainjs';
 import {
-  throwErrorFromSDK,
   findKeyValue,
   getKeyValueListFromJsonString,
   genRows,
@@ -65,7 +63,6 @@ export class CollectionsService {
   async showCollections(clientId: string, data?: ShowCollectionsReq) {
     const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.showCollections(data);
-    throwErrorFromSDK(res.status);
     return res;
   }
 
@@ -78,7 +75,6 @@ export class CollectionsService {
       data.db_name
     )) as CollectionFullObject[];
 
-    throwErrorFromSDK(res);
     return newCollection[0];
   }
 
@@ -92,7 +88,6 @@ export class CollectionsService {
       collection_name,
       db_name,
     });
-    throwErrorFromSDK(res.status);
     return res;
   }
 
@@ -107,8 +102,6 @@ export class CollectionsService {
       collection_name: data.collection_name,
     });
 
-    throwErrorFromSDK(res.status);
-
     const vectorFields: FieldObject[] = [];
     const scalarFields: FieldObject[] = [];
     const functionFields: FieldObject[] = [];
@@ -200,7 +193,6 @@ export class CollectionsService {
   async renameCollection(clientId: string, data: RenameCollectionReq) {
     const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.renameCollection(data);
-    throwErrorFromSDK(res);
 
     const newCollection = (await this.getAllCollections(
       clientId,
@@ -213,8 +205,7 @@ export class CollectionsService {
 
   async alterCollection(clientId: string, data: AlterCollectionReq) {
     const { milvusClient } = clientCache.get(clientId);
-    const res = await milvusClient.alterCollection(data);
-    throwErrorFromSDK(res);
+    const res = await milvusClient.alterCollectionProperties(data);
 
     const newCollection = (await this.getAllCollections(
       clientId,
@@ -228,14 +219,12 @@ export class CollectionsService {
   async dropCollection(clientId: string, data: DropCollectionReq) {
     const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.dropCollection(data);
-    throwErrorFromSDK(res);
     return res;
   }
 
   async loadCollection(clientId: string, data: LoadCollectionReq) {
     const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.loadCollection(data);
-    throwErrorFromSDK(res);
 
     return data.collection_name;
   }
@@ -243,7 +232,6 @@ export class CollectionsService {
   async loadCollectionAsync(clientId: string, data: LoadCollectionReq) {
     const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.loadCollectionAsync(data);
-    throwErrorFromSDK(res);
 
     return data.collection_name;
   }
@@ -251,7 +239,6 @@ export class CollectionsService {
   async releaseCollection(clientId: string, data: ReleaseLoadCollectionReq) {
     const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.releaseCollection(data);
-    throwErrorFromSDK(res);
 
     // emit update to client
     this.updateCollectionsDetails(
@@ -269,14 +256,12 @@ export class CollectionsService {
   ) {
     const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.getCollectionStatistics(data);
-    throwErrorFromSDK(res.status);
     return res;
   }
 
   async getLoadState(clientId: string, data: GetLoadStateReq) {
     const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.getLoadState(data);
-    throwErrorFromSDK(res.status);
     return res;
   }
 
@@ -307,21 +292,18 @@ export class CollectionsService {
   async insert(clientId: string, data: InsertReq) {
     const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.insert(data);
-    throwErrorFromSDK(res.status);
     return res;
   }
 
   async upsert(clientId: string, data: InsertReq) {
     const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.upsert(data);
-    throwErrorFromSDK(res.status);
     return res;
   }
 
   async deleteEntities(clientId: string, data: DeleteEntitiesReq) {
     const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.deleteEntities(data);
-    throwErrorFromSDK(res.status);
     return res;
   }
 
@@ -354,7 +336,6 @@ export class CollectionsService {
     );
     const after = Date.now();
 
-    throwErrorFromSDK(res.status);
     Object.assign(res, { latency: after - now });
     return res;
   }
@@ -362,7 +343,6 @@ export class CollectionsService {
   async createAlias(clientId: string, data: CreateAliasReq) {
     const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.createAlias(data);
-    throwErrorFromSDK(res);
 
     const newCollection = (await this.getAllCollections(
       clientId,
@@ -376,14 +356,12 @@ export class CollectionsService {
   async alterAlias(clientId: string, data: AlterAliasReq) {
     const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.alterAlias(data);
-    throwErrorFromSDK(res);
     return res;
   }
 
   async dropAlias(clientId: string, collection_name: string, data: any) {
     const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.dropAlias(data);
-    throwErrorFromSDK(res);
 
     const newCollection = (await this.getAllCollections(
       clientId,
@@ -412,7 +390,6 @@ export class CollectionsService {
 
     const after = Date.now();
 
-    throwErrorFromSDK(res.status);
     Object.assign(res, { latency: after - now });
     return res;
   }
@@ -702,14 +679,12 @@ export class CollectionsService {
   async getCompactionState(clientId: string, data: GetCompactionStateReq) {
     const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.getCompactionState(data);
-    throwErrorFromSDK(res.status);
     return res;
   }
 
   async getQuerySegmentInfo(clientId: string, data: GetQuerySegmentInfoReq) {
     const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.getQuerySegmentInfo(data);
-    throwErrorFromSDK(res.status);
     return res;
   }
 
@@ -719,21 +694,18 @@ export class CollectionsService {
   ) {
     const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.getPersistentSegmentInfo(data);
-    throwErrorFromSDK(res.status);
     return res;
   }
 
   async compact(clientId: string, data: CompactReq) {
     const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.compact(data);
-    throwErrorFromSDK(res.status);
     return res;
   }
 
   async hasCollection(clientId: string, data: HasCollectionReq) {
     const { milvusClient } = clientCache.get(clientId);
     const res = await milvusClient.hasCollection(data);
-    throwErrorFromSDK(res.status);
     return res;
   }
 
@@ -778,7 +750,6 @@ export class CollectionsService {
   async createIndex(clientId: string, data: CreateIndexReq) {
     const { milvusClient, indexCache, database } = clientCache.get(clientId);
     const res = await milvusClient.createIndex(data);
-    throwErrorFromSDK(res);
     const key = `${database}/${data.collection_name}`;
     // clear cache;
     indexCache.delete(key);
@@ -790,7 +761,6 @@ export class CollectionsService {
       data.db_name
     )) as CollectionFullObject[];
 
-    throwErrorFromSDK(res);
     return newCollection[0];
   }
 
@@ -847,7 +817,6 @@ export class CollectionsService {
   async dropIndex(clientId: string, data: DropIndexReq) {
     const { milvusClient, indexCache, database } = clientCache.get(clientId);
     const res = await milvusClient.dropIndex(data);
-    throwErrorFromSDK(res);
 
     const key = `${database}/${data.collection_name}`;
 

+ 2 - 8
server/src/database/databases.service.ts

@@ -6,7 +6,6 @@ import {
   DropDatabasesRequest,
   AlterDatabaseRequest,
 } from '@zilliz/milvus2-sdk-node';
-import { throwErrorFromSDK } from '../utils/Error';
 import { clientCache } from '../app';
 import { DatabaseObject } from '../types';
 import { SimpleQueue } from '../utils';
@@ -16,7 +15,6 @@ export class DatabasesService {
     const { milvusClient } = clientCache.get(clientId);
 
     const res = await milvusClient.createDatabase(data);
-    throwErrorFromSDK(res);
     return res;
   }
 
@@ -24,7 +22,6 @@ export class DatabasesService {
     const { milvusClient } = clientCache.get(clientId);
 
     const res = await milvusClient.describeDatabase(data);
-    throwErrorFromSDK(res.status);
     return res as DescribeDatabaseResponse;
   }
 
@@ -55,7 +52,7 @@ export class DatabasesService {
           });
         } catch (e) {
           // ignore
-          console.log('error', e);
+          console.warn('error', e.details);
         }
 
         availableDatabases.push({
@@ -66,14 +63,13 @@ export class DatabasesService {
         });
       } catch (e) {
         // ignore
-        console.log('error', e);
+        console.warn('error', e.details);
       }
     }
 
     // recover current database
     await milvusClient.use({ db_name: database });
 
-    throwErrorFromSDK(res.status);
     return availableDatabases;
   }
 
@@ -81,7 +77,6 @@ export class DatabasesService {
     const { milvusClient } = clientCache.get(clientId);
 
     const res = await milvusClient.dropDatabase(data);
-    throwErrorFromSDK(res);
     return res;
   }
 
@@ -104,7 +99,6 @@ export class DatabasesService {
     const { milvusClient } = clientCache.get(clientId);
 
     const res = await milvusClient.alterDatabase(data);
-    throwErrorFromSDK(res);
     return res;
   }
 }

+ 10 - 4
server/src/middleware/index.ts

@@ -37,7 +37,7 @@ export const ReqHeaderMiddleware = (
 
   if (!bypass && !hasClient) {
     throw HttpErrors(
-      HTTP_STATUS_CODE.FORBIDDEN,
+      HTTP_STATUS_CODE.UNAUTHORIZED,
       'Can not find your connection, please reconnect.'
     );
   } else {
@@ -52,7 +52,6 @@ export const TransformResMiddleware = (
 ) => {
   const oldSend = res.json;
   res.json = data => {
-    // console.log(data); // do something with the data
     const statusCode = data?.statusCode;
     const message = data?.message;
     const error = data?.error;
@@ -75,7 +74,7 @@ export const ErrorMiddleware = (
   res: Response,
   next: NextFunction
 ) => {
-  const statusCode = err.statusCode || 500;
+  let statusCode = err.statusCode || 500;
   if (!isElectron()) {
     console.log(
       chalk.blue.bold(req.method, req.url),
@@ -89,10 +88,17 @@ export const ErrorMiddleware = (
     return next(err);
   }
 
+  // handle error from milvus service
+  switch (err.code) {
+    case 7:
+      statusCode = HTTP_STATUS_CODE.FORBIDDEN;
+      break;
+  }
+
   if (err) {
     res
       .status(statusCode)
-      .json({ message: `${err}`, error: 'Bad Request', statusCode });
+      .json({ message: `${err.details || err.message}`, statusCode });
   }
   next();
 };

+ 8 - 2
server/src/milvus/milvus.service.ts

@@ -6,10 +6,16 @@ import {
   CONNECT_STATUS,
 } from '@zilliz/milvus2-sdk-node';
 import { LRUCache } from 'lru-cache';
-import { DEFAULT_MILVUS_PORT, INDEX_TTL, SimpleQueue } from '../utils';
+import {
+  DEFAULT_MILVUS_PORT,
+  INDEX_TTL,
+  SimpleQueue,
+  HTTP_STATUS_CODE,
+} from '../utils';
 import { clientCache } from '../app';
 import { DescribeIndexRes, AuthReq, AuthObject } from '../types';
 import { cronsManager } from '../crons';
+import HttpErrors from 'http-errors';
 
 export class MilvusService {
   private DEFAULT_DATABASE = 'default';
@@ -88,7 +94,7 @@ export class MilvusService {
       } catch (error) {
         // If the connection fails, clear the cache and throw an error
         clientCache.delete(milvusClient.clientId);
-        throw new Error('Failed to connect to Milvus: ' + error);
+        throw HttpErrors(HTTP_STATUS_CODE.UNAUTHORIZED, error);
       }
 
       // Check the health of the Milvus server

+ 0 - 7
server/src/partitions/partitions.service.ts

@@ -6,7 +6,6 @@ import {
   ReleasePartitionsReq,
   ShowPartitionsReq,
 } from '@zilliz/milvus2-sdk-node';
-import { throwErrorFromSDK } from '../utils/Error';
 import { findKeyValue } from '../utils/Helper';
 import { ROW_COUNT } from '../utils';
 import { clientCache } from '../app';
@@ -40,7 +39,6 @@ export class PartitionsService {
     const { milvusClient } = clientCache.get(clientId);
 
     const res = await milvusClient.showPartitions(data);
-    throwErrorFromSDK(res.status);
     return res;
   }
 
@@ -48,7 +46,6 @@ export class PartitionsService {
     const { milvusClient } = clientCache.get(clientId);
 
     const res = await milvusClient.createPartition(data);
-    throwErrorFromSDK(res);
     return res;
   }
 
@@ -56,7 +53,6 @@ export class PartitionsService {
     const { milvusClient } = clientCache.get(clientId);
 
     const res = await milvusClient.dropPartition(data);
-    throwErrorFromSDK(res);
     return res;
   }
 
@@ -67,7 +63,6 @@ export class PartitionsService {
     const { milvusClient } = clientCache.get(clientId);
 
     const res = await milvusClient.getPartitionStatistics(data);
-    throwErrorFromSDK(res.status);
     return res;
   }
 
@@ -75,7 +70,6 @@ export class PartitionsService {
     const { milvusClient } = clientCache.get(clientId);
 
     const res = await milvusClient.loadPartitions(data);
-    throwErrorFromSDK(res);
     return res;
   }
 
@@ -83,7 +77,6 @@ export class PartitionsService {
     const { milvusClient } = clientCache.get(clientId);
 
     const res = await milvusClient.releasePartitions(data);
-    throwErrorFromSDK(res);
     return res;
   }
 }

+ 40 - 0
server/src/types/users.type.ts

@@ -7,5 +7,45 @@ import {
 
 export type Users = ListCredUsersResponse;
 export type UsersWithRoles = SelectRoleResponse;
+export type UserWithRoles = {
+  username: string;
+  roles: string[];
+};
+
+export type RolesWithPrivileges = {
+  roleName: string;
+  privileges: DBCollectionsPrivileges;
+};
+
 export type PrivilegeGroupsRes = ListPrivilegeGroupsResponse;
 export type PrivilegeGroup = PrivelegeGroup;
+
+export type Privilege = {
+  [key: string]: boolean; // key: privilege name, value: whether it's selected
+};
+
+export type CollectionPrivileges = {
+  [collectionValue: string]: Privilege; // key: collection value, value: privileges
+};
+
+export type DBPrivileges = {
+  collections: CollectionPrivileges; // Collection-level privileges
+};
+
+export type DBCollectionsPrivileges = {
+  [dbValue: string]: DBPrivileges; // key: DB value, value: DB privileges and collections
+};
+
+export type RBACOptions = {
+  ClusterPrivilegeGroups: Record<string, unknown>;
+  DatabasePrivilegeGroups: Record<string, unknown>;
+  CollectionPrivilegeGroups: Record<string, unknown>;
+  CustomPrivilegeGroups: Record<string, unknown>;
+  CollectionPrivileges: Record<string, unknown>;
+  DatabasePrivileges: Record<string, unknown>;
+  EntityPrivileges: Record<string, unknown>;
+  IndexPrivileges: Record<string, unknown>;
+  PartitionPrivileges: Record<string, unknown>;
+  RBACPrivileges: Record<string, unknown>;
+  ResourceManagementPrivileges: Record<string, unknown>;
+};

+ 230 - 35
server/src/users/users.controller.ts

@@ -11,7 +11,19 @@ import {
   CreatePrivilegeGroupDto,
   PrivilegeToRoleDto,
 } from './dto';
-import { OperateRolePrivilegeReq } from '@zilliz/milvus2-sdk-node';
+import type {
+  DBCollectionsPrivileges,
+  RolesWithPrivileges,
+} from '../types/users.type';
+import {
+  DatabasePrivileges,
+  CollectionPrivileges,
+  PartitionPrivileges,
+  IndexPrivileges,
+  EntityPrivileges,
+  ResourceManagementPrivileges,
+  RBACPrivileges,
+} from '../utils';
 
 export class UserController {
   private router: Router;
@@ -49,7 +61,10 @@ export class UserController {
 
     // role
     this.router.get('/rbac', this.rbac.bind(this));
-    this.router.get('/privilegeGroups', this.allPrivilegeGroups.bind(this));
+    this.router.get(
+      '/privilegeGroups',
+      this.getDefaultPriviegGroups.bind(this)
+    );
     this.router.get('/roles', this.getRoles.bind(this));
     this.router.post(
       '/roles',
@@ -91,7 +106,25 @@ export class UserController {
     try {
       const result = await this.userService.getUsers(req.clientId);
 
-      res.send(result);
+      const results = [];
+      for (let i = 0; i < result.usernames.length; i++) {
+        const username = result.usernames[i];
+        const roles = await this.userService.selectUser(req.clientId, {
+          username,
+          includeRoleInfo: true,
+        });
+        results.push({
+          username,
+          roles: roles.results[0].roles.map(r => r.name),
+        });
+        if (username === 'root') {
+          // Remove the recently pushed "root" user and insert it at the beginning of the results array
+          const rootUser = results.pop();
+          results.unshift(rootUser);
+        }
+      }
+
+      res.send(results);
     } catch (error) {
       next(error);
     }
@@ -150,17 +183,58 @@ export class UserController {
 
   async getRoles(req: Request, res: Response, next: NextFunction) {
     try {
-      const result = await this.userService.getRoles(req.clientId);
+      // Fetch all roles
+      const rolesResult = await this.userService.getRoles(req.clientId);
 
-      for (let i = 0; i < result.results.length; i++) {
-        const { entities } = await this.userService.listGrants(req.clientId, {
-          roleName: result.results[i].role.name,
+      // Initialize the result array
+      const rolesWithPrivileges: RolesWithPrivileges[] = [];
+
+      // Iterate through each role
+      for (let i = 0; i < rolesResult.results.length; i++) {
+        const roleName = rolesResult.results[i].role.name;
+
+        // Fetch grants for the current role
+        const grantsResponse = await this.userService.listGrants(
+          req.clientId,
+          roleName
+        );
+
+        // Initialize the privileges structure for the current role
+        const dbCollectionsPrivileges: DBCollectionsPrivileges = {};
+
+        // Iterate through each grant entity
+        for (const entity of grantsResponse.entities) {
+          const { db_name, object_name, grantor } = entity;
+
+          // Initialize the database entry if it doesn't exist
+          if (!dbCollectionsPrivileges[db_name]) {
+            dbCollectionsPrivileges[db_name] = {
+              collections: {},
+            };
+          }
+
+          // Initialize the collection entry if it doesn't exist
+          if (!dbCollectionsPrivileges[db_name].collections[object_name]) {
+            dbCollectionsPrivileges[db_name].collections[object_name] = {};
+          }
+
+          // Add the privilege to the collection
+          dbCollectionsPrivileges[db_name].collections[object_name][
+            grantor.privilege.name
+          ] = true;
+        }
+
+        // Add the role and its privileges to the result array
+        rolesWithPrivileges.push({
+          roleName,
+          privileges: dbCollectionsPrivileges,
         });
-        result.results[i].entities = entities;
       }
 
-      res.send(result);
+      // Send the transformed result
+      res.status(200).json(rolesWithPrivileges);
     } catch (error) {
+      // Pass the error to the error-handling middleware
       next(error);
     }
   }
@@ -268,17 +342,59 @@ export class UserController {
 
   async rbac(req: Request, res: Response, next: NextFunction) {
     try {
-      const result = await this.userService.getRBAC();
+      const privilegeGrps = await this.userService.getPriviegGroups(
+        req.clientId
+      );
+
+      const ClusterPrivilegeGroups = {} as any;
+      const DatabasePrivilegeGroups = {} as any;
+      const CollectionPrivilegeGroups = {} as any;
+      const CustomPrivilegeGroups = {} as any;
+
+      privilegeGrps.cluster.forEach((g: any) => {
+        ClusterPrivilegeGroups[g.group_name] = g.group_name;
+      });
+
+      privilegeGrps.db.forEach((g: any) => {
+        DatabasePrivilegeGroups[g.group_name] = g.group_name;
+      });
+
+      privilegeGrps.collection.forEach((g: any) => {
+        CollectionPrivilegeGroups[g.group_name] = g.group_name;
+      });
+
+      privilegeGrps.custom.forEach((g: any) => {
+        CustomPrivilegeGroups[g.group_name] = g.group_name;
+      });
+
+      const result = {
+        ClusterPrivilegeGroups,
+        DatabasePrivilegeGroups,
+        CollectionPrivilegeGroups,
+        CustomPrivilegeGroups,
+        DatabasePrivileges,
+        ResourceManagementPrivileges,
+        RBACPrivileges,
+        CollectionPrivileges,
+        PartitionPrivileges,
+        IndexPrivileges,
+        EntityPrivileges,
+      };
+
       res.send(result);
     } catch (error) {
       next(error);
     }
   }
 
-  async allPrivilegeGroups(req: Request, res: Response, next: NextFunction) {
+  async getDefaultPriviegGroups(
+    req: Request,
+    res: Response,
+    next: NextFunction
+  ) {
     try {
-      const result = await this.userService.getAllPrivilegeGroups(req.clientId);
-      res.send(result);
+      const result = await this.userService.getPriviegGroups(req.clientId);
+      res.send(result.default);
     } catch (error) {
       next(error);
     }
@@ -293,7 +409,8 @@ export class UserController {
     try {
       const result = await this.userService.listGrants(req.clientId, {
         roleName,
-      });
+        db_name: '*',
+      } as any);
       res.send(result);
     } catch (error) {
       next(error);
@@ -304,33 +421,111 @@ export class UserController {
     req: Request<
       { roleName: string },
       {},
-      { privileges: OperateRolePrivilegeReq[] }
+      { privileges: DBCollectionsPrivileges }
     >,
     res: Response,
     next: NextFunction
   ) {
     const { privileges } = req.body;
     const { roleName } = req.params;
+    const clientId = req.clientId;
 
     const results = [];
+    const rollbackStack = []; // Stack to store actions for rollback in case of failure
 
     try {
-      // revoke all
-      await this.userService.revokeAllRolePrivileges(req.clientId, {
+      // check if role exists
+      const hasRole = await this.userService.hasRole(clientId, {
         roleName,
       });
+      // if role does not exist, create it
+      if (hasRole.hasRole === false) {
+        await this.userService.createRole(clientId, { roleName });
+      }
 
-      // assign new user roles
-      for (let i = 0; i < privileges.length; i++) {
-        const result = await this.userService.grantRolePrivilege(
-          req.clientId,
-          privileges[i]
-        );
-        results.push(result);
+      // Iterate over each database
+      for (const [dbName, dbPrivileges] of Object.entries(privileges)) {
+        // Iterate over each collection in the database
+        for (const [collectionName, collectionPrivileges] of Object.entries(
+          dbPrivileges.collections
+        )) {
+          // Iterate over each privilege in the collection
+          for (const [privilegeName, isGranted] of Object.entries(
+            collectionPrivileges
+          )) {
+            const requestData = {
+              role: roleName,
+              privilege: privilegeName,
+              db_name: dbName,
+              collection_name: collectionName,
+            };
+
+            let result;
+            try {
+              if (isGranted) {
+                // If the privilege is true, call grantPrivilegeV2
+                result = await this.userService.grantPrivilegeV2(
+                  clientId,
+                  requestData
+                );
+                // Push the reverse action (revoke) to the rollback stack
+                rollbackStack.push({ action: 'revoke', data: requestData });
+              } else {
+                // If the privilege is false, call revokePrivilegeV2
+                result = await this.userService.revokePrivilegeV2(
+                  clientId,
+                  requestData
+                );
+                // Push the reverse action (grant) to the rollback stack
+                rollbackStack.push({ action: 'grant', data: requestData });
+              }
+
+              // Collect the result
+              results.push({
+                dbName,
+                collectionName,
+                privilegeName,
+                isGranted,
+                result,
+              });
+            } catch (error) {
+              // If an error occurs, log it and initiate rollback
+              console.error(
+                `Failed to update privilege: ${privilegeName} for collection: ${collectionName} in database: ${dbName}`,
+                error
+              );
+
+              // Rollback all previously applied changes
+              while (rollbackStack.length > 0) {
+                const { action, data } = rollbackStack.pop();
+                try {
+                  if (action === 'grant') {
+                    await this.userService.grantPrivilegeV2(clientId, data);
+                  } else if (action === 'revoke') {
+                    await this.userService.revokePrivilegeV2(clientId, data);
+                  }
+                } catch (rollbackError) {
+                  console.error(
+                    `Rollback failed for action: ${action} on privilege: ${data.privilege}`,
+                    rollbackError
+                  );
+                }
+              }
+
+              // drop the role if creation fails
+              await this.userService.deleteRole(clientId, { roleName });
+
+              // Propagate the error to the error handler
+              throw error;
+            }
+          }
+        }
       }
 
-      res.send(results);
+      // Return the results if everything succeeds
+      res.status(200).json({ results });
     } catch (error) {
+      // Pass the error to the error-handling middleware
       next(error);
     }
   }
@@ -350,7 +545,7 @@ export class UserController {
       // add privileges to the group
       const result = await this.userService.addPrivilegeToGroup(req.clientId, {
         group_name,
-        priviliges: privileges,
+        privileges,
       });
 
       res.send(result);
@@ -410,7 +605,7 @@ export class UserController {
     const { privileges } = req.body;
     // get existing group
     const theGroup = await this.userService.getPrivilegeGroup(req.clientId, {
-      group_name: group_name,
+      group_name,
     });
 
     // if no group found, return error
@@ -421,14 +616,14 @@ export class UserController {
     try {
       // remove all privileges from the group
       await this.userService.removePrivilegeFromGroup(req.clientId, {
-        group_name: group_name,
-        priviliges: theGroup.privileges.map(p => p.name),
+        group_name,
+        privileges: theGroup.privileges.map(p => p.name),
       });
 
       // add new privileges to the group
       const result = await this.userService.addPrivilegeToGroup(req.clientId, {
-        group_name: group_name,
-        priviliges: privileges,
+        group_name,
+        privileges,
       });
 
       res.send(result);
@@ -445,10 +640,10 @@ export class UserController {
     const { role, collection, privilege } = req.body;
     try {
       const result = await this.userService.grantPrivilegeV2(req.clientId, {
-        role: role,
+        role,
         collection_name: collection || '*',
         db_name: req.db_name,
-        privilege: privilege,
+        privilege,
       });
       res.send(result);
     } catch (error) {
@@ -464,10 +659,10 @@ export class UserController {
     const { role, collection, privilege } = req.body;
     try {
       const result = await this.userService.revokePrivilegeV2(req.clientId, {
-        role: role,
+        role,
         collection_name: collection || '*',
         db_name: req.db_name,
-        privilege: privilege,
+        privilege,
       });
       res.send(result);
     } catch (error) {

+ 73 - 72
server/src/users/users.service.ts

@@ -9,19 +9,10 @@ import {
   HasRoleReq,
   listRoleReq,
   SelectUserReq,
-  ListGrantsReq,
   OperateRolePrivilegeReq,
   GrantPrivilegeV2Request,
   RevokePrivilegeV2Request,
 } from '@zilliz/milvus2-sdk-node';
-import { throwErrorFromSDK } from '../utils/Error';
-import {
-  Privileges,
-  GlobalPrivileges,
-  CollectionPrivileges,
-  UserPrivileges,
-  RbacObjects,
-} from '../utils';
 import { clientCache } from '../app';
 
 export class UserService {
@@ -29,7 +20,6 @@ export class UserService {
     const { milvusClient } = clientCache.get(clientId);
 
     const res = await milvusClient.listUsers();
-    throwErrorFromSDK(res.status);
 
     return res;
   }
@@ -38,7 +28,6 @@ export class UserService {
     const { milvusClient } = clientCache.get(clientId);
 
     const res = await milvusClient.createUser(data);
-    throwErrorFromSDK(res);
 
     return res;
   }
@@ -47,7 +36,6 @@ export class UserService {
     const { milvusClient } = clientCache.get(clientId);
 
     const res = await milvusClient.updateUser(data);
-    throwErrorFromSDK(res);
 
     return res;
   }
@@ -56,7 +44,6 @@ export class UserService {
     const { milvusClient } = clientCache.get(clientId);
 
     const res = await milvusClient.deleteUser(data);
-    throwErrorFromSDK(res);
     return res;
   }
 
@@ -64,7 +51,6 @@ export class UserService {
     const { milvusClient } = clientCache.get(clientId);
 
     const res = await milvusClient.listRoles(data);
-    throwErrorFromSDK(res.status);
 
     return res;
   }
@@ -73,7 +59,6 @@ export class UserService {
     const { milvusClient } = clientCache.get(clientId);
 
     const res = await milvusClient.selectUser(data);
-    throwErrorFromSDK(res.status);
 
     return res;
   }
@@ -82,7 +67,6 @@ export class UserService {
     const { milvusClient } = clientCache.get(clientId);
 
     const res = await milvusClient.createRole(data);
-    throwErrorFromSDK(res);
 
     return res;
   }
@@ -91,7 +75,6 @@ export class UserService {
     const { milvusClient } = clientCache.get(clientId);
 
     const res = await milvusClient.dropRole(data);
-    throwErrorFromSDK(res);
     return res;
   }
 
@@ -99,7 +82,6 @@ export class UserService {
     const { milvusClient } = clientCache.get(clientId);
 
     const res = await milvusClient.addUserToRole(data);
-    throwErrorFromSDK(res);
     return res;
   }
 
@@ -107,7 +89,6 @@ export class UserService {
     const { milvusClient } = clientCache.get(clientId);
 
     const res = await milvusClient.removeUserFromRole(data);
-    throwErrorFromSDK(res);
     return res;
   }
 
@@ -115,26 +96,13 @@ export class UserService {
     const { milvusClient } = clientCache.get(clientId);
 
     const res = await milvusClient.hasRole(data);
-    throwErrorFromSDK(res.status);
     return res;
   }
 
-  async getRBAC() {
-    return {
-      Privileges,
-      GlobalPrivileges,
-      CollectionPrivileges,
-      UserPrivileges,
-      RbacObjects,
-    };
-  }
-
-  async getAllPrivilegeGroups(clientId: string) {
+  async getPriviegGroups(clientId: string) {
     const { milvusClient } = clientCache.get(clientId);
     const privilegeGrps = await milvusClient.listPrivilegeGroups();
 
-    throwErrorFromSDK(privilegeGrps.status);
-
     const defaultGrp = [
       'ClusterAdmin',
       'ClusterReadOnly',
@@ -149,24 +117,71 @@ export class UserService {
       'CollectionReadWrite',
     ];
 
-    // only show default groups
-    const groups = privilegeGrps.privilege_groups.filter(
-      g => defaultGrp.indexOf(g.group_name) !== -1
-    );
+    const clusterGrp = ['ClusterAdmin', 'ClusterReadOnly', 'ClusterReadWrite'];
+
+    const databaseGrp = [
+      'DatabaseAdmin',
+      'DatabaseReadOnly',
+      'DatabaseReadWrite',
+    ];
 
-    // sort groups by the order in defaultGrp
-    groups.sort(
-      (a, b) =>
-        defaultGrp.indexOf(a.group_name) - defaultGrp.indexOf(b.group_name)
+    const collectionGrp = [
+      'CollectionAdmin',
+      'CollectionReadOnly',
+      'CollectionReadWrite',
+    ];
+
+    let res: Record<string, any> = {};
+
+    ['cluster', 'db', 'collection', 'default', 'custom', 'all'].forEach(
+      type => {
+        let groups = [] as any[];
+        switch (type) {
+          case 'cluster':
+            groups = privilegeGrps.privilege_groups.filter(g =>
+              clusterGrp.includes(g.group_name)
+            );
+            break;
+          case 'db':
+            groups = privilegeGrps.privilege_groups.filter(g =>
+              databaseGrp.includes(g.group_name)
+            );
+            break;
+          case 'collection':
+            groups = privilegeGrps.privilege_groups.filter(g =>
+              collectionGrp.includes(g.group_name)
+            );
+            break;
+          case 'default':
+            groups = privilegeGrps.privilege_groups.filter(g =>
+              defaultGrp.includes(g.group_name)
+            );
+            break;
+          case 'custom':
+            groups = privilegeGrps.privilege_groups.filter(
+              g => !defaultGrp.includes(g.group_name)
+            );
+            break;
+          case 'all':
+            groups = privilegeGrps.privilege_groups;
+            break;
+        }
+
+        res[type] = groups.sort(
+          (a, b) =>
+            defaultGrp.indexOf(a.group_name) - defaultGrp.indexOf(b.group_name)
+        );
+      }
     );
 
-    return groups;
+    return res;
   }
 
-  async listGrants(clientId: string, data: ListGrantsReq) {
+  async listGrants(clientId: string, roleName: string) {
     const { milvusClient } = clientCache.get(clientId);
-    const res = await milvusClient.listGrants(data);
-    throwErrorFromSDK(res.status);
+    const res = await milvusClient.listGrants({
+      roleName,
+    });
     return res;
   }
 
@@ -174,7 +189,6 @@ export class UserService {
     const { milvusClient } = clientCache.get(clientId);
 
     const res = await milvusClient.grantRolePrivilege(data);
-    throwErrorFromSDK(res);
     return res;
   }
 
@@ -182,24 +196,20 @@ export class UserService {
     const { milvusClient } = clientCache.get(clientId);
 
     const res = await milvusClient.revokeRolePrivilege(data);
-    throwErrorFromSDK(res);
     return res;
   }
 
   async revokeAllRolePrivileges(clientId: string, data: { roleName: string }) {
     // get existing privileges
-    const existingPrivileges = await this.listGrants(clientId, {
-      roleName: data.roleName,
-    });
-
-    // revoke all
-    for (let i = 0; i < existingPrivileges.entities.length; i++) {
-      const res = existingPrivileges.entities[i];
-      await this.revokeRolePrivilege(clientId, {
-        object: res.object.name,
-        objectName: res.object_name,
-        privilegeName: res.grantor.privilege.name,
-        roleName: res.role.name,
+    const existingPrivileges = await this.listGrants(clientId, data.roleName);
+
+    // revoke all existing privileges
+    for (const entity of existingPrivileges.entities) {
+      const res = await this.revokePrivilegeV2(clientId, {
+        db_name: entity.db_name,
+        collection_name: entity.object_name,
+        privilege: entity.grantor.privilege.name,
+        role: entity.role.name,
       });
     }
   }
@@ -212,7 +222,6 @@ export class UserService {
       group_name: data.group_name,
     });
 
-    throwErrorFromSDK(res);
     return res;
   }
 
@@ -222,8 +231,6 @@ export class UserService {
 
     const res = await milvusClient.listPrivilegeGroups();
 
-    throwErrorFromSDK(res.status);
-
     return res;
   }
 
@@ -235,7 +242,6 @@ export class UserService {
       g => g.group_name === data.group_name
     );
 
-    throwErrorFromSDK(res.status);
     return group;
   }
 
@@ -247,39 +253,36 @@ export class UserService {
       group_name: data.group_name,
     });
 
-    throwErrorFromSDK(res);
     return res;
   }
 
   // update privilege group
   async addPrivilegeToGroup(
     clientId: string,
-    data: { group_name: string; priviliges: string[] }
+    data: { group_name: string; privileges: string[] }
   ) {
     const { milvusClient } = clientCache.get(clientId);
 
     const res = await milvusClient.addPrivilegesToGroup({
       group_name: data.group_name,
-      privileges: data.priviliges,
+      privileges: data.privileges,
     });
 
-    throwErrorFromSDK(res);
     return res;
   }
 
   // remove privilege from group
   async removePrivilegeFromGroup(
     clientId: string,
-    data: { group_name: string; priviliges: string[] }
+    data: { group_name: string; privileges: string[] }
   ) {
     const { milvusClient } = clientCache.get(clientId);
 
     const res = await milvusClient.removePrivilegesFromGroup({
       group_name: data.group_name,
-      privileges: data.priviliges,
+      privileges: data.privileges,
     });
 
-    throwErrorFromSDK(res);
     return res;
   }
 
@@ -289,7 +292,6 @@ export class UserService {
 
     const res = await milvusClient.grantPrivilegeV2(data);
 
-    throwErrorFromSDK(res);
     return res;
   }
 
@@ -299,7 +301,6 @@ export class UserService {
 
     const res = await milvusClient.revokePrivilegeV2(data);
 
-    throwErrorFromSDK(res);
     return res;
   }
 }

+ 40 - 62
server/src/utils/Const.ts

@@ -5,8 +5,6 @@ export const ROW_COUNT = 'row_count';
 export const MILVUS_CLIENT_ID = 'milvus-client-id';
 
 // for lru cache
-export const CLIENT_CACHE = 'insight_cache';
-export const INDEX_CACHE = 'index_cache';
 export const CLIENT_TTL = 1000 * 60 * 60 * 24;
 export const INDEX_TTL = 1000 * 60 * 60;
 
@@ -74,98 +72,77 @@ export enum HTTP_STATUS_CODE {
   HTTP_VERSION_NOT_SUPPORTED = 505,
 }
 
-// RBAC: default objects
 export enum RbacObjects {
   Collection = 'Collection',
   Global = 'Global',
   User = 'User',
 }
 
-// RBAC: collection privileges
+export enum DatabasePrivileges {
+  CreateDatabase = 'CreateDatabase',
+  DescribeDatabase = 'DescribeDatabase',
+  ListDatabases = 'ListDatabases',
+  DropDatabase = 'DropDatabase',
+  AlterDatabase = 'AlterDatabase',
+}
+
 export enum CollectionPrivileges {
   CreateCollection = 'CreateCollection',
-  DropCollection = 'DropCollection',
   DescribeCollection = 'DescribeCollection',
   ShowCollections = 'ShowCollections',
+  DropCollection = 'DropCollection',
   RenameCollection = 'RenameCollection',
-  CreateIndex = 'CreateIndex',
-  DropIndex = 'DropIndex',
-  IndexDetail = 'IndexDetail',
+  CreateAlias = 'CreateAlias',
+  DescribeAlias = 'DescribeAlias',
+  DropAlias = 'DropAlias',
+  ListAliases = 'ListAliases',
   Load = 'Load',
   GetLoadingProgress = 'GetLoadingProgress',
   GetLoadState = 'GetLoadState',
   Release = 'Release',
-  Insert = 'Insert',
-  Upsert = 'Upsert',
-  Delete = 'Delete',
-  Search = 'Search',
   Flush = 'Flush',
   GetFlushState = 'GetFlushState',
-  Query = 'Query',
   GetStatistics = 'GetStatistics',
   Compaction = 'Compaction',
-  Import = 'Import',
-  LoadBalance = 'LoadBalance',
+  FlushAll = 'FlushAll',
+}
+
+export enum PartitionPrivileges {
   CreatePartition = 'CreatePartition',
   DropPartition = 'DropPartition',
   ShowPartitions = 'ShowPartitions',
   HasPartition = 'HasPartition',
-  FlushAll = 'FlushAll',
-  CreateAlias = 'CreateAlias',
-  DropAlias = 'DropAlias',
-  DescribeAlias = 'DescribeAlias',
-  ListAliases = 'ListAliases',
 }
 
-// RBAC: global privileges
-export enum DatabasePrivileges {
-  ListDatabases = 'ListDatabases',
-  DescribeDatabase = 'DescribeDatabase',
-  CreateDatabase = 'CreateDatabase',
-  DropDatabase = 'DropDatabase',
+export enum EntityPrivileges {
+  Query = 'Query',
+  Insert = 'Insert',
+  Upsert = 'Upsert',
+  Delete = 'Delete',
+  Search = 'Search',
+  Import = 'Import',
 }
 
-// RBAC: global privileges
-export enum GlobalPrivileges {
-  All = '*',
-  CreateCollection = 'CreateCollection',
-  DropCollection = 'DropCollection',
-  DescribeCollection = 'DescribeCollection',
-  ShowCollections = 'ShowCollections',
-  RenameCollection = 'RenameCollection',
-  FlushAll = 'FlushAll',
-  CreateOwnership = 'CreateOwnership',
-  DropOwnership = 'DropOwnership',
-  SelectOwnership = 'SelectOwnership',
-  ManageOwnership = 'ManageOwnership',
+export enum ResourceManagementPrivileges {
   CreateResourceGroup = 'CreateResourceGroup',
   DropResourceGroup = 'DropResourceGroup',
+  UpdateResourceGroups = 'UpdateResourceGroups',
   DescribeResourceGroup = 'DescribeResourceGroup',
   ListResourceGroups = 'ListResourceGroups',
+  LoadBalance = 'LoadBalance',
   TransferNode = 'TransferNode',
   TransferReplica = 'TransferReplica',
-  CreateDatabase = 'CreateDatabase',
-  ListDatabases = 'ListDatabases',
-  DropDatabase = 'DropDatabase',
-  CreateAlias = 'CreateAlias',
-  DropAlias = 'DropAlias',
-  DescribeAlias = 'DescribeAlias',
-  ListAliases = 'ListAliases',
+  BackupRBAC = 'BackupRBAC',
+  RestoreRBAC = 'RestoreRBAC',
 }
 
-// RBAC: resource group privileges
-export enum ResourceGroupPrivileges {
-  CreateResourceGroup = 'CreateResourceGroup',
-  DropResourceGroup = 'DropResourceGroup',
-  DescribeResourceGroup = 'DescribeResourceGroup',
-  ListResourceGroups = 'ListResourceGroups',
-  UpdateResourceGroups = 'UpdateResourceGroups',
-  TransferNode = 'TransferNode',
-  TransferReplica = 'TransferReplica',
+export enum IndexPrivileges {
+  CreateIndex = 'CreateIndex',
+  DropIndex = 'DropIndex',
+  IndexDetail = 'IndexDetail',
 }
 
-// RBAC: user privileges
-export enum UserPrivileges {
+export enum RBACPrivileges {
   UpdateUser = 'UpdateUser',
   SelectUser = 'SelectUser',
   SelectOwnership = 'SelectOwnership',
@@ -176,16 +153,17 @@ export enum UserPrivileges {
   DropPrivilegeGroup = 'DropPrivilegeGroup',
   ListPrivilegeGroups = 'ListPrivilegeGroups',
   OperatePrivilegeGroup = 'OperatePrivilegeGroup',
-  RestoreRBAC = 'RestoreRBAC',
-  BackupRBAC = 'BackupRBAC',
 }
 
 // RBAC: all privileges
 export const Privileges = {
-  ...CollectionPrivileges,
   ...DatabasePrivileges,
-  ...ResourceGroupPrivileges,
-  ...UserPrivileges,
+  ...CollectionPrivileges,
+  ...PartitionPrivileges,
+  ...IndexPrivileges,
+  ...EntityPrivileges,
+  ...ResourceManagementPrivileges,
+  ...RBACPrivileges,
 };
 
 export enum LOADING_STATE {

+ 2 - 2
server/src/utils/Error.ts

@@ -1,7 +1,7 @@
-import { ErrorCode, ResStatus } from '@zilliz/milvus2-sdk-node/dist/milvus';
+import { ErrorCode, ResStatus } from '@zilliz/milvus2-sdk-node';
 
 export const throwErrorFromSDK = (res: ResStatus) => {
   if (res.error_code !== ErrorCode.SUCCESS) {
-    throw res.reason;
+    // throw res.reason;
   }
 };

+ 10 - 4
server/yarn.lock

@@ -998,6 +998,11 @@
     mkdirp "^1.0.4"
     rimraf "^3.0.2"
 
+"@opentelemetry/api@^1.9.0":
+  version "1.9.0"
+  resolved "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz#d03eba68273dc0f7509e2a3d5cba21eae10379fe"
+  integrity sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==
+
 "@petamoriken/float16@^3.8.6":
   version "3.8.6"
   resolved "https://registry.yarnpkg.com/@petamoriken/float16/-/float16-3.8.6.tgz#580701cb97a510882342333d31c7cbfd9e14b4f4"
@@ -1476,13 +1481,14 @@
   resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.9.tgz#b6ef7457e826be8049667ae673eda7876eb049be"
   integrity sha512-4VSbbcMoxc4KLjb1gs96SRmi7w4h1SF+fCoiK0XaQX62buCc1G5d0DC5bJ9xJBNPDSVCmIrcl8BiYxzjrqaaJA==
 
-"@zilliz/milvus2-sdk-node@2.5.3":
-  version "2.5.3"
-  resolved "https://registry.yarnpkg.com/@zilliz/milvus2-sdk-node/-/milvus2-sdk-node-2.5.3.tgz#3ea5883c584de691eed36eeb602eee472069831e"
-  integrity sha512-8rLs/E0uukHdV1TEe5zMJiL+l6cmqgcYgmprlSbQLvF8ZBDl7rZsxayy94yNMTAxtrzwV5EUCvhd6aPZeO7FSw==
+"@zilliz/milvus2-sdk-node@2.5.5":
+  version "2.5.5"
+  resolved "https://registry.npmjs.org/@zilliz/milvus2-sdk-node/-/milvus2-sdk-node-2.5.5.tgz#86f2d37a44c3b63949bd1be07c33a46b0bf0d73d"
+  integrity sha512-vKDbNu6auOTV0PZSvjKhdXR4wEs+ldREmYA3Ffll2kqJxaljAXmTcDXCw7arFa+/n+xDsg+nP1KTpuZATYDZ8g==
   dependencies:
     "@grpc/grpc-js" "^1.12.1"
     "@grpc/proto-loader" "^0.7.10"
+    "@opentelemetry/api" "^1.9.0"
     "@petamoriken/float16" "^3.8.6"
     dayjs "^1.11.7"
     generic-pool "^3.9.0"