Răsfoiți Sursa

Merge remote-tracking branch 'upstream/main' into issue-56

iynewz 3 ani în urmă
părinte
comite
8a6c57ce6f

+ 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>
         );

+ 4 - 3
client/src/components/grid/Grid.tsx

@@ -123,12 +123,13 @@ const AttuGrid: FC<AttuGridType> = props => {
     headEditable = false,
     editHeads = [],
     selected = [],
-    setSelected = () => { },
-    setRowsPerPage = () => { },
+    setSelected = () => {},
+    setRowsPerPage = () => {},
     tableCellMaxWidth,
     handleSort,
     order,
     orderBy,
+    showPagination = true,
   } = props;
 
   const _isSelected = (row: { [x: string]: any }) => {
@@ -225,7 +226,7 @@ const AttuGrid: FC<AttuGridType> = props => {
           order={order}
           orderBy={orderBy}
         ></Table>
-        {rowCount ? (
+        {rowCount && showPagination ? (
           <TablePagination
             component="div"
             colSpan={3}

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

@@ -115,6 +115,7 @@ export type ColDefinitionsType = {
 };
 
 export type AttuGridType = ToolBarType & {
+  showPagination?: boolean;
   rowCount: number;
   rowsPerPage?: number;
   // used to dynamic set page size by table container and row height
@@ -149,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;

+ 10 - 0
client/src/components/icons/Icons.tsx

@@ -24,6 +24,8 @@ import FilterListIcon from '@material-ui/icons/FilterList';
 import AlternateEmailIcon from '@material-ui/icons/AlternateEmail';
 import DatePicker from '@material-ui/icons/Event';
 import GetAppIcon from '@material-ui/icons/GetApp';
+// import PersonOutlineIcon from '@material-ui/icons/PersonOutline';
+import PersonOutlineIcon from '@material-ui/icons/Person';
 import { SvgIcon } from '@material-ui/core';
 import { ReactComponent as ZillizIcon } from '../../assets/icons/zilliz.svg';
 import { ReactComponent as OverviewIcon } from '../../assets/icons/overview.svg';
@@ -68,6 +70,14 @@ const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
   zilliz: (props = {}) => (
     <SvgIcon viewBox="0 0 30 30" component={ZillizIcon} {...props} />
   ),
+  navPerson: (props = {}) => (
+    <SvgIcon
+      viewBox="0 0 24 24"
+      component={PersonOutlineIcon}
+      strokeWidth="2"
+      {...props}
+    />
+  ),
   navOverview: (props = {}) => (
     <SvgIcon viewBox="0 0 20 20" component={OverviewIcon} {...props} />
   ),

+ 1 - 0
client/src/components/icons/Types.ts

@@ -17,6 +17,7 @@ export type IconsType =
   | 'navConsole'
   | 'navSearch'
   | 'navSystem'
+  | 'navPerson'
   | 'expandLess'
   | 'expandMore'
   | 'back'

+ 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) {

+ 9 - 5
client/src/components/layout/Layout.tsx

@@ -66,6 +66,10 @@ const Layout = (props: any) => {
       return navTrans('system');
     }
 
+    if (location.pathname.includes('users')) {
+      return navTrans('user');
+    }
+
     return navTrans('overview');
   }, [location, navTrans]);
 
@@ -75,11 +79,11 @@ const Layout = (props: any) => {
       label: navTrans('overview'),
       onClick: () => history.push('/'),
     },
-    // {
-    //   icon: icons.navSystem,
-    //   label: navTrans('system'),
-    //   onClick: () => history.push('/system'),
-    // },
+    {
+      icon: icons.navPerson,
+      label: navTrans('user'),
+      onClick: () => history.push('/users'),
+    },
     {
       icon: icons.navCollection,
       label: navTrans('collection'),

+ 1 - 1
client/src/context/Root.tsx

@@ -11,7 +11,7 @@ import {
 import CustomSnackBar from '../components/customSnackBar/CustomSnackBar';
 import CustomDialog from '../components/customDialog/CustomDialog';
 import { theme } from '../styles/theme';
-import { MilvusHttp } from 'insight_src/http/Milvus';
+import { MilvusHttp } from '../http/Milvus';
 
 const DefaultDialogConfigs: DialogType = {
   open: false,

+ 8 - 0
client/src/hooks/Navigation.ts

@@ -58,6 +58,14 @@ export const useNavigationHook = (
         setNavInfo(navInfo);
         break;
       }
+      case ALL_ROUTER_TYPES.USER: {
+        const navInfo: NavInfo = {
+          navTitle: navTrans('user'),
+          backPath: '',
+        };
+        setNavInfo(navInfo);
+        break;
+      }
       case ALL_ROUTER_TYPES.PLUGIN: {
         const navInfo: NavInfo = {
           navTitle: title,

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

@@ -0,0 +1,37 @@
+import {
+  CreateUserParams,
+  DeleteUserParams,
+  UpdateUserParams,
+} from 'insight_src/pages/user/Types';
+import BaseModel from './BaseModel';
+
+export class UserHttp extends BaseModel {
+  private names!: string[];
+
+  constructor(props: {}) {
+    super(props);
+    Object.assign(this, props);
+  }
+
+  static USER_URL = `/users`;
+
+  static getUsers() {
+    return super.search({ path: this.USER_URL, params: {} });
+  }
+
+  static createUser(data: CreateUserParams) {
+    return super.create({ path: this.USER_URL, data });
+  }
+
+  static updateUser(data: UpdateUserParams) {
+    return super.update({ path: this.USER_URL, data });
+  }
+
+  static deleteUser(data: DeleteUserParams) {
+    return super.delete({ path: `${this.USER_URL}/${data.username}` });
+  }
+
+  get _names() {
+    return this.names;
+  }
+}

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

@@ -4,6 +4,7 @@ const navTrans = {
   console: 'Search Console',
   search: 'Vector Search',
   system: 'System View',
+  user: 'User',
 };
 
 export default navTrans;

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

@@ -0,0 +1,15 @@
+const userTrans = {
+  createTitle: 'Create User',
+  updateTitle: 'Update 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',
+  deleteTip:
+    'Please select at least one item to delete and root can not be deleted.',
+};
+
+export default userTrans;

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

@@ -4,6 +4,7 @@ const navTrans = {
   console: 'Search Console',
   search: 'Vector Search',
   system: 'System View',
+  user: 'User',
 };
 
 export default navTrans;

+ 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;

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

@@ -0,0 +1,15 @@
+const userTrans = {
+  createTitle: 'Create User',
+  updateTitle: 'Update 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',
+  deleteTip:
+    'Please select at least one item to delete and root can not be deleted.',
+};
+
+export default userTrans;

+ 5 - 1
client/src/i18n/index.ts

@@ -27,6 +27,8 @@ import searchEn from './en/search';
 import searchCn from './cn/search';
 import systemViewTransEn from './en/systemView';
 import systemViewTransCn from './cn/systemView';
+import userTransEn from './en/user';
+import userTransCn from './cn/user';
 
 export const resources = {
   cn: {
@@ -43,6 +45,7 @@ export const resources = {
     insert: insertCn,
     search: searchCn,
     systemView: systemViewTransCn,
+    user: userTransCn,
   },
   en: {
     translation: commonEn,
@@ -57,7 +60,8 @@ export const resources = {
     index: indexEn,
     insert: insertEn,
     search: searchEn,
-    systemView: systemViewTransEn
+    systemView: systemViewTransEn,
+    user: userTransEn,
   },
 };
 

+ 5 - 5
client/src/pages/connect/AuthForm.tsx

@@ -9,14 +9,14 @@ import { ITextfieldConfig } from '../../components/customInput/Types';
 import { useFormValidation } from '../../hooks/Form';
 import { formatForm } from '../../utils/Form';
 import { MilvusHttp } from '../../http/Milvus';
-import { formatAddress } from 'insight_src/utils/Format';
+import { formatAddress } from '../../utils/Format';
 import { useHistory } from 'react-router-dom';
 import { rootContext } from '../../context/Root';
 import { authContext } from '../../context/Auth';
-import { MILVUS_ADDRESS } from 'insight_src/consts/Localstorage';
-import { CODE_STATUS } from 'insight_src/consts/Http';
-import { MILVUS_URL } from 'insight_src/consts/Milvus';
-import { CustomRadio } from 'insight_src/components/customRadio/CustomRadio';
+import { MILVUS_ADDRESS } from '../../consts/Localstorage';
+import { CODE_STATUS } from '../../consts/Http';
+import { MILVUS_URL } from '../../consts/Milvus';
+import { CustomRadio } from '../../components/customRadio/CustomRadio';
 
 const useStyles = makeStyles((theme: Theme) => ({
   wrapper: {

+ 0 - 25
client/src/pages/partitions/Partitions.tsx

@@ -58,10 +58,7 @@ const Partitions: FC<{
   const InfoIcon = icons.info;
 
   const { handleInsertDialog } = useInsertDialogHook();
-  // const LoadIcon = icons.load;
-  // const ReleaseIcon = icons.release;
 
-  // const { handleAction } = useDialogHook({ type: 'partition' });
   const [selectedPartitions, setSelectedPartitions] = useState<PartitionView[]>(
     []
   );
@@ -138,28 +135,6 @@ const Partitions: FC<{
     handleCloseDialog();
   };
 
-  // const handleRelease = async (data: PartitionView) => {
-  //   const param: PartitionParam = {
-  //     collectionName,
-  //     partitionNames: [data._name],
-  //   };
-  //   const res = await PartitionHttp.releasePartition(param);
-  //   openSnackBar(successTrans('release', { name: t('partition') }));
-  //   fetchPartitions(collectionName);
-  //   return res;
-  // };
-
-  // const handleLoad = async (data: PartitionView) => {
-  //   const param: PartitionParam = {
-  //     collectionName,
-  //     partitionNames: [data._name!],
-  //   };
-  //   const res = await PartitionHttp.loadPartition(param);
-  //   openSnackBar(successTrans('load', { name: t('partition') }));
-  //   fetchPartitions(collectionName);
-  //   return res;
-  // };
-
   const handleSearch = (value: string) => {
     if (timer) {
       clearTimeout(timer);

+ 106 - 0
client/src/pages/user/Create.tsx

@@ -0,0 +1,106 @@
+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 { CreateUserProps, CreateUserParams } from './Types';
+
+const useStyles = makeStyles((theme: Theme) => ({
+  input: {
+    margin: theme.spacing(3, 0, 0.5),
+  },
+}));
+
+const CreateUser: FC<CreateUserProps> = ({ 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<CreateUserParams>({
+    username: '',
+    password: '',
+  });
+  const checkedForm = useMemo(() => {
+    return formatForm(form);
+  }, [form]);
+  const { validation, checkIsValid, disabled } = useFormValidation(checkedForm);
+
+  const classes = useStyles();
+
+  const handleInputChange = (key: 'username' | 'password', value: string) => {
+    setForm(v => ({ ...v, [key]: value }));
+  };
+
+  const createConfigs: ITextfieldConfig[] = [
+    {
+      label: attuTrans.username,
+      key: 'username',
+      onChange: (value: string) => handleInputChange('username', value),
+      variant: 'filled',
+      className: classes.input,
+      placeholder: attuTrans.username,
+      fullWidth: true,
+      validations: [
+        {
+          rule: 'require',
+          errorText: warningTrans('required', {
+            name: attuTrans.username,
+          }),
+        },
+      ],
+      defaultValue: form.username,
+    },
+    {
+      label: attuTrans.password,
+      key: 'password',
+      onChange: (value: string) => handleInputChange('password', value),
+      variant: 'filled',
+      className: classes.input,
+      placeholder: attuTrans.password,
+      fullWidth: true,
+      type: 'password',
+      validations: [
+        {
+          rule: 'require',
+          errorText: warningTrans('required', {
+            name: attuTrans.password,
+          }),
+        },
+      ],
+      defaultValue: form.username,
+    },
+  ];
+
+  const handleCreateUser = () => {
+    handleCreate(form);
+  };
+
+  return (
+    <DialogTemplate
+      title={userTrans('createTitle')}
+      handleClose={handleClose}
+      confirmLabel={btnTrans('create')}
+      handleConfirm={handleCreateUser}
+      confirmDisabled={disabled}
+    >
+      <form>
+        {createConfigs.map(v => (
+          <CustomInput
+            type="text"
+            textConfig={v}
+            checkValid={checkIsValid}
+            validInfo={validation}
+            key={v.label}
+          />
+        ))}
+      </form>
+    </DialogTemplate>
+  );
+};
+
+export default CreateUser;

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

@@ -0,0 +1,27 @@
+export interface UserData {
+  name: string;
+}
+export interface CreateUserParams {
+  username: string;
+  password: string;
+}
+
+export interface CreateUserProps {
+  handleCreate: (data: CreateUserParams) => void;
+  handleClose: () => void;
+}
+export interface UpdateUserProps {
+  handleUpdate: (data: UpdateUserParams) => void;
+  handleClose: () => void;
+  username: string;
+}
+
+export interface UpdateUserParams {
+  oldPassword: string;
+  newPassword: string;
+  username: string;
+}
+
+export interface DeleteUserParams {
+  username: string;
+}

+ 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('updateTitle')}
+      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;

+ 195 - 0
client/src/pages/user/User.tsx

@@ -0,0 +1,195 @@
+import { UserHttp } from '../../http/User';
+import React, { useContext, useEffect, useState } from 'react';
+import AttuGrid from 'insight_src/components/grid/Grid';
+import {
+  ColDefinitionsType,
+  ToolBarConfig,
+} from 'insight_src/components/grid/Types';
+import { makeStyles, Theme } from '@material-ui/core';
+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';
+import { useNavigationHook } from 'insight_src/hooks/Navigation';
+import { ALL_ROUTER_TYPES } from 'insight_src/router/Types';
+
+const useStyles = makeStyles((theme: Theme) => ({
+  actionButton: {
+    position: 'relative',
+    left: ' -10px',
+    '& .MuiButton-root': {
+      color: theme.palette.primary.main,
+    },
+  },
+}));
+
+const Users = () => {
+  useNavigationHook(ALL_ROUTER_TYPES.USER);
+
+  const classes = useStyles();
+  const [users, setUsers] = useState<UserData[]>([]);
+  const [selectedUser, setSelectedUser] = useState<UserData[]>([]);
+  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 fetchUsers = async () => {
+    const res = await UserHttp.getUsers();
+
+    setUsers(res.usernames.map((v: string) => ({ name: v })));
+  };
+
+  const handleCreate = async (data: CreateUserParams) => {
+    await UserHttp.createUser(data);
+    fetchUsers();
+    openSnackBar(successTrans('create', { name: userTrans('user') }));
+    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 = {
+        username: user.name,
+      };
+      await UserHttp.deleteUser(param);
+    }
+
+    openSnackBar(successTrans('delete', { name: userTrans('user') }));
+    fetchUsers();
+    handleCloseDialog();
+  };
+
+  const toolbarConfigs: ToolBarConfig[] = [
+    {
+      label: 'Create user',
+      onClick: () => {
+        setDialog({
+          open: true,
+          type: 'custom',
+          params: {
+            component: (
+              <CreateUser
+                handleCreate={handleCreate}
+                handleClose={handleCloseDialog}
+              />
+            ),
+          },
+        });
+      },
+      icon: 'add',
+    },
+
+    {
+      type: 'iconBtn',
+      onClick: () => {
+        setDialog({
+          open: true,
+          type: 'custom',
+          params: {
+            component: (
+              <DeleteTemplate
+                label={btnTrans('delete')}
+                title={dialogTrans('deleteTitle', { type: userTrans('user') })}
+                text={userTrans('deleteWarning')}
+                handleDelete={handleDelete}
+              />
+            ),
+          },
+        });
+      },
+      label: '',
+      disabled: () =>
+        selectedUser.length === 0 ||
+        selectedUser.findIndex(v => v.name === 'root') > -1,
+      disabledTooltip: userTrans('deleteTip'),
+
+      icon: 'delete',
+    },
+  ];
+
+  const colDefinitions: ColDefinitionsType[] = [
+    {
+      id: 'name',
+      align: 'left',
+      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[]) => {
+    setSelectedUser(value);
+  };
+
+  useEffect(() => {
+    fetchUsers();
+  }, []);
+
+  return (
+    <div className="page-wrapper">
+      <AttuGrid
+        toolbarConfigs={toolbarConfigs}
+        colDefinitions={colDefinitions}
+        rows={users}
+        rowCount={users.length}
+        primaryKey="name"
+        showPagination={false}
+        selected={selectedUser}
+        setSelected={handleSelectChange}
+        // page={currentPage}
+        // onChangePage={handlePageChange}
+        // rowsPerPage={pageSize}
+        // setRowsPerPage={handlePageSize}
+        // isLoading={loading}
+        // order={order}
+        // orderBy={orderBy}
+        // handleSort={handleGridSort}
+      />
+    </div>
+  );
+};
+
+export default Users;

+ 2 - 2
client/src/plugins/search/Types.ts

@@ -3,8 +3,8 @@ import { searchKeywordsType } from '../../consts/Milvus';
 import {
   DataTypeEnum,
   DataTypeStringEnum,
-} from 'insight_src/pages/collections/Types';
-import { IndexView } from 'insight_src/pages/schema/Types';
+} from '../../pages/collections/Types';
+import { IndexView } from '../../pages/schema/Types';
 
 export interface SearchParamsProps {
   // if user created index, pass metric type choosed when creating

+ 3 - 6
client/src/plugins/search/VectorSearch.tsx

@@ -16,10 +16,7 @@ import SimpleMenu from '../../components/menu/SimpleMenu';
 import { TOP_K_OPTIONS } from './Constants';
 import { Option } from '../../components/customSelector/Types';
 import { CollectionHttp } from '../../http/Collection';
-import {
-  CollectionData,
-  DataTypeEnum,
-} from 'insight_src/pages/collections/Types';
+import { CollectionData, DataTypeEnum } from '../../pages/collections/Types';
 import { IndexHttp } from '../../http/Index';
 import { getVectorSearchStyles } from './Styles';
 import { parseValue } from '../../utils/Insert';
@@ -36,8 +33,8 @@ import Filter from '../../components/advancedSearch';
 import { Field } from '../../components/advancedSearch/Types';
 import { useLocation } from 'react-router-dom';
 import { parseLocationSearch } from '../../utils/Format';
-import { CustomDatePicker } from 'insight_src/components/customDatePicker/CustomDatePicker';
-import { useTimeTravelHook } from 'insight_src/hooks/TimeTravel';
+import { CustomDatePicker } from '../../components/customDatePicker/CustomDatePicker';
+import { useTimeTravelHook } from '../../hooks/TimeTravel';
 
 const VectorSearch = () => {
   useNavigationHook(ALL_ROUTER_TYPES.SEARCH);

+ 2 - 5
client/src/router/Config.ts

@@ -5,6 +5,7 @@ import Overview from '../pages/overview/Overview';
 // import VectorSearch from '../pages/seach/VectorSearch';
 import { RouterConfigType } from './Types';
 import loadable from '@loadable/component';
+import Users from '../pages/user/User';
 
 const PLUGIN_DEV = process.env.REACT_APP_PLUGIN_DEV;
 
@@ -29,11 +30,7 @@ const RouterConfig: RouterConfigType[] = [
     component: Collection,
     auth: true,
   },
-  // {
-  //   path: '/search',
-  //   component: VectorSearch,
-  //   auth: true,
-  // },
+  { path: '/users', component: Users, auth: true },
 ];
 
 function importAll(r: any, outOfRoot = false) {

+ 1 - 0
client/src/router/Types.ts

@@ -11,6 +11,7 @@ export enum ALL_ROUTER_TYPES {
   SYSTEM = 'system',
   // plugins
   PLUGIN = 'plugin',
+  USER = 'user',
 }
 
 export type NavInfo = {

+ 1 - 0
server/package.json

@@ -81,6 +81,7 @@
     "start": "nodemon src/app.ts",
     "start:plugin": "yarn build && cross-env PLUGIN_DEV=1 node dist/attu/express/src/app.js",
     "start:prod": "node dist/src/app.js",
+    "start:debug": "DEBUG=express:* nodemon src/app.ts",
     "test": "cross-env NODE_ENV=test jest --passWithNoTests",
     "test:watch": "jest --watch",
     "test:cov": "cross-env NODE_ENV=test jest --passWithNoTests --coverage",

+ 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)

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

@@ -50,6 +50,7 @@ export class MilvusService {
         HTTP_STATUS_CODE.FORBIDDEN,
         'Can not find your connection, please connect Milvus again'
       );
+
       // throw new Error('Please connect milvus first');
     }
   }
@@ -80,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);
       }

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

@@ -18,8 +18,3 @@ export class UpdateUserDto {
   @IsString()
   readonly newPassword: string;
 }
-
-export class DeleteUserDto {
-  @IsString()
-  readonly username: string;
-}

+ 7 - 11
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, DeleteUserDto } from './dto';
+import { CreateUserDto, UpdateUserDto } from './dto';
 
 export class UserController {
   private router: Router;
@@ -29,11 +29,7 @@ export class UserController {
       this.updateUsers.bind(this)
     );
 
-    this.router.delete(
-      '/',
-      dtoValidationMiddleware(DeleteUserDto),
-      this.deleteUsers.bind(this)
-    );
+    this.router.delete('/:username', this.deleteUser.bind(this));
 
     return this.router;
   }
@@ -50,7 +46,7 @@ export class UserController {
   async createUsers(req: Request, res: Response, next: NextFunction) {
     const { username, password } = req.body;
     try {
-      const result = this.userService.createUser({ username, password });
+      const result = await this.userService.createUser({ username, password });
       res.send(result);
     } catch (error) {
       next(error);
@@ -60,7 +56,7 @@ export class UserController {
   async updateUsers(req: Request, res: Response, next: NextFunction) {
     const { username, oldPassword, newPassword } = req.body;
     try {
-      const result = this.userService.updateUser({
+      const result = await this.userService.updateUser({
         username,
         oldPassword,
         newPassword,
@@ -71,10 +67,10 @@ export class UserController {
     }
   }
 
-  async deleteUsers(req: Request, res: Response, next: NextFunction) {
-    const { username } = req.body;
+  async deleteUser(req: Request, res: Response, next: NextFunction) {
+    const { username } = req.params;
     try {
-      const result = this.userService.deleteUser({ username });
+      const result = await this.userService.deleteUser({ username });
       res.send(result);
     } catch (error) {
       next(error);

+ 16 - 8
server/src/users/users.service.ts

@@ -4,6 +4,7 @@ import {
   UpdateUserReq,
   DeleteUserReq,
 } from '@zilliz/milvus2-sdk-node/dist/milvus/types/User';
+import { throwErrorFromSDK } from '../utils/Error';
 
 export class UserService {
   constructor(private milvusService: MilvusService) {}
@@ -13,22 +14,29 @@ export class UserService {
   }
 
   async getUsers() {
-    const result = await this.userManager.listUsers();
-    return result;
+    const res = await this.userManager.listUsers();
+    throwErrorFromSDK(res.status);
+
+    return res;
   }
 
   async createUser(data: CreateUserReq) {
-    const result = await this.userManager.createUser(data);
-    return result;
+    const res = await this.userManager.createUser(data);
+    throwErrorFromSDK(res);
+
+    return res;
   }
 
   async updateUser(data: UpdateUserReq) {
-    const result = await this.userManager.updateUser(data);
-    return result;
+    const res = await this.userManager.updateUser(data);
+    throwErrorFromSDK(res);
+
+    return res;
   }
 
   async deleteUser(data: DeleteUserReq) {
-    const result = await this.userManager.deleteUser(data);
-    return result;
+    const res = await this.userManager.deleteUser(data);
+    throwErrorFromSDK(res);
+    return res;
   }
 }