Browse Source

support milvus auth login

Signed-off-by: nameczz <zizhao.chen@zilliz.com>
nameczz 3 năm trước cách đây
mục cha
commit
49ea6124e3

+ 23 - 0
client/src/components/customRadio/CustomRadio.tsx

@@ -0,0 +1,23 @@
+import * as React from 'react';
+import { FormGroup, FormControlLabel, Switch } from '@material-ui/core';
+
+export const CustomRadio = (props: {
+  label: string;
+  handleChange: (checked: boolean) => void;
+}) => {
+  const { label, handleChange } = props;
+  const onChange = (
+    e: React.ChangeEvent<HTMLInputElement>,
+    checked: boolean
+  ) => {
+    handleChange(checked);
+  };
+  return (
+    <FormGroup>
+      <FormControlLabel
+        control={<Switch onChange={onChange} color="primary" />}
+        label={label}
+      />
+    </FormGroup>
+  );
+};

+ 1 - 6
client/src/components/layout/GlobalEffect.tsx

@@ -2,15 +2,12 @@ import React, { useContext } from 'react';
 import axiosInstance from '../../http/Axios';
 import { rootContext } from '../../context/Root';
 import { CODE_STATUS } from '../../consts/Http';
-import { MILVUS_ADDRESS } from '../../consts/Localstorage';
-import { authContext } from '../../context/Auth';
 
 let axiosResInterceptor: number | null = null;
 // let timer: Record<string, ReturnType<typeof setTimeout> | number>[] = [];
 // we only take side effect here, nothing else
 const GlobalEffect = (props: { children: React.ReactNode }) => {
   const { openSnackBar } = useContext(rootContext);
-  const { setAddress } = useContext(authContext);
 
   // catch axios error here
   if (axiosResInterceptor === null) {
@@ -27,9 +24,7 @@ const GlobalEffect = (props: { children: React.ReactNode }) => {
         const { response = {} } = error;
         switch (response.status) {
           case CODE_STATUS.UNAUTHORIZED:
-            setAddress('');
-            window.localStorage.removeItem(MILVUS_ADDRESS);
-            break;
+            return Promise.reject(error);
           default:
             break;
         }

+ 2 - 1
client/src/components/layout/Header.tsx

@@ -59,7 +59,7 @@ const useStyles = makeStyles((theme: Theme) =>
 const Header: FC<HeaderType> = props => {
   const classes = useStyles();
   const { navInfo } = useContext(navContext);
-  const { address, setAddress } = useContext(authContext);
+  const { address, setAddress, setIsAuth } = useContext(authContext);
   const history = useHistory();
   const { t: commonTrans } = useTranslation();
   const statusTrans = commonTrans('status');
@@ -72,6 +72,7 @@ const Header: FC<HeaderType> = props => {
 
   const handleLogout = () => {
     setAddress('');
+    setIsAuth(false);
     window.localStorage.removeItem(MILVUS_ADDRESS);
   };
 

+ 3 - 0
client/src/consts/Milvus.tsx

@@ -1,5 +1,8 @@
 import { DataTypeEnum } from '../pages/collections/Types';
 
+export const MILVUS_URL =
+  ((window as any)._env_ && (window as any)._env_.MILVUS_URL) || '';
+
 export enum METRIC_TYPES_VALUES {
   L2 = 'L2',
   IP = 'IP',

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

@@ -1,4 +1,4 @@
-import { createContext, useEffect, useMemo, useState } from 'react';
+import { createContext, useEffect, useState } from 'react';
 import { MILVUS_ADDRESS } from '../consts/Localstorage';
 import { MilvusHttp } from '../http/Milvus';
 import { AuthContextType } from './Types';
@@ -6,7 +6,8 @@ import { AuthContextType } from './Types';
 export const authContext = createContext<AuthContextType>({
   isAuth: false,
   address: '',
-  setAddress: () => { },
+  setAddress: () => {},
+  setIsAuth: () => {},
 });
 
 const { Provider } = authContext;
@@ -15,7 +16,8 @@ export const AuthProvider = (props: { children: React.ReactNode }) => {
   const [address, setAddress] = useState<string>(
     window.localStorage.getItem(MILVUS_ADDRESS) || ''
   );
-  const isAuth = useMemo(() => !!address, [address]);
+  const [isAuth, setIsAuth] = useState<boolean>(false);
+  // const isAuth = useMemo(() => !!address, [address]);
 
   useEffect(() => {
     // check if the milvus is still available
@@ -43,7 +45,7 @@ export const AuthProvider = (props: { children: React.ReactNode }) => {
   }, [address]);
 
   return (
-    <Provider value={{ isAuth, address, setAddress }}>
+    <Provider value={{ isAuth, address, setAddress, setIsAuth }}>
       {props.children}
     </Provider>
   );

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

@@ -58,6 +58,7 @@ export type AuthContextType = {
   isAuth: boolean;
   address: string;
   setAddress: Dispatch<SetStateAction<string>>;
+  setIsAuth: Dispatch<SetStateAction<boolean>>;
 };
 
 export type NavContextType = {

+ 7 - 2
client/src/http/Milvus.ts

@@ -16,8 +16,13 @@ export class MilvusHttp extends BaseModel {
     Object.assign(this, props);
   }
 
-  static connect(address: string) {
-    return super.create({ path: this.CONNECT_URL, data: { address } });
+  static connect(data: {
+    address: string;
+    username?: string;
+    password?: string;
+    ssl?: boolean;
+  }) {
+    return super.create({ path: this.CONNECT_URL, data });
   }
 
   static getVersion() {

+ 4 - 0
client/src/i18n/cn/common.ts

@@ -2,6 +2,10 @@ const commonTrans = {
   attu: {
     admin: 'Attu',
     address: 'Milvus Address',
+    unAuth: 'Username or password is not correct',
+    username: 'Username',
+    password: 'Password',
+    ssl: 'SSL',
   },
   status: {
     loaded: 'loaded for search',

+ 4 - 0
client/src/i18n/en/common.ts

@@ -2,6 +2,10 @@ const commonTrans = {
   attu: {
     admin: 'Attu',
     address: 'Milvus Address',
+    unAuth: 'Username or password is not correct',
+    username: 'Username',
+    password: 'Password',
+    ssl: 'SSL',
   },
   status: {
     loaded: 'loaded for search',

+ 206 - 0
client/src/pages/connect/AuthForm.tsx

@@ -0,0 +1,206 @@
+import React, { useContext, useMemo, useState } from 'react';
+
+import { makeStyles, Theme, Typography } from '@material-ui/core';
+import CustomButton from '../../components/customButton/CustomButton';
+import CustomInput from '../../components/customInput/CustomInput';
+import icons from '../../components/icons/Icons';
+import { useTranslation } from 'react-i18next';
+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 { 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';
+
+const useStyles = makeStyles((theme: Theme) => ({
+  wrapper: {
+    display: 'flex',
+    flexDirection: 'column',
+    alignItems: 'flex-end',
+
+    padding: theme.spacing(0, 3),
+  },
+  titleWrapper: {
+    display: 'flex',
+    alignItems: 'center',
+    padding: theme.spacing(3),
+    margin: '0 auto',
+
+    '& .title': {
+      margin: 0,
+      color: '#323232',
+      fontWeight: 'bold',
+    },
+  },
+  logo: {
+    width: '42px',
+    height: 'auto',
+  },
+  input: {
+    margin: theme.spacing(3, 0, 0.5),
+  },
+  sslWrapper: {
+    display: 'flex',
+    width: '100%',
+    justifyContent: 'flex-start',
+  },
+}));
+export const AuthForm = (props: any) => {
+  const history = useHistory();
+  const classes = useStyles();
+
+  const { openSnackBar } = useContext(rootContext);
+  const { setAddress, setIsAuth } = useContext(authContext);
+
+  const Logo = icons.zilliz;
+  const { t: commonTrans } = useTranslation();
+  const attuTrans = commonTrans('attu');
+  const { t: btnTrans } = useTranslation('btn');
+  const { t: warningTrans } = useTranslation('warning');
+  const { t: successTrans } = useTranslation('success');
+
+  const [showAuthForm, setShowAuthForm] = useState(false);
+  const [form, setForm] = useState({
+    address: MILVUS_URL,
+    username: '',
+    password: '',
+    ssl: false,
+  });
+  const checkedForm = useMemo(() => {
+    return formatForm(form);
+  }, [form]);
+  const { validation, checkIsValid, disabled } = useFormValidation(checkedForm);
+
+  const handleInputChange = (
+    key: 'address' | 'username' | 'password' | 'ssl',
+    value: string | boolean
+  ) => {
+    setForm(v => ({ ...v, [key]: value }));
+  };
+
+  const inputConfigs: ITextfieldConfig[] = useMemo(() => {
+    const noAuthConfigs: ITextfieldConfig[] = [
+      {
+        label: attuTrans.address,
+        key: 'address',
+        onChange: (val: string) => handleInputChange('address', val),
+        variant: 'filled',
+        className: classes.input,
+        placeholder: attuTrans.address,
+        fullWidth: true,
+        validations: [
+          {
+            rule: 'require',
+            errorText: warningTrans('required', { name: attuTrans.address }),
+          },
+        ],
+        defaultValue: form.address,
+      },
+    ];
+    return showAuthForm
+      ? [
+          ...noAuthConfigs,
+          {
+            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,
+          },
+        ]
+      : noAuthConfigs;
+  }, [form, attuTrans, warningTrans, classes.input, showAuthForm]);
+
+  const handleConnect = async () => {
+    const address = formatAddress(form.address);
+    try {
+      const data = showAuthForm ? { ...form, address } : { address };
+      await MilvusHttp.connect(data);
+      setIsAuth(true);
+      setAddress(address);
+
+      openSnackBar(successTrans('connect'));
+      window.localStorage.setItem(MILVUS_ADDRESS, address);
+      history.push('/');
+    } catch (error: any) {
+      if (error?.response?.status === CODE_STATUS.UNAUTHORIZED) {
+        showAuthForm
+          ? openSnackBar(attuTrans.unAuth, 'error')
+          : setShowAuthForm(true);
+      }
+    }
+  };
+
+  const btnDisabled = useMemo(() => {
+    return showAuthForm ? disabled : form.address.trim().length === 0;
+  }, [showAuthForm, disabled, form.address]);
+
+  return (
+    <section className={classes.wrapper}>
+      <div className={classes.titleWrapper}>
+        <Logo classes={{ root: classes.logo }} />
+        <Typography variant="h2" className="title">
+          {attuTrans.admin}
+        </Typography>
+      </div>
+      {inputConfigs.map(v => (
+        <CustomInput
+          type="text"
+          textConfig={v}
+          checkValid={checkIsValid}
+          validInfo={validation}
+          key={v.label}
+        />
+      ))}
+      <div className={classes.sslWrapper}>
+        <CustomRadio
+          label={attuTrans.ssl}
+          handleChange={(val: boolean) => handleInputChange('ssl', val)}
+        />
+      </div>
+      <CustomButton
+        variant="contained"
+        disabled={btnDisabled}
+        onClick={handleConnect}
+      >
+        {btnTrans('connect')}
+      </CustomButton>
+    </section>
+  );
+};

+ 2 - 118
client/src/pages/connect/Connect.tsx

@@ -1,126 +1,10 @@
-import { Theme, makeStyles, Typography } from '@material-ui/core';
-import { useTranslation } from 'react-i18next';
-import { ITextfieldConfig } from '../../components/customInput/Types';
-import icons from '../../components/icons/Icons';
 import ConnectContainer from './ConnectContainer';
-import CustomInput from '../../components/customInput/CustomInput';
-import { useContext, useMemo, useState } from 'react';
-import { formatForm } from '../../utils/Form';
-import { useFormValidation } from '../../hooks/Form';
-import CustomButton from '../../components/customButton/CustomButton';
-import { useHistory } from 'react-router-dom';
-import { authContext } from '../../context/Auth';
-import { MilvusHttp } from '../../http/Milvus';
-import { rootContext } from '../../context/Root';
-import { MILVUS_ADDRESS } from '../../consts/Localstorage';
-import { formatAddress } from '../../utils/Format';
-
-const useStyles = makeStyles((theme: Theme) => ({
-  wrapper: {
-    display: 'flex',
-    flexDirection: 'column',
-    alignItems: 'flex-end',
-
-    padding: theme.spacing(0, 3),
-  },
-  titleWrapper: {
-    display: 'flex',
-    alignItems: 'center',
-    padding: theme.spacing(3),
-    margin: '0 auto',
-
-    '& .title': {
-      margin: 0,
-      color: '#323232',
-      fontWeight: 'bold',
-    },
-  },
-  logo: {
-    width: '42px',
-    height: 'auto',
-  },
-  input: {
-    margin: theme.spacing(3, 0, 0.5),
-  },
-}));
-const MILVUS_URL =
-  ((window as any)._env_ && (window as any)._env_.MILVUS_URL) || '';
+import { AuthForm } from './AuthForm';
 
 const Connect = () => {
-  const history = useHistory();
-  const { setAddress } = useContext(authContext);
-  const { openSnackBar } = useContext(rootContext);
-  const classes = useStyles();
-  const { t: commonTrans } = useTranslation();
-  const { t: warningTrans } = useTranslation('warning');
-  const attuTrans = commonTrans('attu');
-  const { t: btnTrans } = useTranslation('btn');
-  const { t: successTrans } = useTranslation('success');
-
-  const [form, setForm] = useState({
-    address: MILVUS_URL,
-  });
-  const checkedForm = useMemo(() => {
-    const { address } = form;
-    return formatForm({ address });
-  }, [form]);
-  const { validation, checkIsValid, disabled } = useFormValidation(checkedForm);
-
-  const Logo = icons.zilliz;
-
-  const handleInputChange = (value: string) => {
-    setForm({ address: value });
-  };
-
-  const handleConnect = async () => {
-    const address = formatAddress(form.address);
-    await MilvusHttp.connect(address);
-    openSnackBar(successTrans('connect'));
-    setAddress(form.address);
-    window.localStorage.setItem(MILVUS_ADDRESS, address);
-    history.push('/');
-  };
-
-  const addressInputConfig: ITextfieldConfig = {
-    label: attuTrans.address,
-    key: 'address',
-    onChange: handleInputChange,
-    variant: 'filled',
-    className: classes.input,
-    placeholder: attuTrans.address,
-    fullWidth: true,
-    validations: [
-      {
-        rule: 'require',
-        errorText: warningTrans('required', { name: attuTrans.address }),
-      },
-    ],
-    defaultValue: form.address,
-  };
-
   return (
     <ConnectContainer>
-      <section className={classes.wrapper}>
-        <div className={classes.titleWrapper}>
-          <Logo classes={{ root: classes.logo }} />
-          <Typography variant="h2" className="title">
-            {attuTrans.admin}
-          </Typography>
-        </div>
-        <CustomInput
-          type="text"
-          textConfig={addressInputConfig}
-          checkValid={checkIsValid}
-          validInfo={validation}
-        />
-        <CustomButton
-          variant="contained"
-          disabled={form.address ? false : disabled}
-          onClick={handleConnect}
-        >
-          {btnTrans('connect')}
-        </CustomButton>
-      </section>
+      <AuthForm />
     </ConnectContainer>
   );
 };

+ 6 - 3
server/generate-csv.ts

@@ -3,7 +3,10 @@ import { createObjectCsvWriter as createCsvWriter } from 'csv-writer';
 // use to test vector insert
 const csvWriter = createCsvWriter({
   path: './vectors.csv',
-  header: [{ id: 'vector', title: 'vector' }],
+  header: [
+    { id: 'vector', title: 'vector' },
+    { id: 'age', title: 'age' },
+  ],
 });
 
 const records = [];
@@ -19,8 +22,8 @@ const generateVector = (dimension: number) => {
 };
 
 while (records.length < 50000) {
-  const value = generateVector(8);
-  records.push({ vector: value });
+  const value = generateVector(4);
+  records.push({ vector: value, age: records.length });
 }
 
 csvWriter

+ 2 - 2
server/package.json

@@ -12,7 +12,7 @@
     "url": "https://github.com/zilliztech/attu"
   },
   "dependencies": {
-    "@zilliz/milvus2-sdk-node": "^2.0.2",
+    "@zilliz/milvus2-sdk-node": "^2.0.3",
     "chalk": "^4.1.2",
     "class-sanitizer": "^1.0.1",
     "class-transformer": "^0.4.0",
@@ -78,7 +78,7 @@
     "prebuild": "tslint -c tslint.json -p tsconfig.json --fix",
     "build": "yarn clean && tsc",
     "prestart": "rm -rf dist && yarn build",
-    "start": "nodemon dist/src/app.js",
+    "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",
     "test": "cross-env NODE_ENV=test jest --passWithNoTests",

+ 3 - 2
server/src/milvus/milvus.controller.ts

@@ -41,16 +41,17 @@ export class MilvusController {
   }
 
   async connectMilvus(req: Request, res: Response, next: NextFunction) {
-    const address = req.body?.address;
+    const { address, username, password, ssl } = req.body;
     const insightCache = req.app.get(INSIGHT_CACHE);
     try {
       const result = await this.milvusService.connectMilvus(
-        address,
+        { address, username, password, ssl },
         insightCache
       );
 
       res.send(result);
     } catch (error) {
+      console.log(error);
       next(error);
     }
   }

+ 26 - 14
server/src/milvus/milvus.service.ts

@@ -41,19 +41,31 @@ export class MilvusService {
 
   checkMilvus() {
     if (!MilvusService.activeMilvusClient) {
-      throw HttpErrors(
-        HTTP_STATUS_CODE.UNAUTHORIZED,
-        'Please connect milvus first'
-      );
+      throw HttpErrors(HTTP_STATUS_CODE.BAD_REQUEST, {
+        status: 401,
+        message: 'please connect milvus first',
+      });
       // throw new Error('Please connect milvus first');
     }
   }
 
-  async connectMilvus(address: string, cache: LruCache<any, any>) {
+  async connectMilvus(
+    data: {
+      address: string;
+      username?: string;
+      password?: string;
+      ssl?: boolean;
+    },
+    cache: LruCache<any, any>
+  ) {
+    const { address, username, password, ssl = false } = data;
     // grpc only need address without http
     const milvusAddress = MilvusService.formatAddress(address);
+    const hasAuth = username !== undefined && password !== undefined;
     try {
-      const milvusClient = new MilvusClient(milvusAddress);
+      const milvusClient = hasAuth
+        ? new MilvusClient(milvusAddress, ssl, username, password)
+        : new MilvusClient(milvusAddress, ssl);
       await milvusClient.collectionManager.hasCollection({
         collection_name: 'not_exist',
       });
@@ -63,10 +75,10 @@ export class MilvusService {
     } catch (error) {
       // if milvus is not working, delete connection.
       cache.del(milvusAddress);
-      throw HttpErrors(
-        HTTP_STATUS_CODE.BAD_REQUEST,
-        'Connect milvus failed, check your milvus address.'
-      );
+      if (error.toString().includes('unauthenticated')) {
+        throw HttpErrors(HTTP_STATUS_CODE.UNAUTHORIZED, error);
+      }
+      throw HttpErrors(HTTP_STATUS_CODE.BAD_REQUEST, error);
     }
   }
 
@@ -75,10 +87,10 @@ export class MilvusService {
     if (!cache.has(milvusAddress)) {
       return { connected: false };
     }
-    const res = await this.connectMilvus(address, cache);
-    return {
-      connected: res.address ? true : false,
-    };
+    // const res = await this.connectMilvus(address, cache);
+    // return {
+    //   connected: res.address ? true : false,
+    // };
   }
 
   async flush(data: FlushReq) {

+ 4 - 4
server/yarn.lock

@@ -1137,10 +1137,10 @@
   dependencies:
     "@types/yargs-parser" "*"
 
-"@zilliz/milvus2-sdk-node@^2.0.2":
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/@zilliz/milvus2-sdk-node/-/milvus2-sdk-node-2.0.2.tgz#d3e3272b20dfd11ed2ddc5a6c5b21fa6e0b08315"
-  integrity sha512-fuYzQKL1M87s4k/UXIYOSIRriC5Oe2uoP+fxI6p4+whSA80ftnztPiQNRgn/9KnDUFTF0ZVqaEe3P8UW7TSA1w==
+"@zilliz/milvus2-sdk-node@^2.0.3":
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/@zilliz/milvus2-sdk-node/-/milvus2-sdk-node-2.0.3.tgz#9007f6e5162ca9b51b522b1d6eff76a31dc6921c"
+  integrity sha512-BWTIRBhJI/z9HWCeJsfaCC6vueGcTVcL7Ai7SASnwFR4RIzfQ2bclo41G6767cHqZNiyOdoEyM+u0XuPpo8vjA==
   dependencies:
     "@grpc/grpc-js" "^1.2.12"
     "@grpc/proto-loader" "^0.6.0"

+ 4 - 0
yarn.lock

@@ -0,0 +1,4 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+