Browse Source

support manage roles

Signed-off-by: shanghaikid <jiangruiyi@gmail.com>
shanghaikid 1 year ago
parent
commit
7c80f0caa5

+ 14 - 0
client/src/http/User.ts

@@ -2,6 +2,8 @@ import {
   CreateUserParams,
   CreateUserParams,
   DeleteUserParams,
   DeleteUserParams,
   UpdateUserParams,
   UpdateUserParams,
+  CreateRoleParams,
+  DeleteRoleParams,
 } from '../pages/user/Types';
 } from '../pages/user/Types';
 import BaseModel from './BaseModel';
 import BaseModel from './BaseModel';
 
 
@@ -31,6 +33,18 @@ export class UserHttp extends BaseModel {
     return super.delete({ path: `${this.USER_URL}/${data.username}` });
     return super.delete({ path: `${this.USER_URL}/${data.username}` });
   }
   }
 
 
+  static createRole(data: CreateRoleParams) {
+    return super.create({ path: `${this.USER_URL}/roles`, data });
+  }
+
+  static getRoles() {
+    return super.search({ path: `${this.USER_URL}/roles`, params: {} });
+  }
+
+  static deleteRole(data: DeleteRoleParams) {
+    return super.delete({ path: `${this.USER_URL}/roles/${data.roleName}` });
+  }
+
   get _names() {
   get _names() {
     return this.names;
     return this.names;
   }
   }

+ 5 - 0
client/src/i18n/en/user.ts

@@ -2,6 +2,7 @@ const userTrans = {
   createTitle: 'Create User',
   createTitle: 'Create User',
   updateTitle: 'Update Milvus User',
   updateTitle: 'Update Milvus User',
   user: 'User',
   user: 'User',
+  users: 'Users',
   deleteWarning: 'You are trying to drop user. This action cannot be undone.',
   deleteWarning: 'You are trying to drop user. This action cannot be undone.',
   oldPassword: 'Current Password',
   oldPassword: 'Current Password',
   newPassword: 'New Password',
   newPassword: 'New Password',
@@ -10,6 +11,10 @@ const userTrans = {
   isNotSame: 'Not same as new password',
   isNotSame: 'Not same as new password',
   deleteTip:
   deleteTip:
     'Please select at least one item to drop and root can not be dropped.',
     'Please select at least one item to drop and root can not be dropped.',
+
+  role: 'Role',
+  roles: 'Roles',
+  createRoleTitle: 'Create Role',
 };
 };
 
 
 export default userTrans;
 export default userTrans;

+ 86 - 0
client/src/pages/user/CreateRole.tsx

@@ -0,0 +1,86 @@
+import { makeStyles, Theme } from '@material-ui/core';
+import { FC, useMemo, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import DialogTemplate from '@/components/customDialog/DialogTemplate';
+import CustomInput from '@/components/customInput/CustomInput';
+import { ITextfieldConfig } from '@/components/customInput/Types';
+import { useFormValidation } from '@/hooks/Form';
+import { formatForm } from '@/utils/Form';
+import { CreateRoleProps, CreateRoleParams } from './Types';
+
+const useStyles = makeStyles((theme: Theme) => ({
+  input: {
+    margin: theme.spacing(3, 0, 0.5),
+  },
+}));
+
+const CreateRole: FC<CreateRoleProps> = ({ handleCreate, handleClose }) => {
+  const { t: commonTrans } = useTranslation();
+  const { t: userTrans } = useTranslation('user');
+  const { t: btnTrans } = useTranslation('btn');
+  const { t: warningTrans } = useTranslation('warning');
+  const attuTrans = commonTrans('attu');
+
+  const [form, setForm] = useState<CreateRoleParams>({
+    roleName: '',
+  });
+  const checkedForm = useMemo(() => {
+    return formatForm(form);
+  }, [form]);
+  const { validation, checkIsValid, disabled } = useFormValidation(checkedForm);
+
+  const classes = useStyles();
+
+  const handleInputChange = (key: 'roleName' | 'password', value: string) => {
+    setForm(v => ({ ...v, [key]: value }));
+  };
+
+  const createConfigs: ITextfieldConfig[] = [
+    {
+      label: userTrans('role'),
+      key: 'roleName',
+      onChange: (value: string) => handleInputChange('roleName', value),
+      variant: 'filled',
+      className: classes.input,
+      placeholder: userTrans('role'),
+      fullWidth: true,
+      validations: [
+        {
+          rule: 'require',
+          errorText: warningTrans('required', {
+            name: userTrans('role'),
+          }),
+        },
+      ],
+      defaultValue: form.roleName,
+    },
+  ];
+
+  const handleCreateRole = () => {
+    handleCreate(form);
+  };
+
+  return (
+    <DialogTemplate
+      title={userTrans('createRoleTitle')}
+      handleClose={handleClose}
+      confirmLabel={btnTrans('create')}
+      handleConfirm={handleCreateRole}
+      confirmDisabled={disabled}
+    >
+      <>
+        {createConfigs.map(v => (
+          <CustomInput
+            type="text"
+            textConfig={v}
+            checkValid={checkIsValid}
+            validInfo={validation}
+            key={v.label}
+          />
+        ))}
+      </>
+    </DialogTemplate>
+  );
+};
+
+export default CreateRole;

+ 0 - 0
client/src/pages/user/Create.tsx → client/src/pages/user/CreateUser.tsx


+ 148 - 0
client/src/pages/user/Roles.tsx

@@ -0,0 +1,148 @@
+import React, { useContext, useEffect, useState } from 'react';
+import { makeStyles, Theme } from '@material-ui/core';
+import { useTranslation } from 'react-i18next';
+import { UserHttp } from '@/http/User';
+import AttuGrid from '@/components/grid/Grid';
+import { ColDefinitionsType, ToolBarConfig } from '@/components/grid/Types';
+import { CreateRoleParams, DeleteRoleParams, RoleData } from './Types';
+import DeleteTemplate from '@/components/customDialog/DeleteDialogTemplate';
+import { rootContext } from '@/context/Root';
+import { useNavigationHook } from '@/hooks/Navigation';
+import { ALL_ROUTER_TYPES } from '@/router/Types';
+import CreateRole from './CreateRole';
+
+const useStyles = makeStyles((theme: Theme) => ({
+  wrapper: {
+    height: `calc(100vh - 160px)`,
+  },
+}));
+
+const Roles = () => {
+  useNavigationHook(ALL_ROUTER_TYPES.USER);
+  const classes = useStyles();
+
+  const [roles, setRoles] = useState<RoleData[]>([]);
+  const [selectedRole, setSelectedRole] = useState<RoleData[]>([]);
+  const { setDialog, handleCloseDialog, openSnackBar } =
+    useContext(rootContext);
+  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 UserHttp.getRoles();
+
+    setRoles(roles.results.map((v: any) => ({ name: v.role.name })));
+  };
+
+  const handleCreate = async (data: CreateRoleParams) => {
+    await UserHttp.createRole(data);
+    fetchRoles();
+    openSnackBar(successTrans('create', { name: userTrans('role') }));
+    handleCloseDialog();
+  };
+
+  const handleDelete = async () => {
+    for (const role of selectedRole) {
+      const param: DeleteRoleParams = {
+        roleName: role.name,
+      };
+      await UserHttp.deleteRole(param);
+    }
+
+    openSnackBar(successTrans('delete', { name: userTrans('role') }));
+    fetchRoles();
+    handleCloseDialog();
+  };
+
+  const toolbarConfigs: ToolBarConfig[] = [
+    {
+      label: userTrans('role'),
+      onClick: () => {
+        setDialog({
+          open: true,
+          type: 'custom',
+          params: {
+            component: (
+              <CreateRole
+                handleCreate={handleCreate}
+                handleClose={handleCloseDialog}
+              />
+            ),
+          },
+        });
+      },
+      icon: 'add',
+    },
+
+    {
+      type: 'iconBtn',
+      onClick: () => {
+        setDialog({
+          open: true,
+          type: 'custom',
+          params: {
+            component: (
+              <DeleteTemplate
+                label={btnTrans('drop')}
+                title={dialogTrans('deleteTitle', { type: userTrans('role') })}
+                text={userTrans('deleteWarning')}
+                handleDelete={handleDelete}
+              />
+            ),
+          },
+        });
+      },
+      label: '',
+      disabled: () =>
+        selectedRole.length === 0 ||
+        selectedRole.findIndex(v => v.name === 'root') > -1,
+      disabledTooltip: userTrans('deleteTip'),
+
+      icon: 'delete',
+    },
+  ];
+
+  const colDefinitions: ColDefinitionsType[] = [
+    {
+      id: 'name',
+      align: 'left',
+      disablePadding: false,
+      label: userTrans('role'),
+    },
+  ];
+
+  const handleSelectChange = (value: RoleData[]) => {
+    setSelectedRole(value);
+  };
+
+  useEffect(() => {
+    fetchRoles();
+  }, []);
+
+  return (
+    <div className={classes.wrapper}>
+      <AttuGrid
+        toolbarConfigs={toolbarConfigs}
+        colDefinitions={colDefinitions}
+        rows={roles}
+        rowCount={roles.length}
+        primaryKey="name"
+        showPagination={false}
+        selected={selectedRole}
+        setSelected={handleSelectChange}
+        // page={currentPage}
+        // onChangePage={handlePageChange}
+        // rowsPerPage={pageSize}
+        // setRowsPerPage={handlePageSize}
+        // isLoading={loading}
+        // order={order}
+        // orderBy={orderBy}
+        // handleSort={handleGridSort}
+      />
+    </div>
+  );
+};
+
+export default Roles;

+ 26 - 0
client/src/pages/user/Types.ts

@@ -1,6 +1,7 @@
 export interface UserData {
 export interface UserData {
   name: string;
   name: string;
 }
 }
+
 export interface CreateUserParams {
 export interface CreateUserParams {
   username: string;
   username: string;
   password: string;
   password: string;
@@ -10,6 +11,7 @@ export interface CreateUserProps {
   handleCreate: (data: CreateUserParams) => void;
   handleCreate: (data: CreateUserParams) => void;
   handleClose: () => void;
   handleClose: () => void;
 }
 }
+
 export interface UpdateUserProps {
 export interface UpdateUserProps {
   handleUpdate: (data: UpdateUserParams) => void;
   handleUpdate: (data: UpdateUserParams) => void;
   handleClose: () => void;
   handleClose: () => void;
@@ -25,3 +27,27 @@ export interface UpdateUserParams {
 export interface DeleteUserParams {
 export interface DeleteUserParams {
   username: string;
   username: string;
 }
 }
+
+export interface CreateRoleParams {
+  roleName: string;
+}
+
+export interface CreateRoleProps {
+  handleCreate: (data: CreateRoleParams) => void;
+  handleClose: () => void;
+}
+
+export interface DeleteRoleParams {
+  roleName: string;
+}
+
+export interface RoleData {
+  name: string;
+}
+
+export enum TAB_EMUM {
+  'schema',
+  'partition',
+  'data-preview',
+  'data-query',
+}

+ 14 - 3
client/src/pages/user/User.tsx

@@ -1,4 +1,5 @@
 import React, { useContext, useEffect, useState } from 'react';
 import React, { useContext, useEffect, useState } from 'react';
+import { makeStyles, Theme } from '@material-ui/core';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 import { UserHttp } from '@/http/User';
 import { UserHttp } from '@/http/User';
 import AttuGrid from '@/components/grid/Grid';
 import AttuGrid from '@/components/grid/Grid';
@@ -13,11 +14,18 @@ import DeleteTemplate from '@/components/customDialog/DeleteDialogTemplate';
 import { rootContext } from '@/context/Root';
 import { rootContext } from '@/context/Root';
 import { useNavigationHook } from '@/hooks/Navigation';
 import { useNavigationHook } from '@/hooks/Navigation';
 import { ALL_ROUTER_TYPES } from '@/router/Types';
 import { ALL_ROUTER_TYPES } from '@/router/Types';
-import CreateUser from './Create';
+import CreateUser from './CreateUser';
 import UpdateUser from './Update';
 import UpdateUser from './Update';
 
 
+const useStyles = makeStyles((theme: Theme) => ({
+  wrapper: {
+    height: `calc(100vh - 160px)`,
+  },
+}));
+
 const Users = () => {
 const Users = () => {
   useNavigationHook(ALL_ROUTER_TYPES.USER);
   useNavigationHook(ALL_ROUTER_TYPES.USER);
+  const classes = useStyles();
 
 
   const [users, setUsers] = useState<UserData[]>([]);
   const [users, setUsers] = useState<UserData[]>([]);
   const [selectedUser, setSelectedUser] = useState<UserData[]>([]);
   const [selectedUser, setSelectedUser] = useState<UserData[]>([]);
@@ -30,6 +38,9 @@ const Users = () => {
 
 
   const fetchUsers = async () => {
   const fetchUsers = async () => {
     const res = await UserHttp.getUsers();
     const res = await UserHttp.getUsers();
+    const roles = await UserHttp.getRoles();
+
+    console.log('roles', roles);
 
 
     setUsers(res.usernames.map((v: string) => ({ name: v })));
     setUsers(res.usernames.map((v: string) => ({ name: v })));
   };
   };
@@ -114,7 +125,7 @@ const Users = () => {
       id: 'name',
       id: 'name',
       align: 'left',
       align: 'left',
       disablePadding: false,
       disablePadding: false,
-      label: 'Name',
+      label: userTrans('user'),
     },
     },
     {
     {
       id: 'action',
       id: 'action',
@@ -154,7 +165,7 @@ const Users = () => {
   }, []);
   }, []);
 
 
   return (
   return (
-    <div className="page-wrapper">
+    <div className={classes.wrapper}>
       <AttuGrid
       <AttuGrid
         toolbarConfigs={toolbarConfigs}
         toolbarConfigs={toolbarConfigs}
         colDefinitions={colDefinitions}
         colDefinitions={colDefinitions}

+ 77 - 0
client/src/pages/user/Users.tsx

@@ -0,0 +1,77 @@
+import { useMemo } from 'react';
+import { useNavigate, useLocation, useParams } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
+import { makeStyles, Theme } from '@material-ui/core';
+import { useNavigationHook } from '@/hooks/Navigation';
+import { ALL_ROUTER_TYPES } from '@/router/Types';
+import CustomTabList from '@/components/customTabList/CustomTabList';
+import { ITab } from '@/components/customTabList/Types';
+import { parseLocationSearch } from '@/utils/Format';
+import User from './User';
+import Roles from './Roles';
+import { TAB_EMUM } from './Types';
+
+const useStyles = makeStyles((theme: Theme) => ({
+  wrapper: {
+    flexDirection: 'row',
+    gap: theme.spacing(4),
+  },
+  card: {
+    boxShadow: 'none',
+    flexBasis: theme.spacing(28),
+    width: theme.spacing(28),
+    flexGrow: 0,
+    flexShrink: 0,
+  },
+  tab: {
+    flexGrow: 1,
+    flexShrink: 1,
+    overflowX: 'auto',
+  },
+}));
+
+const Users = () => {
+  const classes = useStyles();
+  useNavigationHook(ALL_ROUTER_TYPES.USER);
+
+  const navigate = useNavigate();
+  const location = useLocation();
+
+  const { t: userTrans } = useTranslation('user');
+
+  const activeTabIndex = useMemo(() => {
+    const { activeIndex } = location.search
+      ? parseLocationSearch(location.search)
+      : { activeIndex: TAB_EMUM.schema };
+    return Number(activeIndex);
+  }, [location]);
+
+  const handleTabChange = (activeIndex: number) => {
+    const path = location.pathname;
+    navigate(`${path}?activeIndex=${activeIndex}`);
+  };
+
+  const tabs: ITab[] = [
+    {
+      label: userTrans('users'),
+      component: <User />,
+    },
+    {
+      label: userTrans('roles'),
+      component: <Roles />,
+    },
+  ];
+
+  return (
+    <section className={`page-wrapper ${classes.wrapper}`}>
+      <CustomTabList
+        tabs={tabs}
+        wrapperClass={classes.tab}
+        activeIndex={activeTabIndex}
+        handleTabChange={handleTabChange}
+      />
+    </section>
+  );
+};
+
+export default Users;

+ 1 - 1
client/src/router/Router.tsx

@@ -4,7 +4,7 @@ import { authContext } from '../context/Auth';
 import Collection from '../pages/collections/Collection';
 import Collection from '../pages/collections/Collection';
 import Collections from '../pages/collections/Collections';
 import Collections from '../pages/collections/Collections';
 import Connect from '../pages/connect/Connect';
 import Connect from '../pages/connect/Connect';
-import Users from '../pages/user/User';
+import Users from '../pages/user/Users';
 import Database from '../pages/database/Database';
 import Database from '../pages/database/Database';
 import Index from '../pages/index';
 import Index from '../pages/index';
 import Search from '../pages/search/VectorSearch';
 import Search from '../pages/search/VectorSearch';

+ 1 - 0
server/src/milvus/milvus.service.ts

@@ -56,6 +56,7 @@ export class MilvusService {
         address: milvusAddress,
         address: milvusAddress,
         username,
         username,
         password,
         password,
+        logLevel: "debug"
       });
       });
 
 
       // don't break attu
       // don't break attu

+ 5 - 0
server/src/users/dto.ts

@@ -8,6 +8,11 @@ export class CreateUserDto {
   readonly password: string;
   readonly password: string;
 }
 }
 
 
+export class CreateRoleDto {
+  @IsString()
+  readonly roleName: string;
+}
+
 export class UpdateUserDto {
 export class UpdateUserDto {
   @IsString()
   @IsString()
   readonly username: string;
   readonly username: string;

+ 40 - 1
server/src/users/users.controller.ts

@@ -3,7 +3,7 @@ import { dtoValidationMiddleware } from '../middlewares/validation';
 import { UserService } from './users.service';
 import { UserService } from './users.service';
 import { milvusService } from '../milvus';
 import { milvusService } from '../milvus';
 
 
-import { CreateUserDto, UpdateUserDto } from './dto';
+import { CreateUserDto, UpdateUserDto, CreateRoleDto } from './dto';
 
 
 export class UserController {
 export class UserController {
   private router: Router;
   private router: Router;
@@ -31,6 +31,16 @@ export class UserController {
 
 
     this.router.delete('/:username', this.deleteUser.bind(this));
     this.router.delete('/:username', this.deleteUser.bind(this));
 
 
+    this.router.get('/roles', this.getRoles.bind(this));
+
+    this.router.post(
+      '/roles',
+      dtoValidationMiddleware(CreateRoleDto),
+      this.createRole.bind(this)
+    );
+
+    this.router.delete('/roles/:roleName', this.deleteRole.bind(this));
+
     return this.router;
     return this.router;
   }
   }
 
 
@@ -76,4 +86,33 @@ export class UserController {
       next(error);
       next(error);
     }
     }
   }
   }
+
+  async getRoles(req: Request, res: Response, next: NextFunction) {
+    try {
+      const result = await this.userService.getRoles();
+      res.send(result);
+    } catch (error) {
+      next(error);
+    }
+  }
+
+  async createRole(req: Request, res: Response, next: NextFunction) {
+    const { roleName } = req.body;
+    try {
+      const result = await this.userService.createRole({ roleName });
+      res.send(result);
+    } catch (error) {
+      next(error);
+    }
+  }
+
+  async deleteRole(req: Request, res: Response, next: NextFunction) {
+    const { roleName } = req.params;
+    try {
+      const result = await this.userService.deleteRole({ roleName });
+      res.send(result);
+    } catch (error) {
+      next(error);
+    }
+  }
 }
 }

+ 24 - 0
server/src/users/users.service.ts

@@ -3,6 +3,8 @@ import {
   CreateUserReq,
   CreateUserReq,
   UpdateUserReq,
   UpdateUserReq,
   DeleteUserReq,
   DeleteUserReq,
+  CreateRoleReq,
+  DropRoleReq,
 } from '@zilliz/milvus2-sdk-node';
 } from '@zilliz/milvus2-sdk-node';
 import { throwErrorFromSDK } from '../utils/Error';
 import { throwErrorFromSDK } from '../utils/Error';
 
 
@@ -35,4 +37,26 @@ export class UserService {
     throwErrorFromSDK(res);
     throwErrorFromSDK(res);
     return res;
     return res;
   }
   }
+
+  async getRoles() {
+    const res = await this.milvusService.client.listRoles({
+      includeUserInfo: true,
+    });
+    throwErrorFromSDK(res.status);
+
+    return res;
+  }
+
+  async createRole(data: CreateRoleReq) {
+    const res = await this.milvusService.client.createRole(data);
+    throwErrorFromSDK(res);
+
+    return res;
+  }
+
+  async deleteRole(data: DropRoleReq) {
+    const res = await this.milvusService.client.dropRole(data);
+    throwErrorFromSDK(res);
+    return res;
+  }
 }
 }