From 7c80f0caa5ebe004613f69f49f0a3207e14d13f9 Mon Sep 17 00:00:00 2001 From: shanghaikid Date: Thu, 27 Jul 2023 15:23:28 +0800 Subject: [PATCH 1/2] support manage roles Signed-off-by: shanghaikid --- client/src/http/User.ts | 14 ++ client/src/i18n/en/user.ts | 5 + client/src/pages/user/CreateRole.tsx | 86 ++++++++++ .../pages/user/{Create.tsx => CreateUser.tsx} | 0 client/src/pages/user/Roles.tsx | 148 ++++++++++++++++++ client/src/pages/user/Types.ts | 26 +++ client/src/pages/user/User.tsx | 17 +- client/src/pages/user/Users.tsx | 77 +++++++++ client/src/router/Router.tsx | 2 +- server/src/milvus/milvus.service.ts | 1 + server/src/users/dto.ts | 5 + server/src/users/users.controller.ts | 41 ++++- server/src/users/users.service.ts | 24 +++ 13 files changed, 441 insertions(+), 5 deletions(-) create mode 100644 client/src/pages/user/CreateRole.tsx rename client/src/pages/user/{Create.tsx => CreateUser.tsx} (100%) create mode 100644 client/src/pages/user/Roles.tsx create mode 100644 client/src/pages/user/Users.tsx diff --git a/client/src/http/User.ts b/client/src/http/User.ts index 0671c4c9..0cb2b972 100644 --- a/client/src/http/User.ts +++ b/client/src/http/User.ts @@ -2,6 +2,8 @@ import { CreateUserParams, DeleteUserParams, UpdateUserParams, + CreateRoleParams, + DeleteRoleParams, } from '../pages/user/Types'; import BaseModel from './BaseModel'; @@ -31,6 +33,18 @@ export class UserHttp extends BaseModel { 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() { return this.names; } diff --git a/client/src/i18n/en/user.ts b/client/src/i18n/en/user.ts index ef407e95..0fe07c77 100644 --- a/client/src/i18n/en/user.ts +++ b/client/src/i18n/en/user.ts @@ -2,6 +2,7 @@ const userTrans = { createTitle: 'Create User', updateTitle: 'Update Milvus User', user: 'User', + users: 'Users', deleteWarning: 'You are trying to drop user. This action cannot be undone.', oldPassword: 'Current Password', newPassword: 'New Password', @@ -10,6 +11,10 @@ const userTrans = { isNotSame: 'Not same as new password', deleteTip: 'Please select at least one item to drop and root can not be dropped.', + + role: 'Role', + roles: 'Roles', + createRoleTitle: 'Create Role', }; export default userTrans; diff --git a/client/src/pages/user/CreateRole.tsx b/client/src/pages/user/CreateRole.tsx new file mode 100644 index 00000000..a09c4013 --- /dev/null +++ b/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 = ({ 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({ + 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 ( + + <> + {createConfigs.map(v => ( + + ))} + + + ); +}; + +export default CreateRole; diff --git a/client/src/pages/user/Create.tsx b/client/src/pages/user/CreateUser.tsx similarity index 100% rename from client/src/pages/user/Create.tsx rename to client/src/pages/user/CreateUser.tsx diff --git a/client/src/pages/user/Roles.tsx b/client/src/pages/user/Roles.tsx new file mode 100644 index 00000000..5f6d4930 --- /dev/null +++ b/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([]); + const [selectedRole, setSelectedRole] = useState([]); + 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: ( + + ), + }, + }); + }, + icon: 'add', + }, + + { + type: 'iconBtn', + onClick: () => { + setDialog({ + open: true, + type: 'custom', + params: { + component: ( + + ), + }, + }); + }, + 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 ( +
+ +
+ ); +}; + +export default Roles; diff --git a/client/src/pages/user/Types.ts b/client/src/pages/user/Types.ts index 606f6f20..4879e154 100644 --- a/client/src/pages/user/Types.ts +++ b/client/src/pages/user/Types.ts @@ -1,6 +1,7 @@ export interface UserData { name: string; } + export interface CreateUserParams { username: string; password: string; @@ -10,6 +11,7 @@ export interface CreateUserProps { handleCreate: (data: CreateUserParams) => void; handleClose: () => void; } + export interface UpdateUserProps { handleUpdate: (data: UpdateUserParams) => void; handleClose: () => void; @@ -25,3 +27,27 @@ export interface UpdateUserParams { export interface DeleteUserParams { 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', +} diff --git a/client/src/pages/user/User.tsx b/client/src/pages/user/User.tsx index acbe33f0..4320780b 100644 --- a/client/src/pages/user/User.tsx +++ b/client/src/pages/user/User.tsx @@ -1,4 +1,5 @@ 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'; @@ -13,11 +14,18 @@ import DeleteTemplate from '@/components/customDialog/DeleteDialogTemplate'; import { rootContext } from '@/context/Root'; import { useNavigationHook } from '@/hooks/Navigation'; import { ALL_ROUTER_TYPES } from '@/router/Types'; -import CreateUser from './Create'; +import CreateUser from './CreateUser'; import UpdateUser from './Update'; +const useStyles = makeStyles((theme: Theme) => ({ + wrapper: { + height: `calc(100vh - 160px)`, + }, +})); + const Users = () => { useNavigationHook(ALL_ROUTER_TYPES.USER); + const classes = useStyles(); const [users, setUsers] = useState([]); const [selectedUser, setSelectedUser] = useState([]); @@ -30,6 +38,9 @@ const Users = () => { const fetchUsers = async () => { const res = await UserHttp.getUsers(); + const roles = await UserHttp.getRoles(); + + console.log('roles', roles); setUsers(res.usernames.map((v: string) => ({ name: v }))); }; @@ -114,7 +125,7 @@ const Users = () => { id: 'name', align: 'left', disablePadding: false, - label: 'Name', + label: userTrans('user'), }, { id: 'action', @@ -154,7 +165,7 @@ const Users = () => { }, []); return ( -
+
({ + 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: , + }, + { + label: userTrans('roles'), + component: , + }, + ]; + + return ( +
+ +
+ ); +}; + +export default Users; diff --git a/client/src/router/Router.tsx b/client/src/router/Router.tsx index 87a49eff..4ca92453 100644 --- a/client/src/router/Router.tsx +++ b/client/src/router/Router.tsx @@ -4,7 +4,7 @@ import { authContext } from '../context/Auth'; import Collection from '../pages/collections/Collection'; import Collections from '../pages/collections/Collections'; 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 Index from '../pages/index'; import Search from '../pages/search/VectorSearch'; diff --git a/server/src/milvus/milvus.service.ts b/server/src/milvus/milvus.service.ts index 2387860e..5606ab99 100644 --- a/server/src/milvus/milvus.service.ts +++ b/server/src/milvus/milvus.service.ts @@ -56,6 +56,7 @@ export class MilvusService { address: milvusAddress, username, password, + logLevel: "debug" }); // don't break attu diff --git a/server/src/users/dto.ts b/server/src/users/dto.ts index 4b3f5ff1..4412f250 100644 --- a/server/src/users/dto.ts +++ b/server/src/users/dto.ts @@ -8,6 +8,11 @@ export class CreateUserDto { readonly password: string; } +export class CreateRoleDto { + @IsString() + readonly roleName: string; +} + export class UpdateUserDto { @IsString() readonly username: string; diff --git a/server/src/users/users.controller.ts b/server/src/users/users.controller.ts index ed79827f..b27ac8e5 100644 --- a/server/src/users/users.controller.ts +++ b/server/src/users/users.controller.ts @@ -3,7 +3,7 @@ import { dtoValidationMiddleware } from '../middlewares/validation'; import { UserService } from './users.service'; import { milvusService } from '../milvus'; -import { CreateUserDto, UpdateUserDto } from './dto'; +import { CreateUserDto, UpdateUserDto, CreateRoleDto } from './dto'; export class UserController { private router: Router; @@ -31,6 +31,16 @@ export class UserController { 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; } @@ -76,4 +86,33 @@ export class UserController { 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); + } + } } diff --git a/server/src/users/users.service.ts b/server/src/users/users.service.ts index f44e5c05..bd87d503 100644 --- a/server/src/users/users.service.ts +++ b/server/src/users/users.service.ts @@ -3,6 +3,8 @@ import { CreateUserReq, UpdateUserReq, DeleteUserReq, + CreateRoleReq, + DropRoleReq, } from '@zilliz/milvus2-sdk-node'; import { throwErrorFromSDK } from '../utils/Error'; @@ -35,4 +37,26 @@ export class UserService { throwErrorFromSDK(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; + } } From ddc52a325701806107aa381e5bb9e3124496de01 Mon Sep 17 00:00:00 2001 From: shanghaikid Date: Thu, 27 Jul 2023 15:25:22 +0800 Subject: [PATCH 2/2] remove console Signed-off-by: shanghaikid --- client/src/pages/user/User.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/client/src/pages/user/User.tsx b/client/src/pages/user/User.tsx index 4320780b..d2b6452e 100644 --- a/client/src/pages/user/User.tsx +++ b/client/src/pages/user/User.tsx @@ -38,9 +38,6 @@ const Users = () => { const fetchUsers = async () => { const res = await UserHttp.getUsers(); - const roles = await UserHttp.getRoles(); - - console.log('roles', roles); setUsers(res.usernames.map((v: string) => ({ name: v }))); };