Browse Source

support update password

Signed-off-by: nameczz <zizhao.chen@zilliz.com>
nameczz 3 years ago
parent
commit
fb7e5dd0a2

+ 41 - 18
client/src/components/grid/ActionBar.tsx

@@ -1,5 +1,11 @@
 import { FC } from 'react';
-import { IconButton, makeStyles, Theme, createStyles } from '@material-ui/core';
+import {
+  IconButton,
+  makeStyles,
+  Theme,
+  createStyles,
+  Button,
+} from '@material-ui/core';
 import Icons from '../icons/Icons';
 import { ActionBarType } from './Types';
 import CustomToolTip from '../customToolTip/CustomToolTip';
@@ -40,31 +46,48 @@ const ActionBar: FC<ActionBarType> = props => {
 
   return (
     <>
-      {configs.map(v => {
+      {configs.map((v, i) => {
         const label = v.getLabel ? v.getLabel(row) : v.label;
+
         return (
           <span
             className={`${classes.root} ${v.className} ${
               isHoverType ? classes.hoverType : ''
             }`}
-            key={label}
+            key={i}
           >
             <CustomToolTip title={label || ''} placement="bottom">
-              <IconButton
-                aria-label={label || ''}
-                onClickCapture={e => {
-                  e.stopPropagation();
-                  v.onClick(e, row);
-                }}
-                disabled={v.disabled ? v.disabled(row) : false}
-                classes={{
-                  disabled: classes.disabled,
-                }}
-              >
-                {v.showIconMethod === 'renderFn'
-                  ? v.renderIconFn && v.renderIconFn(row)
-                  : Icons[v.icon]()}
-              </IconButton>
+              {v.icon ? (
+                <IconButton
+                  aria-label={label || ''}
+                  onClickCapture={e => {
+                    e.stopPropagation();
+                    v.onClick(e, row);
+                  }}
+                  disabled={v.disabled ? v.disabled(row) : false}
+                  classes={{
+                    disabled: classes.disabled,
+                  }}
+                >
+                  {v.showIconMethod === 'renderFn'
+                    ? v.renderIconFn && v.renderIconFn(row)
+                    : Icons[v.icon]()}
+                </IconButton>
+              ) : (
+                <Button
+                  aria-label={label || ''}
+                  onClickCapture={e => {
+                    e.stopPropagation();
+                    v.onClick(e, row);
+                  }}
+                  disabled={v.disabled ? v.disabled(row) : false}
+                  classes={{
+                    disabled: classes.disabled,
+                  }}
+                >
+                  {v.text}
+                </Button>
+              )}
             </CustomToolTip>
           </span>
         );

+ 2 - 1
client/src/components/grid/Types.ts

@@ -150,7 +150,8 @@ export type ActionBarType = {
 
 type ActionBarConfig = {
   onClick: (e: React.MouseEvent, row: any) => void;
-  icon: IconsType;
+  icon?: IconsType;
+  text?: string;
   showIconMethod?: 'iconType' | 'renderFn';
   renderIconFn?: (row: any) => ReactElement;
   label?: string;

+ 11 - 4
client/src/components/layout/GlobalEffect.tsx

@@ -25,21 +25,28 @@ const GlobalEffect = (props: { children: React.ReactNode }) => {
       },
       function (error: any) {
         const { response = {} } = error;
+        const reset = () => {
+          setIsAuth(false);
+          setAddress('');
+          window.localStorage.removeItem(MILVUS_ADDRESS);
+        };
         switch (response.status) {
           case CODE_STATUS.UNAUTHORIZED:
             return Promise.reject(error);
           case CODE_STATUS.FORBIDDEN:
-            setIsAuth(false);
-            setAddress('');
-            window.localStorage.removeItem(MILVUS_ADDRESS);
+            reset();
             break;
           default:
             break;
         }
         if (response.data) {
           const { message: errMsg } = response.data;
-
+          // We need check status 401 in login page
+          // So server will return 500 when change the user password.
           errMsg && openSnackBar(errMsg, 'error');
+          if (errMsg.includes('unauthenticated')) {
+            reset();
+          }
           return Promise.reject(error);
         }
         if (error.message) {

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

@@ -2,6 +2,11 @@ const userTrans = {
   createTitle: 'Create user',
   user: 'User',
   deleteWarning: 'You are trying to delete user. This action cannot be undone.',
+  oldPassword: 'Current Password',
+  newPassword: 'New Password',
+  confirmPassword: 'Confirm Password',
+  update: 'Update password',
+  isNotSame: 'Confirm password is not same as new password',
 };
 
 export default userTrans;

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

@@ -4,6 +4,7 @@ const successTrans = {
   load: `{{name}} has been loaded`,
   delete: `{{name}} successfully deleted`,
   release: `{{name}} has been released`,
+  update: `{{name}} has been updated`,
 };
 
 export default successTrans;

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

@@ -2,6 +2,11 @@ const userTrans = {
   createTitle: 'Create user',
   user: 'User',
   deleteWarning: 'You are trying to delete user. This action cannot be undone.',
+  oldPassword: 'Current Password',
+  newPassword: 'New Password',
+  confirmPassword: 'Confirm Password',
+  update: 'Update password',
+  isNotSame: 'Not same as new password',
 };
 
 export default userTrans;

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

@@ -10,11 +10,16 @@ export interface CreateUserProps {
   handleCreate: (data: CreateUserParams) => void;
   handleClose: () => void;
 }
+export interface UpdateUserProps {
+  handleUpdate: (data: UpdateUserParams) => void;
+  handleClose: () => void;
+  username: string;
+}
 
 export interface UpdateUserParams {
-  username: string;
   oldPassword: string;
   newPassword: string;
+  username: string;
 }
 
 export interface DeleteUserParams {

+ 140 - 0
client/src/pages/user/Update.tsx

@@ -0,0 +1,140 @@
+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 { UpdateUserParams, UpdateUserProps } from './Types';
+
+const useStyles = makeStyles((theme: Theme) => ({
+  input: {
+    margin: theme.spacing(3, 0, 0.5),
+  },
+}));
+
+const UpdateUser: FC<UpdateUserProps> = ({
+  handleClose,
+  handleUpdate,
+  username,
+}) => {
+  const { t: userTrans } = useTranslation('user');
+  const { t: btnTrans } = useTranslation('btn');
+  const { t: warningTrans } = useTranslation('warning');
+
+  const [form, setForm] = useState<
+    Omit<UpdateUserParams, 'username'> & { confirmPassword: string }
+  >({
+    oldPassword: '',
+    newPassword: '',
+    confirmPassword: '',
+  });
+  const checkedForm = useMemo(() => {
+    const { oldPassword, newPassword } = form;
+    return formatForm({ oldPassword, newPassword });
+  }, [form]);
+  const { validation, checkIsValid, disabled } = useFormValidation(checkedForm);
+
+  const classes = useStyles();
+
+  const handleInputChange = (
+    key: 'oldPassword' | 'newPassword' | 'confirmPassword',
+    value: string
+  ) => {
+    setForm(v => ({ ...v, [key]: value }));
+  };
+
+  const createConfigs: ITextfieldConfig[] = [
+    {
+      label: userTrans('oldPassword'),
+      key: 'oldPassword',
+      onChange: (value: string) => handleInputChange('oldPassword', value),
+      variant: 'filled',
+      className: classes.input,
+      placeholder: userTrans('oldPassword'),
+      fullWidth: true,
+      validations: [
+        {
+          rule: 'require',
+          errorText: warningTrans('required', {
+            name: userTrans('oldPassword'),
+          }),
+        },
+      ],
+      type: 'password',
+      defaultValue: form.oldPassword,
+    },
+    {
+      label: userTrans('newPassword'),
+      key: 'newPassword',
+      onChange: (value: string) => handleInputChange('newPassword', value),
+      variant: 'filled',
+      className: classes.input,
+      placeholder: userTrans('newPassword'),
+      fullWidth: true,
+      validations: [
+        {
+          rule: 'require',
+          errorText: warningTrans('required', {
+            name: userTrans('newPassword'),
+          }),
+        },
+      ],
+      type: 'password',
+      defaultValue: form.newPassword,
+    },
+    {
+      label: userTrans('confirmPassword'),
+      key: 'confirmPassword',
+      onChange: (value: string) => handleInputChange('confirmPassword', value),
+      variant: 'filled',
+      className: classes.input,
+      placeholder: userTrans('confirmPassword'),
+      fullWidth: true,
+      validations: [
+        {
+          rule: 'confirm',
+          extraParam: {
+            compareValue: form.newPassword,
+          },
+          errorText: userTrans('isNotSame'),
+        },
+      ],
+      type: 'password',
+      defaultValue: form.confirmPassword,
+    },
+  ];
+
+  const handleUpdateUser = () => {
+    handleUpdate({
+      username,
+      newPassword: form.newPassword,
+      oldPassword: form.oldPassword,
+    });
+  };
+
+  return (
+    <DialogTemplate
+      title={userTrans('createTitle')}
+      handleClose={handleClose}
+      confirmLabel={btnTrans('create')}
+      handleConfirm={handleUpdateUser}
+      confirmDisabled={disabled}
+    >
+      <form>
+        {createConfigs.map(v => (
+          <CustomInput
+            type="text"
+            textConfig={v}
+            checkValid={checkIsValid}
+            validInfo={validation}
+            key={v.label}
+          />
+        ))}
+      </form>
+    </DialogTemplate>
+  );
+};
+
+export default UpdateUser;

+ 47 - 11
client/src/pages/user/User.tsx

@@ -6,23 +6,25 @@ import {
   ToolBarConfig,
 } from 'insight_src/components/grid/Types';
 import { makeStyles, Theme } from '@material-ui/core';
-import { CreateUserParams, DeleteUserParams, UserData } from './Types';
+import {
+  CreateUserParams,
+  DeleteUserParams,
+  UpdateUserParams,
+  UserData,
+} from './Types';
 import { rootContext } from 'insight_src/context/Root';
 import CreateUser from './Create';
 import { useTranslation } from 'react-i18next';
 import DeleteTemplate from 'insight_src/components/customDialog/DeleteDialogTemplate';
+import UpdateUser from './Update';
 
 const useStyles = makeStyles((theme: Theme) => ({
-  wrapper: {
-    height: '100%',
-  },
-  icon: {
-    fontSize: '20px',
-    marginLeft: theme.spacing(0.5),
-  },
-  highlight: {
-    color: theme.palette.primary.main,
-    backgroundColor: 'transparent',
+  actionButton: {
+    position: 'relative',
+    left: ' -10px',
+    '& .MuiButton-root': {
+      color: theme.palette.primary.main,
+    },
   },
 }));
 
@@ -50,6 +52,13 @@ const Users = () => {
     handleCloseDialog();
   };
 
+  const handleUpdate = async (data: UpdateUserParams) => {
+    await UserHttp.updateUser(data);
+    fetchUsers();
+    openSnackBar(successTrans('update', { name: userTrans('user') }));
+    handleCloseDialog();
+  };
+
   const handleDelete = async () => {
     for (const user of selectedUser) {
       const param: DeleteUserParams = {
@@ -113,6 +122,33 @@ const Users = () => {
       disablePadding: false,
       label: 'Name',
     },
+    {
+      id: 'action',
+      disablePadding: false,
+      label: 'Action',
+      showActionCell: true,
+      actionBarConfigs: [
+        {
+          onClick: (e: React.MouseEvent, row: UserData) => {
+            setDialog({
+              open: true,
+              type: 'custom',
+              params: {
+                component: (
+                  <UpdateUser
+                    username={row.name}
+                    handleUpdate={handleUpdate}
+                    handleClose={handleCloseDialog}
+                  />
+                ),
+              },
+            });
+          },
+          text: 'Update password',
+          className: classes.actionButton,
+        },
+      ],
+    },
   ];
 
   const handleSelectChange = (value: UserData[]) => {

+ 2 - 0
server/src/middlewares/index.ts

@@ -4,6 +4,7 @@ import chalk from 'chalk';
 import { MilvusService } from '../milvus/milvus.service';
 import { INSIGHT_CACHE, MILVUS_ADDRESS } from '../utils/Const';
 import { HttpError } from 'http-errors';
+import { HTTP_STATUS_CODE } from '../utils/Error';
 
 export const ReqHeaderMiddleware = (
   req: Request,
@@ -70,6 +71,7 @@ export const ErrorMiddleware = (
   if (res.headersSent) {
     return next(err);
   }
+
   if (err) {
     res
       .status(statusCode)

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

@@ -81,6 +81,10 @@ export class MilvusService {
     } catch (error) {
       // if milvus is not working, delete connection.
       cache.del(milvusAddress);
+      /**
+       * When user change the user password, milvus will also return unauthenticated error.
+       * Need to care it in cloud service.
+       */
       if (error.toString().includes('unauthenticated')) {
         throw HttpErrors(HTTP_STATUS_CODE.UNAUTHORIZED, error);
       }