Sfoglia il codice sorgente

Merge pull request #604 from zilliztech/attu-473

feature: Users can select an address from their connection history on the connect page.
ryjiang 9 mesi fa
parent
commit
53fec2afce

+ 4 - 1
client/src/components/customInput/CustomInput.tsx

@@ -48,11 +48,12 @@ const handleOnChange = (param: IChangeParam) => {
     event,
     key,
     param: { cb, checkValid, validations },
+    type,
   } = param;
   let input = event.target.value;
 
   // fix for number input
-  if (!isNaN(input) && input.trim() !== '') {
+  if (!isNaN(input) && input.trim() !== '' && type === 'number') {
     input = parseFloat(input);
   }
 
@@ -121,6 +122,7 @@ const getAdornmentInput = (
               ...param,
               cb: onInputChange || (() => {}),
             },
+            type: config.type || 'text',
           });
         }}
         endAdornment={
@@ -246,6 +248,7 @@ const getTextfield = (
           event,
           key,
           param: { ...param, cb: onChange || (() => {}) },
+          type: others.type || 'text',
         });
       }}
     />

+ 5 - 2
client/src/components/customInput/Types.ts

@@ -32,7 +32,9 @@ export interface IBlurParam {
   };
 }
 
-export interface IChangeParam extends IBlurParam {}
+export interface IChangeParam extends IBlurParam {
+  type: 'text' | 'number' | 'password';
+}
 
 export interface ICustomInputProps {
   type?: InputType;
@@ -75,7 +77,7 @@ export interface ITextfieldConfig {
   validations?: IValidation[];
   fullWidth?: boolean;
   className?: string;
-  type?: string;
+  type?: 'text' | 'number' | 'password';
   onBlur?: (event: any) => void;
   onChange?: (event: any) => void;
   onKeyDown?: (event: any) => void;
@@ -94,6 +96,7 @@ export interface IAdornmentConfig {
   onInputBlur?: (event: any) => void;
   onInputChange?: (event: any) => void;
   onKeyDown?: (event: any) => void;
+  type?: 'text' | 'number' | 'password';
 }
 
 export type SearchType = {

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

@@ -326,6 +326,7 @@ const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
       viewBox="0 0 400 400"
       fill="none"
       xmlns="http://www.w3.org/2000/svg"
+      {...props}
     >
       <path
         fillRule="evenodd"
@@ -834,6 +835,7 @@ const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
       viewBox="0 0 15 15"
       fill="none"
       xmlns="http://www.w3.org/2000/svg"
+      {...props}
     >
       <path
         d="M4.85355 2.14645C5.04882 2.34171 5.04882 2.65829 4.85355 2.85355L3.70711 4H9C11.4853 4 13.5 6.01472 13.5 8.5C13.5 10.9853 11.4853 13 9 13H5C4.72386 13 4.5 12.7761 4.5 12.5C4.5 12.2239 4.72386 12 5 12H9C10.933 12 12.5 10.433 12.5 8.5C12.5 6.567 10.933 5 9 5H3.70711L4.85355 6.14645C5.04882 6.34171 5.04882 6.65829 4.85355 6.85355C4.65829 7.04882 4.34171 7.04882 4.14645 6.85355L2.14645 4.85355C1.95118 4.65829 1.95118 4.34171 2.14645 4.14645L4.14645 2.14645C4.34171 1.95118 4.65829 1.95118 4.85355 2.14645Z"
@@ -843,6 +845,40 @@ const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
       ></path>
     </svg>
   ),
+  link: (props = {}) => (
+    <svg
+      width="15"
+      height="15"
+      viewBox="0 0 15 15"
+      fill="none"
+      xmlns="http://www.w3.org/2000/svg"
+      {...props}
+    >
+      <path
+        d="M4.62471 4.00001L4.56402 4.00001C4.04134 3.99993 3.70687 3.99988 3.4182 4.055C2.2379 4.28039 1.29846 5.17053 1.05815 6.33035C0.999538 6.61321 0.999604 6.93998 0.999703 7.43689L0.999711 7.50001L0.999703 7.56313C0.999604 8.06004 0.999538 8.38681 1.05815 8.66967C1.29846 9.8295 2.2379 10.7196 3.4182 10.945C3.70688 11.0001 4.04135 11.0001 4.56403 11L4.62471 11H5.49971C5.77585 11 5.99971 10.7762 5.99971 10.5C5.99971 10.2239 5.77585 10 5.49971 10H4.62471C4.02084 10 3.78907 9.99777 3.60577 9.96277C2.80262 9.8094 2.19157 9.21108 2.03735 8.46678C2.00233 8.29778 1.99971 8.08251 1.99971 7.50001C1.99971 6.91752 2.00233 6.70225 2.03735 6.53324C2.19157 5.78895 2.80262 5.19062 3.60577 5.03725C3.78907 5.00225 4.02084 5.00001 4.62471 5.00001H5.49971C5.77585 5.00001 5.99971 4.77615 5.99971 4.50001C5.99971 4.22387 5.77585 4.00001 5.49971 4.00001H4.62471ZM10.3747 5.00001C10.9786 5.00001 11.2104 5.00225 11.3937 5.03725C12.1968 5.19062 12.8079 5.78895 12.9621 6.53324C12.9971 6.70225 12.9997 6.91752 12.9997 7.50001C12.9997 8.08251 12.9971 8.29778 12.9621 8.46678C12.8079 9.21108 12.1968 9.8094 11.3937 9.96277C11.2104 9.99777 10.9786 10 10.3747 10H9.49971C9.22357 10 8.99971 10.2239 8.99971 10.5C8.99971 10.7762 9.22357 11 9.49971 11H10.3747L10.4354 11C10.9581 11.0001 11.2925 11.0001 11.5812 10.945C12.7615 10.7196 13.701 9.8295 13.9413 8.66967C13.9999 8.38681 13.9998 8.06005 13.9997 7.56314L13.9997 7.50001L13.9997 7.43688C13.9998 6.93998 13.9999 6.61321 13.9413 6.33035C13.701 5.17053 12.7615 4.28039 11.5812 4.055C11.2925 3.99988 10.9581 3.99993 10.4354 4.00001L10.3747 4.00001H9.49971C9.22357 4.00001 8.99971 4.22387 8.99971 4.50001C8.99971 4.77615 9.22357 5.00001 9.49971 5.00001H10.3747ZM5.00038 7C4.72424 7 4.50038 7.22386 4.50038 7.5C4.50038 7.77614 4.72424 8 5.00038 8H10.0004C10.2765 8 10.5004 7.77614 10.5004 7.5C10.5004 7.22386 10.2765 7 10.0004 7H5.00038Z"
+        fill="currentColor"
+        fillRule="evenodd"
+        clipRule="evenodd"
+      ></path>
+    </svg>
+  ),
+  cross: (props = {}) => (
+    <svg
+      width="15"
+      height="15"
+      viewBox="0 0 15 15"
+      fill="none"
+      xmlns="http://www.w3.org/2000/svg"
+      {...props}
+    >
+      <path
+        d="M12.8536 2.85355C13.0488 2.65829 13.0488 2.34171 12.8536 2.14645C12.6583 1.95118 12.3417 1.95118 12.1464 2.14645L7.5 6.79289L2.85355 2.14645C2.65829 1.95118 2.34171 1.95118 2.14645 2.14645C1.95118 2.34171 1.95118 2.65829 2.14645 2.85355L6.79289 7.5L2.14645 12.1464C1.95118 12.3417 1.95118 12.6583 2.14645 12.8536C2.34171 13.0488 2.65829 13.0488 2.85355 12.8536L7.5 8.20711L12.1464 12.8536C12.3417 13.0488 12.6583 13.0488 12.8536 12.8536C13.0488 12.6583 13.0488 12.3417 12.8536 12.1464L8.20711 7.5L12.8536 2.85355Z"
+        fill="currentColor"
+        fill-rule="evenodd"
+        clip-rule="evenodd"
+      ></path>
+    </svg>
+  ),
 };
 
 export default icons;

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

@@ -53,4 +53,6 @@ export type IconsType =
   | 'star'
   | 'magic'
   | 'code'
-  | 'reset';
+  | 'reset'
+  | 'link'
+  | 'cross';

+ 4 - 0
client/src/consts/Localstorage.ts

@@ -10,3 +10,7 @@ export const LAST_TIME_HEALTHY_THRESHOLD_CPU =
   'last-time-healthy-threshold-cpu';
 export const LAST_TIME_HEALTHY_THRESHOLD_MEMORY =
   'last-time-healthy-threshold-memory';
+
+// new local storage keys
+export const ATTU_UI_TREE_WIDTH = 'attu.ui.tree.with';
+export const ATTU_AUTH_HISTORY = 'attu.auth.history';

+ 1 - 1
client/src/consts/Milvus.ts

@@ -2,7 +2,7 @@ export const MILVUS_URL =
   ((window as any)._env_ && (window as any)._env_.MILVUS_URL) || '';
 
 export const MILVUS_DATABASE =
-  ((window as any)._env_ && (window as any)._env_.DATABASE) || '';
+  ((window as any)._env_ && (window as any)._env_.DATABASE) || 'default';
 
 export const DYNAMIC_FIELD = `$meta`;
 

+ 4 - 11
client/src/context/Auth.tsx

@@ -35,25 +35,18 @@ export const AuthProvider = (props: { children: React.ReactNode }) => {
       JSON.stringify({
         username: '',
         password: '',
-        address: '' || MILVUS_URL,
+        address: MILVUS_URL,
         token: '',
-        database: '' || MILVUS_DATABASE,
+        database: MILVUS_DATABASE,
       })
   );
+
   // state
   const [authReq, setAuthReq] = useState<AuthReq>(localAuthReq);
   const [clientId, setClientId] = useState<string>(
     window.localStorage.getItem(MILVUS_CLIENT_ID) || ''
   );
 
-  // update title when address changes
-  useEffect(() => {
-    document.title = authReq.address ? `${authReq.address} - Attu` : 'Attu';
-    return () => {
-      document.title = 'Attu';
-    };
-  }, [authReq.address]);
-
   // update local storage when authReq changes
   useEffect(() => {
     // store auth request in local storage
@@ -68,7 +61,7 @@ export const AuthProvider = (props: { children: React.ReactNode }) => {
     // connect to Milvus
     const res = await MilvusService.connect(params);
     // update auth request
-    setAuthReq({ ...params, database: res.database, password: '', token: '' });
+    setAuthReq({ ...params, database: res.database });
     setClientId(res.clientId);
 
     return res;

+ 3 - 3
client/src/context/Data.tsx

@@ -26,7 +26,7 @@ import {
   DatabaseObject,
 } from '@server/types';
 import { WS_EVENTS, WS_EVENTS_TYPE, LOADING_STATE } from '@server/utils/Const';
-import { DEFAULT_TREE_WIDTH } from '@/consts';
+import { DEFAULT_TREE_WIDTH, ATTU_UI_TREE_WIDTH } from '@/consts';
 import { checkIndexing, checkLoading } from '@server/utils/Shared';
 
 export const dataContext = createContext<DataContextType>({
@@ -367,12 +367,12 @@ export const DataProvider = (props: { children: React.ReactNode }) => {
   // set UI preferences
   const setUIPref = (pref: DataContextType['ui']) => {
     setUI(pref);
-    localStorage.setItem('attu.ui.tree.width', String(pref.tree.width));
+    localStorage.setItem(ATTU_UI_TREE_WIDTH, String(pref.tree.width));
   };
 
   // load UI preferences
   useEffect(() => {
-    const storedWidth = Number(localStorage.getItem('attu.ui.tree.width'));
+    const storedWidth = Number(localStorage.getItem(ATTU_UI_TREE_WIDTH));
     if (storedWidth) {
       setUI(prevUI => ({
         ...prevUI,

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

@@ -8,6 +8,7 @@ const btnTrans = {
   searchMulti: `搜索{{number}}`,
   confirm: '确认',
   connect: '连接',
+  connecting: '连接中...',
   import: '导入',
   delete: '删除',
   drop: 'drop',

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

@@ -8,6 +8,7 @@ const btnTrans = {
   searchMulti: `Search{{number}}`,
   confirm: 'Confirm',
   connect: 'Connect',
+  connecting: 'Connecting...',
   import: 'Import',
   delete: 'Delete',
   drop: 'Drop',

+ 230 - 140
client/src/pages/connect/AuthForm.tsx

@@ -1,5 +1,5 @@
-import React, { useContext, useMemo, useState } from 'react';
-import { makeStyles, Theme, Typography } from '@material-ui/core';
+import React, { useContext, useEffect, useMemo, useState } from 'react';
+import { Typography, Menu } from '@material-ui/core';
 import { useTranslation } from 'react-i18next';
 import CustomButton from '@/components/customButton/CustomButton';
 import CustomInput from '@/components/customInput/CustomInput';
@@ -7,64 +7,33 @@ import { useFormValidation } from '@/hooks';
 import { formatForm } from '@/utils';
 import { useNavigate } from 'react-router-dom';
 import { rootContext, authContext, dataContext } from '@/context';
-import { MILVUS_CLIENT_ID } from '@/consts';
+import {
+  MILVUS_CLIENT_ID,
+  ATTU_AUTH_HISTORY,
+  MILVUS_DATABASE,
+  MILVUS_URL,
+} from '@/consts';
 import { CustomRadio } from '@/components/customRadio/CustomRadio';
 import Icons from '@/components/icons/Icons';
 import CustomToolTip from '@/components/customToolTip/CustomToolTip';
+import CustomIconButton from '@/components/customButton/CustomIconButton';
+import { useStyles } from './style';
+import { AuthReq } from '@server/types';
 
-const useStyles = makeStyles((theme: Theme) => ({
-  wrapper: {
-    display: 'flex',
-    flexDirection: 'column',
-    alignItems: 'flex-end',
-    padding: theme.spacing(0, 3),
-    position: 'relative',
-  },
-  titleWrapper: {
-    textAlign: 'left',
-    alignSelf: 'flex-start',
-    padding: theme.spacing(3, 0),
-    '& svg': {
-      fontSize: 15,
-      marginLeft: theme.spacing(0.5),
-    },
-  },
-  input: {
-    margin: theme.spacing(0.5, 0, 0),
-  },
-  toggle: {
-    display: 'flex',
-    width: '100%',
-    justifyContent: 'flex-start',
-  },
-  star: {
-    position: 'absolute',
-    top: -48,
-    right: -8,
-    marginTop: theme.spacing(1),
-    alignItems: 'center',
-    height: '32px',
-    lineHeight: '32px',
-    color: '#333',
-    background: '#f1f1f1',
-    padding: theme.spacing(0.5, 0, 0.5, 1),
-    fontSize: 13,
-    display: 'block',
-    width: '132px',
-    textDecoration: 'none',
-    marginRight: theme.spacing(1),
-    fontWeight: 500,
-    '&:hover': {
-      fontWeight: 'bold',
-    },
-  },
-  icon: {
-    verticalAlign: '-5px',
-    marginRight: theme.spacing(1),
-  },
-}));
-
-export const AuthForm = (props: any) => {
+type Connection = AuthReq & {
+  time: number;
+};
+
+const DEFAULT_CONNECTION = {
+  address: MILVUS_URL || '127.0.0.1:19530',
+  database: MILVUS_DATABASE,
+  token: '',
+  username: '',
+  password: '',
+  time: -1,
+};
+
+export const AuthForm = () => {
   // styles
   const classes = useStyles();
 
@@ -84,75 +53,52 @@ export const AuthForm = (props: any) => {
   const navigate = useNavigate();
 
   // UI states
-  const [withPass, setWithPass] = useState(authReq.username.length > 0);
+  const [withPass, setWithPass] = useState(false);
+  const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
+  const [connections, setConnections] = useState<Connection[]>([]);
+  const [isConnecting, setIsConnecting] = useState(false);
 
   // form validation
   const checkedForm = useMemo(() => {
     return formatForm(authReq);
   }, [authReq]);
-  const { validation, checkIsValid } = useFormValidation(checkedForm);
+  const { validation, checkIsValid, resetValidation } =
+    useFormValidation(checkedForm);
 
+  // UI handlers
   // handle input change
   const handleInputChange = (
     key: 'address' | 'username' | 'password' | 'database' | 'token',
     value: string | boolean
   ) => {
+    // set database to default if empty
+    if (key === 'database' && value === '') {
+      value = MILVUS_DATABASE;
+    }
     setAuthReq(v => ({ ...v, [key]: value }));
   };
+  // handle menu clicked
+  const handleMenuClick = (event: React.MouseEvent<HTMLButtonElement>) => {
+    setAnchorEl(event.currentTarget);
+  };
 
-  // const {
-  //   withPrometheus,
-  //   setWithPrometheus,
-  //   prometheusAddress,
-  //   prometheusInstance,
-  //   prometheusNamespace,
-  //   setPrometheusAddress,
-  //   setPrometheusInstance,
-  //   setPrometheusNamespace,
-  // } = useContext(prometheusContext);
-
-  // const prometheusConfigs: ITextfieldConfig[] = useMemo(
-  //   () => [
-  //     {
-  //       label: `${attuTrans.prometheusAddress}`,
-  //       key: 'prometheus_address',
-  //       onChange: setPrometheusAddress,
-  //       variant: 'filled',
-  //       className: classes.input,
-  //       placeholder: attuTrans.prometheusAddress,
-  //       fullWidth: true,
-
-  //       defaultValue: prometheusAddress,
-  //     },
-  //     {
-  //       label: `${attuTrans.prometheusNamespace}`,
-  //       key: 'prometheus_namespace',
-  //       onChange: setPrometheusNamespace,
-  //       variant: 'filled',
-  //       className: classes.input,
-  //       placeholder: attuTrans.prometheusNamespace,
-  //       fullWidth: true,
-
-  //       defaultValue: prometheusNamespace,
-  //     },
-  //     {
-  //       label: `${attuTrans.prometheusInstance}`,
-  //       key: 'prometheus_instance',
-  //       onChange: setPrometheusInstance,
-  //       variant: 'filled',
-  //       className: classes.input,
-  //       placeholder: attuTrans.prometheusInstance,
-  //       fullWidth: true,
-
-  //       defaultValue: prometheusInstance,
-  //     },
-  //   ],
-  //   []
-  // );
+  // handle menu close
+  const handleMenuClose = () => {
+    setAnchorEl(null);
+  };
 
+  // handle auth toggle
+  const handleEnableAuth = (val: boolean) => {
+    setWithPass(val);
+  };
+
+  // handle connect
   const handleConnect = async (event: React.FormEvent) => {
     event.preventDefault();
 
+    // set connecting
+    setIsConnecting(true);
+
     try {
       // login
       const result = await login(authReq);
@@ -163,6 +109,41 @@ export const AuthForm = (props: any) => {
       openSnackBar(successTrans('connect'));
       // save clientId to local storage
       window.localStorage.setItem(MILVUS_CLIENT_ID, result.clientId);
+      // get connection history
+      const history = JSON.parse(
+        window.localStorage.getItem(ATTU_AUTH_HISTORY) || '[]'
+      );
+
+      // add new connection to history, filter out the same connection
+      const newHistory = [
+        ...history.filter(
+          (item: any) =>
+            item.address !== authReq.address ||
+            item.database !== authReq.database
+        ),
+        {
+          address: authReq.address,
+          database: authReq.database,
+          username: authReq.username,
+          password: authReq.password,
+          token: authReq.token,
+          time: Date.now(),
+        },
+      ];
+
+      // if the count of history connections are more than 16, remove the first one, but it should keep the default one
+      if (newHistory.length > 16) {
+        newHistory.shift();
+      }
+
+      // save to local storage
+      window.localStorage.setItem(
+        ATTU_AUTH_HISTORY,
+        JSON.stringify(newHistory)
+      );
+
+      // set title
+      document.title = authReq.address ? `${authReq.address} - Attu` : 'Attu';
 
       // redirect to homepage
       navigate('/');
@@ -171,18 +152,81 @@ export const AuthForm = (props: any) => {
       if (error.response.data.message.includes('UNAUTHENTICATED')) {
         handleEnableAuth(true);
       }
+    } finally {
+      setIsConnecting(false);
     }
   };
 
-  const btnDisabled = useMemo(() => {
-    return authReq.address.trim().length === 0;
-  }, [authReq.address]);
+  // connect history clicked
+  const handleClickOnHisotry = (connection: Connection) => {
+    // set auth request
+    setAuthReq(connection);
+    // close menu
+    handleMenuClose();
+  };
+
+  const handleDeleteConnection = (connection: Connection) => {
+    const history = JSON.parse(
+      window.localStorage.getItem(ATTU_AUTH_HISTORY) || '[]'
+    ) as Connection[];
 
-  // handle auth toggle
-  const handleEnableAuth = (val: boolean) => {
-    setWithPass(val);
+    const newHistory = history.filter(
+      item =>
+        item.address !== connection.address ||
+        item.database !== connection.database
+    );
+
+    if (newHistory.length === 0) {
+      newHistory.push(DEFAULT_CONNECTION);
+    }
+
+    // save to local storage
+    window.localStorage.setItem(ATTU_AUTH_HISTORY, JSON.stringify(newHistory));
+    // sort by time
+    newHistory.sort((a, b) => {
+      return new Date(b.time).getTime() - new Date(a.time).getTime();
+    });
+    setConnections(newHistory);
   };
 
+  // is button should be disabled
+  const btnDisabled = authReq.address.trim().length === 0 || isConnecting;
+
+  // load connection from local storage
+  useEffect(() => {
+    const connections: Connection[] = JSON.parse(
+      window.localStorage.getItem(ATTU_AUTH_HISTORY) || '[]'
+    );
+
+    if (connections.length === 0) {
+      connections.push(DEFAULT_CONNECTION);
+    }
+
+    // sort by time
+    connections.sort((a, b) => {
+      return new Date(b.time).getTime() - new Date(a.time).getTime();
+    });
+
+    setConnections(connections);
+  }, []);
+
+  // UI effect
+  useEffect(() => {
+    // if address contains zilliz, or username or password is not empty
+    //  set withpass to true
+    const withPass =
+      (authReq.address.length > 0 && authReq.address.includes('zilliz')) ||
+      authReq.username.length > 0 ||
+      authReq.password.length > 0;
+
+    // set with pass
+    setWithPass(withPass);
+    // reset form
+    resetValidation(formatForm(authReq));
+    // update title
+    document.title = 'Attu';
+  }, [authReq.address, authReq.username, authReq.password]);
+
   return (
     <form onSubmit={handleConnect}>
       <section className={classes.wrapper}>
@@ -194,17 +238,29 @@ export const AuthForm = (props: any) => {
             </CustomToolTip>
           </Typography>
         </div>
+
         {/* address  */}
         <CustomInput
           type="text"
           textConfig={{
             label: attuTrans.address,
             key: 'address',
-            onChange: (val: string) => handleInputChange('address', val),
+            onChange: (val: string) =>
+              handleInputChange('address', String(val)),
             variant: 'filled',
             className: classes.input,
             placeholder: attuTrans.address,
             fullWidth: true,
+            InputProps: {
+              endAdornment: (
+                <CustomIconButton
+                  className={classes.menuBtn}
+                  onClick={handleMenuClick}
+                >
+                  <Icons.link />
+                </CustomIconButton>
+              ),
+            },
             validations: [
               {
                 rule: 'require',
@@ -213,12 +269,13 @@ export const AuthForm = (props: any) => {
                 }),
               },
             ],
-            defaultValue: authReq.address,
+            value: authReq.address,
           }}
           checkValid={checkIsValid}
           validInfo={validation}
           key={attuTrans.address}
         />
+
         {/* db  */}
         <CustomInput
           type="text"
@@ -230,7 +287,7 @@ export const AuthForm = (props: any) => {
             className: classes.input,
             placeholder: dbTrans('database'),
             fullWidth: true,
-            defaultValue: authReq.database,
+            value: authReq.database,
           }}
           checkValid={checkIsValid}
           validInfo={validation}
@@ -259,13 +316,12 @@ export const AuthForm = (props: any) => {
                 className: classes.input,
                 placeholder: attuTrans.token,
                 fullWidth: true,
-                defaultValue: authReq.token,
+                value: authReq.token,
               }}
               checkValid={checkIsValid}
               validInfo={validation}
               key={attuTrans.token}
             />
-
             {/* user  */}
             <CustomInput
               type="text"
@@ -278,13 +334,12 @@ export const AuthForm = (props: any) => {
                 className: classes.input,
                 placeholder: attuTrans.username,
                 fullWidth: true,
-                defaultValue: authReq.username,
+                value: authReq.username,
               }}
               checkValid={checkIsValid}
               validInfo={validation}
               key={attuTrans.username}
             />
-
             {/* pass  */}
             <CustomInput
               type="text"
@@ -298,7 +353,7 @@ export const AuthForm = (props: any) => {
                 placeholder: attuTrans.password,
                 fullWidth: true,
                 type: 'password',
-                defaultValue: authReq.password,
+                value: authReq.password,
               }}
               checkValid={checkIsValid}
               validInfo={validation}
@@ -307,28 +362,63 @@ export const AuthForm = (props: any) => {
           </>
         )}
 
-        {/* <div className={classes.toggle}>
-          <CustomRadio
-            defaultChecked={withPrometheus}
-            label={attuTrans.prometheus}
-            handleChange={setWithPrometheus}
-          />
-        </div>
-        {withPrometheus &&
-          prometheusConfigs.map(v => (
-            <CustomInput
-              type="text"
-              textConfig={v}
-              checkValid={checkIsValid}
-              validInfo={validation}
-              key={v.label}
-            />
-          ))} */}
-
         <CustomButton type="submit" variant="contained" disabled={btnDisabled}>
-          {btnTrans('connect')}
+          {btnTrans(isConnecting ? 'connecting' : 'connect')}
         </CustomButton>
       </section>
+
+      <Menu
+        anchorEl={anchorEl}
+        keepMounted
+        className={classes.menu}
+        anchorOrigin={{
+          vertical: 'bottom',
+          horizontal: 'right',
+        }}
+        transformOrigin={{
+          vertical: 'top',
+          horizontal: 'right',
+        }}
+        open={Boolean(anchorEl)}
+        onClose={handleMenuClose}
+        getContentAnchorEl={null}
+      >
+        {connections.map((connection, index) => (
+          <li
+            key={index}
+            className={classes.connection}
+            onClick={() => {
+              handleClickOnHisotry(connection);
+            }}
+          >
+            <div className="address">
+              <Icons.link className="icon"></Icons.link>
+              <div className="text">
+                {connection.address}/{connection.database}
+              </div>
+            </div>
+            <div className="time">
+              {connection.time !== -1
+                ? new Date(connection.time).toLocaleString()
+                : '--'}
+            </div>
+
+            <div>
+              {connection.time !== -1 && (
+                <CustomIconButton
+                  className="deleteIconBtn"
+                  onClick={e => {
+                    e.stopPropagation();
+                    handleDeleteConnection(connection);
+                  }}
+                >
+                  <Icons.cross></Icons.cross>
+                </CustomIconButton>
+              )}
+            </div>
+          </li>
+        ))}
+      </Menu>
     </form>
   );
 };

+ 125 - 0
client/src/pages/connect/style.ts

@@ -0,0 +1,125 @@
+import { makeStyles, Theme } from '@material-ui/core';
+
+export const useStyles = makeStyles((theme: Theme) => ({
+  wrapper: {
+    display: 'flex',
+    flexDirection: 'column',
+    padding: theme.spacing(0, 3),
+    position: 'relative',
+  },
+  titleWrapper: {
+    textAlign: 'left',
+    alignSelf: 'flex-start',
+    padding: theme.spacing(3, 0),
+    '& svg': {
+      fontSize: 15,
+      marginLeft: theme.spacing(0.5),
+    },
+  },
+  input: {
+    margin: theme.spacing(0.5, 0, 0),
+    '& .MuiFilledInput-adornedEnd': {
+      paddingRight: 0,
+    },
+  },
+  toggle: {
+    display: 'flex',
+    width: '100%',
+    justifyContent: 'flex-start',
+  },
+  star: {
+    position: 'absolute',
+    top: -48,
+    right: -8,
+    marginTop: theme.spacing(1),
+    alignItems: 'center',
+    height: '32px',
+    lineHeight: '32px',
+    color: '#333',
+    background: '#f1f1f1',
+    padding: theme.spacing(0.5, 0, 0.5, 1),
+    fontSize: 13,
+    display: 'block',
+    width: '132px',
+    textDecoration: 'none',
+    marginRight: theme.spacing(1),
+    fontWeight: 500,
+    '&:hover': {
+      fontWeight: 'bold',
+    },
+  },
+  menuBtn: {
+    display: 'flex',
+
+    paddingLeft: 8,
+    paddingRight: 8,
+
+    fontSize: 14,
+    '& button': {
+      width: 36,
+      height: 36,
+    },
+  },
+  menu: {
+    '& ul': {
+      padding: '0',
+      maxHeight: '400px',
+      overflowY: 'auto',
+    },
+  },
+  icon: {
+    verticalAlign: '-5px',
+    marginRight: theme.spacing(1),
+  },
+  connection: {
+    display: 'flex',
+    justifyContent: 'space-between',
+    fontSize: '14px',
+    width: 380,
+    padding: `0 8px`,
+    '&:hover': {
+      backgroundColor: theme.palette.action.hover,
+    },
+    cursor: 'pointer',
+
+    '& .address': {
+      display: 'grid',
+      gridTemplateColumns: '24px 1fr',
+      gap: 4,
+      color: theme.palette.text.primary,
+      fontSize: '14px',
+      padding: '12px 0',
+      '& .text': {
+        overflow: 'hidden',
+        textOverflow: 'ellipsis',
+        width: 200,
+        wordWrap: 'break-word',
+      },
+    },
+
+    '& .icon': {
+      verticalAlign: '-3px',
+      marginRight: 8,
+      fontSize: '14px',
+    },
+
+    '& .time': {
+      color: theme.palette.text.secondary,
+      fontSize: 11,
+      lineHeight: 1.5,
+      padding: '12px 0',
+      width: 130,
+      fontStyle: 'italic',
+    },
+
+    '& .deleteIconBtn': {
+      padding: '8px 0',
+      '& svg': {
+        fontSize: '14px',
+      },
+      height: 16,
+      lineHeight: '16px',
+      margin: 0,
+    },
+  },
+}));