Browse Source

update password dialog (#780)

Signed-off-by: ryjiang <jiangruiyi@gmail.com>
ryjiang 4 months ago
parent
commit
f36783e2db

+ 84 - 9
client/src/components/layout/Header.tsx

@@ -1,11 +1,17 @@
-import { FC, useContext } from 'react';
+import { FC, useContext, useState, MouseEvent } from 'react';
 import { useTranslation } from 'react-i18next';
-import { Theme, Typography, Tooltip } from '@mui/material';
+import { Theme } from '@mui/material';
+import Typography from '@mui/material/Typography';
+import Tooltip from '@mui/material/Tooltip';
+import Menu from '@mui/material/Menu';
+import MenuItem from '@mui/material/MenuItem';
 import { useNavigate } from 'react-router-dom';
-import { navContext, dataContext, authContext } from '@/context';
+import { navContext, dataContext, authContext, rootContext } from '@/context';
 import { MilvusService } from '@/http';
 import CustomSelector from '@/components/customSelector/CustomSelector';
-import StatusIcon, { LoadingType } from '@/components/status/StatusIcon';
+import StatusIcon from '@/components/status/StatusIcon';
+import { LoadingType } from '@/components/status/StatusIcon';
+import UpdateUser from '@/pages/user/dialogs/UpdateUserPassDialog';
 import icons from '../icons/Icons';
 import { makeStyles } from '@mui/styles';
 import IconButton from '@mui/material/IconButton';
@@ -85,22 +91,34 @@ const useStyles = makeStyles((theme: Theme) => ({
 }));
 
 const Header: FC = () => {
+  // styles
   const classes = useStyles();
   // use context
   const { navInfo } = useContext(navContext);
   const { mode, toggleColorMode } = useContext(ColorModeContext);
   const { database, databases, setDatabase, loading } = useContext(dataContext);
   const { authReq, logout } = useContext(authContext);
+  const { setDialog, handleCloseDialog, openSnackBar } =
+    useContext(rootContext);
+
   const { address, username } = authReq;
   const navigate = useNavigate();
 
+  // UI states
+  const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
+
+  // i8n
   const { t: commonTrans } = useTranslation();
   const statusTrans = commonTrans('status');
   const { t: dbTrans } = useTranslation('database');
+  const { t: successTrans } = useTranslation('success');
+
+  // icons
   const BackIcon = icons.back;
   const LogoutIcon = icons.logout;
   const Avatar = icons.avatar;
 
+  // UI handlers
   const handleBack = (path: string) => {
     navigate(path);
   };
@@ -113,6 +131,41 @@ const Header: FC = () => {
     await MilvusService.useDatabase({ database });
   };
 
+  const handleUserMenuClick = (event: MouseEvent<HTMLDivElement>) => {
+    setAnchorEl(event.currentTarget);
+  };
+
+  const handleUserMenuClose = () => {
+    setAnchorEl(null);
+  };
+
+  const handleChangePassword = () => {
+    setAnchorEl(null);
+    setDialog({
+      open: true,
+      type: 'custom',
+      params: {
+        component: (
+          <UpdateUser
+            username={username}
+            onUpdate={res => {
+              if (res.error_code === 'Success') {
+                openSnackBar(successTrans('passwordChanged'));
+                handleCloseDialog();
+                setAnchorEl(null);
+                logout();
+              } else {
+                openSnackBar(res.detail, 'error');
+              }
+            }}
+            handleClose={handleCloseDialog}
+          />
+        ),
+      },
+    });
+  };
+
+  // local computes
   const dbOptions = databases.map(d => ({ value: d.name, label: d.name }));
   const isLoadingDb = dbOptions.length === 0;
 
@@ -173,11 +226,33 @@ const Header: FC = () => {
             <Typography className="status">{statusTrans.running}</Typography>
           </div>
           {username && (
-            <Tooltip title={username}>
-              <div>
-                <Avatar classes={{ root: classes.icon }} />
-              </div>
-            </Tooltip>
+            <>
+              <Tooltip title={username}>
+                <div
+                  onClick={handleUserMenuClick}
+                  style={{ cursor: 'pointer' }}
+                >
+                  <Avatar classes={{ root: classes.icon }} />
+                </div>
+              </Tooltip>
+              <Menu
+                anchorEl={anchorEl}
+                open={Boolean(anchorEl)}
+                onClose={handleUserMenuClose}
+                anchorOrigin={{
+                  vertical: 'bottom',
+                  horizontal: 'right',
+                }}
+                transformOrigin={{
+                  vertical: 'top',
+                  horizontal: 'right',
+                }}
+              >
+                <MenuItem onClick={handleChangePassword}>
+                  Change Password
+                </MenuItem>
+              </Menu>
+            </>
           )}
           <Tooltip title={'disconnect'}>
             <div>

+ 1 - 0
client/src/i18n/cn/success.ts

@@ -10,6 +10,7 @@ const successTrans = {
   empty: `{{name}}清空已经开始.`,
   reset: `{{name}}重置成功。`,
   modifyReplica: `{{name}}修改副本数量成功。`,
+  passwordChanged: `密码更新成功`,
 };
 
 export default successTrans;

+ 1 - 0
client/src/i18n/cn/user.ts

@@ -1,6 +1,7 @@
 const userTrans = {
   createTitle: '创建用户',
   updateTitle: '更新Milvus用户',
+  updateUserPassTitle: `更新 {{username}} 的密码`,
   updateRoleTitle: '更新用户角色',
   user: '用户',
   users: '用户们',

+ 1 - 0
client/src/i18n/en/success.ts

@@ -10,6 +10,7 @@ const successTrans = {
   empty: `Emptying data for {{name}} has started.`,
   reset: `{{name}} has been reset.`,
   modifyReplica: `Replica number for {{name}} has been modified.`,
+  passwordChanged: `Password updated successfully`,
 };
 
 export default successTrans;

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

@@ -1,6 +1,7 @@
 const userTrans = {
   createTitle: 'Create User',
   updateTitle: 'Update Milvus User',
+  updateUserPassTitle: `Update {{username}}'s Password`,
   updateRoleTitle: 'Update User Roles',
   user: 'User',
   users: 'Users',

+ 0 - 35
client/src/pages/databases/tree/style.ts

@@ -6,43 +6,8 @@ export const useStyles = makeStyles((theme: Theme) => ({
     fontSize: '15px',
     color: theme.palette.text.primary,
     backgroundColor: theme.palette.background.default,
-    '& .MuiTreeItem-iconContainer': {
-      width: 'auto',
-    },
-    '& .MuiTreeItem-group': {
-      marginLeft: 0,
-      '& .MuiTreeItem-content': {
-        padding: '0 0 0 8px',
-      },
-    },
-    '& .MuiTreeItem-label:hover': {
-      backgroundColor: 'none',
-    },
-    '& .MuiTreeItem-content': {
-      width: 'auto',
-      padding: '0',
-      '&.Mui-focused': {
-        backgroundColor: 'rgba(10, 206, 130, 0.08)',
-      },
-      '&.Mui-selected': {
-        backgroundColor: 'rgba(10, 206, 130, 0.28)',
-      },
-      '&.Mui-focused.Mui-selected': {
-        backgroundColor: 'rgba(10, 206, 130, 0.28) !important',
-      },
-
-      '&:hover': {
-        backgroundColor: 'rgba(10, 206, 130, 0.08)',
-      },
-      '& .MuiTreeItem-label': {
-        background: 'none',
-      },
-    },
   },
   treeItem: {
-    '& .MuiTreeItem-iconContainer': {
-      color: '#666',
-    },
     '& .right-selected-on': {
       '& .MuiTreeItem-content': {
         backgroundColor: 'rgba(10, 206, 130, 0.08)',

+ 1 - 1
client/src/pages/user/Types.ts

@@ -29,7 +29,7 @@ export interface CreateUserProps {
 }
 
 export interface UpdateUserProps {
-  handleUpdate: (data: UpdateUserParams) => void;
+  onUpdate: (res: any) => void;
   handleClose: () => void;
   username: string;
 }

+ 9 - 6
client/src/pages/user/User.tsx

@@ -92,11 +92,14 @@ const Users = () => {
     handleCloseDialog();
   };
 
-  const handleUpdate = async (data: UpdateUserParams) => {
-    await UserService.updateUser(data);
-    fetchUsers();
-    openSnackBar(successTrans('update', { name: userTrans('user') }));
-    handleCloseDialog();
+  const onUpdateUserPass = async (res: any) => {
+    if (res.error_code === 'Success') {
+      openSnackBar(successTrans('passwordChanged'));
+      fetchUsers();
+      handleCloseDialog();
+    } else {
+      openSnackBar(res.detail, 'error');
+    }
   };
 
   const handleDelete = async () => {
@@ -149,7 +152,7 @@ const Users = () => {
             component: (
               <UpdateUser
                 username={selectedUser[0]!.username}
-                handleUpdate={handleUpdate}
+                onUpdate={onUpdateUserPass}
                 handleClose={handleCloseDialog}
               />
             ),

+ 34 - 16
client/src/pages/user/dialogs/UpdateUserPassDialog.tsx

@@ -6,10 +6,14 @@ import CustomInput from '@/components/customInput/CustomInput';
 import { useFormValidation } from '@/hooks';
 import { formatForm } from '@/utils';
 import { makeStyles } from '@mui/styles';
+import { UserService } from '@/http';
 import type { UpdateUserParams, UpdateUserProps } from '../Types';
 import type { ITextfieldConfig } from '@/components/customInput/Types';
 
 const useStyles = makeStyles((theme: Theme) => ({
+  root: {
+    maxWidth: 480,
+  },
   input: {
     margin: theme.spacing(1, 0, 0.5),
   },
@@ -17,13 +21,18 @@ const useStyles = makeStyles((theme: Theme) => ({
 
 const UpdateUser: FC<UpdateUserProps> = ({
   handleClose,
-  handleUpdate,
+  onUpdate,
   username,
 }) => {
+  // styles
+  const classes = useStyles();
+
+  // i18n
   const { t: userTrans } = useTranslation('user');
   const { t: btnTrans } = useTranslation('btn');
   const { t: warningTrans } = useTranslation('warning');
 
+  // UI state
   const [form, setForm] = useState<
     Omit<UpdateUserParams, 'username'> & { confirmPassword: string }
   >({
@@ -31,14 +40,14 @@ const UpdateUser: FC<UpdateUserProps> = ({
     newPassword: '',
     confirmPassword: '',
   });
+
+  // UI handlers
   const checkedForm = useMemo(() => {
-    const { oldPassword, newPassword } = form;
-    return formatForm({ oldPassword, newPassword });
-  }, [form]);
+    const { oldPassword, newPassword, confirmPassword } = form;
+    return formatForm({ oldPassword, newPassword, confirmPassword });
+  }, [JSON.stringify(form)]);
   const { validation, checkIsValid, disabled } = useFormValidation(checkedForm);
 
-  const classes = useStyles();
-
   const handleInputChange = (
     key: 'oldPassword' | 'newPassword' | 'confirmPassword',
     value: string
@@ -46,6 +55,16 @@ const UpdateUser: FC<UpdateUserProps> = ({
     setForm(v => ({ ...v, [key]: value }));
   };
 
+  const handleUpdateUser = async () => {
+    const res = await UserService.updateUser({
+      username,
+      newPassword: form.newPassword,
+      oldPassword: form.oldPassword,
+    });
+    onUpdate(res);
+  };
+
+  // UI configs
   const createConfigs: ITextfieldConfig[] = [
     {
       label: userTrans('oldPassword'),
@@ -94,6 +113,12 @@ const UpdateUser: FC<UpdateUserProps> = ({
       placeholder: userTrans('confirmPassword'),
       fullWidth: true,
       validations: [
+        {
+          rule: 'require',
+          errorText: warningTrans('required', {
+            name: userTrans('newPassword'),
+          }),
+        },
         {
           rule: 'confirm',
           extraParam: {
@@ -107,19 +132,12 @@ const UpdateUser: FC<UpdateUserProps> = ({
     },
   ];
 
-  const handleUpdateUser = () => {
-    handleUpdate({
-      username,
-      newPassword: form.newPassword,
-      oldPassword: form.oldPassword,
-    });
-  };
-
   return (
     <DialogTemplate
-      title={userTrans('updateTitle')}
+      dialogClass={classes.root}
+      title={userTrans('updateUserPassTitle', { username })}
       handleClose={handleClose}
-      confirmLabel={btnTrans('create')}
+      confirmLabel={btnTrans('update')}
       handleConfirm={handleUpdateUser}
       confirmDisabled={disabled}
     >

+ 79 - 0
client/src/styles/theme.ts

@@ -224,6 +224,85 @@ export const getAttuTheme = (mode: PaletteMode) => {
           },
         },
       },
+      MuiTreeItem: {
+        styleOverrides: {
+          root: {
+            fontSize: '15px',
+            color: commonThemes.palette.primary,
+            backgroundColor: commonThemes.palette.background.default,
+            '& .MuiTreeItem-iconContainer': {
+              width: 'auto',
+              color: '#666',
+            },
+            '& .MuiTreeItem-group': {
+              marginLeft: 0,
+              '& .MuiTreeItem-content': {
+                padding: '0 0 0 8px',
+              },
+            },
+            '& .MuiTreeItem-label:hover': {
+              backgroundColor: 'none',
+            },
+            '& .MuiTreeItem-content': {
+              width: 'auto',
+              padding: '0',
+              '&.Mui-focused': {
+                backgroundColor: 'rgba(10, 206, 130, 0.08)',
+              },
+              '&.Mui-selected': {
+                backgroundColor: 'rgba(10, 206, 130, 0.28)',
+              },
+              '&.Mui-focused.Mui-selected': {
+                backgroundColor: 'rgba(10, 206, 130, 0.28) !important',
+              },
+              '&:hover': {
+                backgroundColor: 'rgba(10, 206, 130, 0.08)',
+              },
+              '& .MuiTreeItem-label': {
+                background: 'none',
+              },
+            },
+          },
+        },
+      },
+      MuiMenu: {
+        styleOverrides: {
+          paper: {
+            backgroundColor: commonThemes.palette.background.paper,
+            boxShadow: '0px 5px 15px rgba(0, 0, 0, 0.15)',
+            borderRadius: '8px',
+          },
+          list: {
+            padding: '8px 0',
+          },
+        },
+      },
+      MuiMenuItem: {
+        styleOverrides: {
+          root: {
+            fontSize: '14px',
+            padding: '8px 16px',
+            minHeight: '36px',
+            transition: 'background-color 0.2s ease',
+            '&:hover': {
+              backgroundColor: 'rgba(10, 206, 130, 0.08)',
+            },
+            '&.Mui-selected': {
+              backgroundColor: 'rgba(10, 206, 130, 0.16)',
+              '&:hover': {
+                backgroundColor: 'rgba(10, 206, 130, 0.24)',
+              },
+            },
+            '&.Mui-disabled': {
+              opacity: 0.6,
+            },
+            '& .MuiListItemIcon-root': {
+              color: mode === 'light' ? '#666' : '#aaa',
+              minWidth: '36px',
+            },
+          },
+        },
+      },
     },
   };
 };