瀏覽代碼

Update connection user experience (#140)

* Update connection user experience

* remove comments

* clear state on logout

* fix comments
ryjiang 2 年之前
父節點
當前提交
59765e8735

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

@@ -6,6 +6,7 @@ import icons from '../icons/Icons';
 import { useNavigate } from 'react-router-dom';
 import { useNavigate } from 'react-router-dom';
 import { authContext } from '../../context/Auth';
 import { authContext } from '../../context/Auth';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
+import { MilvusHttp } from '../../http/Milvus';
 import { MILVUS_ADDRESS } from '../../consts/Localstorage';
 import { MILVUS_ADDRESS } from '../../consts/Localstorage';
 
 
 const useStyles = makeStyles((theme: Theme) =>
 const useStyles = makeStyles((theme: Theme) =>
@@ -70,10 +71,13 @@ const Header: FC<HeaderType> = props => {
     navigate(path);
     navigate(path);
   };
   };
 
 
-  const handleLogout = () => {
+  const handleLogout = async () => {
     setAddress('');
     setAddress('');
     setIsAuth(false);
     setIsAuth(false);
+    await MilvusHttp.closeConnection();
     window.localStorage.removeItem(MILVUS_ADDRESS);
     window.localStorage.removeItem(MILVUS_ADDRESS);
+    // make sure we clear state in all pages
+    navigate(0);
   };
   };
 
 
   return (
   return (

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

@@ -1,2 +1,4 @@
 export const SESSION = 'CLOUD_SESSION';
 export const SESSION = 'CLOUD_SESSION';
 export const MILVUS_ADDRESS = 'milvus-address';
 export const MILVUS_ADDRESS = 'milvus-address';
+export const LAST_TIME_ADDRESS = 'last-time-address';
+

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

@@ -216,3 +216,4 @@ export enum LOADING_STATE {
 
 
 export const DEFAULT_VECTORS = 100000;
 export const DEFAULT_VECTORS = 100000;
 export const DEFAULT_SEFMENT_FILE_SIZE = 1024;
 export const DEFAULT_SEFMENT_FILE_SIZE = 1024;
+export const DEFAULT_MILVUS_PORT = 19530;

+ 5 - 1
client/src/http/Milvus.ts

@@ -3,7 +3,7 @@ import BaseModel from './BaseModel';
 
 
 export class MilvusHttp extends BaseModel {
 export class MilvusHttp extends BaseModel {
   static CONNECT_URL = '/milvus/connect';
   static CONNECT_URL = '/milvus/connect';
-
+  static DISCONNECT_URL = '/milvus/disconnect';
   static CHECK_URL = '/milvus/check';
   static CHECK_URL = '/milvus/check';
   static FLUSH_URL = '/milvus/flush';
   static FLUSH_URL = '/milvus/flush';
   static METRICS_URL = '/milvus/metrics';
   static METRICS_URL = '/milvus/metrics';
@@ -25,6 +25,10 @@ export class MilvusHttp extends BaseModel {
     return super.create({ path: this.CONNECT_URL, data });
     return super.create({ path: this.CONNECT_URL, data });
   }
   }
 
 
+  static closeConnection() {
+    return super.create({ path: this.DISCONNECT_URL });
+  }
+
   static getVersion() {
   static getVersion() {
     return super.search({ path: this.VERSION_URL, params: {} });
     return super.search({ path: this.VERSION_URL, params: {} });
   }
   }

+ 33 - 32
client/src/pages/connect/AuthForm.tsx

@@ -13,7 +13,7 @@ import { formatAddress } from '../../utils/Format';
 import { useNavigate } from 'react-router-dom';
 import { useNavigate } from 'react-router-dom';
 import { rootContext } from '../../context/Root';
 import { rootContext } from '../../context/Root';
 import { authContext } from '../../context/Auth';
 import { authContext } from '../../context/Auth';
-import { MILVUS_ADDRESS } from '../../consts/Localstorage';
+import { MILVUS_ADDRESS, LAST_TIME_ADDRESS } from '../../consts/Localstorage';
 import { CODE_STATUS } from '../../consts/Http';
 import { CODE_STATUS } from '../../consts/Http';
 import { MILVUS_URL } from '../../consts/Milvus';
 import { MILVUS_URL } from '../../consts/Milvus';
 import { CustomRadio } from '../../components/customRadio/CustomRadio';
 import { CustomRadio } from '../../components/customRadio/CustomRadio';
@@ -69,7 +69,7 @@ export const AuthForm = (props: any) => {
 
 
   const [showAuthForm, setShowAuthForm] = useState(false);
   const [showAuthForm, setShowAuthForm] = useState(false);
   const [form, setForm] = useState({
   const [form, setForm] = useState({
-    address: MILVUS_URL,
+    address: window.localStorage.getItem(LAST_TIME_ADDRESS) || MILVUS_URL,
     username: '',
     username: '',
     password: '',
     password: '',
     ssl: false,
     ssl: false,
@@ -138,11 +138,14 @@ export const AuthForm = (props: any) => {
     try {
     try {
       const data = { ...form, address };
       const data = { ...form, address };
       await MilvusHttp.connect(data);
       await MilvusHttp.connect(data);
+
       setIsAuth(true);
       setIsAuth(true);
       setAddress(address);
       setAddress(address);
 
 
       openSnackBar(successTrans('connect'));
       openSnackBar(successTrans('connect'));
       window.localStorage.setItem(MILVUS_ADDRESS, address);
       window.localStorage.setItem(MILVUS_ADDRESS, address);
+      // store address for next time using
+      window.localStorage.setItem(LAST_TIME_ADDRESS, address);
       navigate('/');
       navigate('/');
     } catch (error: any) {
     } catch (error: any) {
       if (error?.response?.status === CODE_STATUS.UNAUTHORIZED) {
       if (error?.response?.status === CODE_STATUS.UNAUTHORIZED) {
@@ -158,35 +161,33 @@ export const AuthForm = (props: any) => {
   }, [form.address]);
   }, [form.address]);
 
 
   return (
   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>
+    <form onSubmit={handleConnect}>
+      <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 type="submit" variant="contained" disabled={btnDisabled}>
+          {btnTrans('connect')}
+        </CustomButton>
+      </section>
+    </form>
   );
   );
 };
 };

+ 6 - 2
client/src/utils/Format.ts

@@ -1,4 +1,5 @@
 import { BYTE_UNITS } from '../consts/Util';
 import { BYTE_UNITS } from '../consts/Util';
+import { DEFAULT_MILVUS_PORT } from '../consts/Milvus';
 import {
 import {
   CreateFieldType,
   CreateFieldType,
   DataTypeEnum,
   DataTypeEnum,
@@ -141,8 +142,11 @@ export const getCreateFieldType = (config: Field): CreateFieldType => {
 };
 };
 
 
 // Trim the address
 // Trim the address
-export const formatAddress = (address: string): string =>
-  address.trim().replace(/(http|https):\/\//, '');
+export const formatAddress = (address: string): string => {
+  // remove http or https prefix from address
+  const ip = address.replace(/(http|https):\/\//, '');
+  return ip.includes(':') ? ip : `${ip}:${DEFAULT_MILVUS_PORT}`;
+};
 
 
 export const formatByteSize = (
 export const formatByteSize = (
   size: number,
   size: number,

+ 46 - 81
server/src/app.ts

@@ -18,7 +18,7 @@ import {
   ReqHeaderMiddleware,
   ReqHeaderMiddleware,
 } from './middlewares';
 } from './middlewares';
 
 
-import { getDirectories, getDirectoriesSync, generateCfgs } from './utils';
+// import { getDirectories, getDirectoriesSync, generateCfgs } from './utils';
 import * as path from 'path';
 import * as path from 'path';
 import chalk from 'chalk';
 import chalk from 'chalk';
 import { surveSwaggerSpecification } from './swagger';
 import { surveSwaggerSpecification } from './swagger';
@@ -26,10 +26,6 @@ import swaggerUi from 'swagger-ui-express';
 import LruCache from 'lru-cache';
 import LruCache from 'lru-cache';
 import { EXPIRED_TIME, INSIGHT_CACHE } from './utils/Const';
 import { EXPIRED_TIME, INSIGHT_CACHE } from './utils/Const';
 
 
-const PLUGIN_DEV = process.env?.PLUGIN_DEV;
-const SRC_PLUGIN_DIR = 'src/plugins';
-const DEV_PLUGIN_DIR = '../../src/*/server';
-
 const insightCache = new LruCache({
 const insightCache = new LruCache({
   maxAge: EXPIRED_TIME,
   maxAge: EXPIRED_TIME,
   updateAgeOnGet: true,
   updateAgeOnGet: true,
@@ -39,13 +35,6 @@ export const app = express();
 const PORT = 3000;
 const PORT = 3000;
 // initialize a simple http server
 // initialize a simple http server
 const server = http.createServer(app);
 const server = http.createServer(app);
-// initialize the WebSocket server instance
-const io = new Server(server, {
-  cors: {
-    origin: '*',
-    methods: ['GET', 'POST'],
-  },
-});
 
 
 app.set(INSIGHT_CACHE, insightCache);
 app.set(INSIGHT_CACHE, insightCache);
 // https://expressjs.com/en/resources/middleware/cors.html
 // https://expressjs.com/en/resources/middleware/cors.html
@@ -68,81 +57,57 @@ app.use(ReqHeaderMiddleware);
 const router = express.Router();
 const router = express.Router();
 const pluginsRouter = express.Router();
 const pluginsRouter = express.Router();
 
 
-// Init WebSocket server event listener
-io.on('connection', (socket: Socket) => {
-  console.log('socket.io connected');
-  socket.on('COLLECTION', (message: any) => {
-    socket.emit('COLLECTION', { data: message });
-  });
-  pubSub.on('ws_pubsub', msg => {
-    const { event, data } = msg;
-    socket.emit(event, data);
-  });
-});
+router.use('/milvus', connectRouter);
+router.use('/collections', collectionsRouter);
+router.use('/partitions', partitionsRouter);
+router.use('/schema', schemaRouter);
+router.use('/crons', cronsRouter);
+router.use('/users', userRouter);
 
 
-// Read plugin files and start express server
-// Import all plguins under "src/plugins"
-getDirectories(SRC_PLUGIN_DIR, async (dirErr: Error, dirRes: string[]) => {
-  const cfgs: any[] = [];
-  if (dirErr) {
-    console.log('Reading plugin directory Error', dirErr);
-  } else {
-    generateCfgs(cfgs, dirRes);
-  }
-  // If under plugin dev mode, import all plugins under "../../src/*/server"
-  if (PLUGIN_DEV) {
-    getDirectoriesSync(
-      DEV_PLUGIN_DIR,
-      (devDirErr: Error, devDirRes: string[]) => {
-        if (devDirErr) {
-          console.log('Reading dev plugin directory Error', devDirErr);
-        } else {
-          generateCfgs(cfgs, devDirRes, false);
-        }
-      }
-    );
-  }
-  console.log('======/api/plugins configs======', cfgs);
-  cfgs.forEach(async (cfg: any) => {
-    const { api: pluginPath, componentPath } = cfg;
-    if (!pluginPath) return;
-    const {
-      default: { router: pluginRouter },
-    } = await import(componentPath);
-    pluginsRouter.use(`/${pluginPath}`, pluginRouter);
-  });
-
-  router.use('/milvus', connectRouter);
-  router.use('/collections', collectionsRouter);
-  router.use('/partitions', partitionsRouter);
-  router.use('/schema', schemaRouter);
-  router.use('/crons', cronsRouter);
-  router.use('/users', userRouter);
+router.get('/healthy', (req, res, next) => {
+  res.json({ status: 200 });
+  next();
+});
 
 
-  router.get('/healthy', (req, res, next) => {
-    res.json({ status: 200 });
-    next();
-  });
+app.use('/api/v1', router);
+app.use('/api/plugins', pluginsRouter);
 
 
-  app.use('/api/v1', router);
-  app.use('/api/plugins', pluginsRouter);
+// Return client build files
+app.use(express.static('build'));
 
 
-  // Return client build files
-  app.use(express.static('build'));
+const data = surveSwaggerSpecification();
+app.use('/api/v1/swagger', swaggerUi.serve, swaggerUi.setup(data));
 
 
-  const data = surveSwaggerSpecification();
-  app.use('/api/v1/swagger', swaggerUi.serve, swaggerUi.setup(data));
+// handle every other route with index.html, which will contain
+// a script tag to your application's JavaScript file(s).
+app.get('*', (request, response) => {
+  response.sendFile(path.join(__dirname, '../build/index.html'));
+});
 
 
-  // handle every other route with index.html, which will contain
-  // a script tag to your application's JavaScript file(s).
-  app.get('*', (request, response) => {
-    response.sendFile(path.join(__dirname, '../build/index.html'));
+// ErrorInterceptor
+app.use(ErrorMiddleware);
+
+// start server
+server.listen(PORT, () => {
+  // initialize the WebSocket server instance
+  const io = new Server(server, {
+    cors: {
+      origin: '*',
+      methods: ['GET', 'POST'],
+    },
   });
   });
-
-  // ErrorInterceptor
-  app.use(ErrorMiddleware);
-  // start server
-  server.listen(PORT, () => {
-    console.log(chalk.green.bold(`Attu Server started on port ${PORT} :)`));
+  // Init WebSocket server event listener
+  io.on('connection', (socket: Socket) => {
+    console.info('ws connected');
+    socket.on('COLLECTION', (message: any) => {
+      socket.emit('COLLECTION', { data: message });
+    });
+    pubSub.on('ws_pubsub', (msg: any) => {
+      socket.emit(msg.event, msg.data);
+    });
+    io.on('disconnect', () => {
+      console.info('ws disconnected');
+    });
   });
   });
+  console.info(chalk.green.bold(`Attu Server started on port ${PORT} :)`));
 });
 });

+ 3 - 1
server/src/middlewares/index.ts

@@ -14,7 +14,9 @@ export const ReqHeaderMiddleware = (
   const insightCache = req.app.get(INSIGHT_CACHE);
   const insightCache = req.app.get(INSIGHT_CACHE);
   // all ape requests need set milvus address in header.
   // all ape requests need set milvus address in header.
   // server will set activeaddress in milvus service.
   // server will set activeaddress in milvus service.
-  const milvusAddress = (req.headers[MILVUS_ADDRESS] as string) || '';
+  const milvusAddress = MilvusService.formatAddress(
+    (req.headers[MILVUS_ADDRESS] as string) || ''
+  );
   // console.log('------ Request headers -------', req.headers);
   // console.log('------ Request headers -------', req.headers);
   //  only api request has MILVUS_ADDRESS.
   //  only api request has MILVUS_ADDRESS.
   //  When client run in express, we dont need static files like: xx.js run this logic.
   //  When client run in express, we dont need static files like: xx.js run this logic.

+ 6 - 4
server/src/milvus/milvus.controller.ts

@@ -20,21 +20,18 @@ export class MilvusController {
 
 
   generateRoutes() {
   generateRoutes() {
     this.router.get('/version', this.getInfo.bind(this));
     this.router.get('/version', this.getInfo.bind(this));
-
     this.router.post(
     this.router.post(
       '/connect',
       '/connect',
       dtoValidationMiddleware(ConnectMilvusDto),
       dtoValidationMiddleware(ConnectMilvusDto),
       this.connectMilvus.bind(this)
       this.connectMilvus.bind(this)
     );
     );
-
+    this.router.post('/disconnect', this.closeConnection.bind(this));
     this.router.get('/check', this.checkConnect.bind(this));
     this.router.get('/check', this.checkConnect.bind(this));
-
     this.router.put(
     this.router.put(
       '/flush',
       '/flush',
       dtoValidationMiddleware(FlushDto),
       dtoValidationMiddleware(FlushDto),
       this.flush.bind(this)
       this.flush.bind(this)
     );
     );
-
     this.router.get('/metrics', this.getMetrics.bind(this));
     this.router.get('/metrics', this.getMetrics.bind(this));
 
 
     return this.router;
     return this.router;
@@ -97,4 +94,9 @@ export class MilvusController {
     };
     };
     res.send(data);
     res.send(data);
   }
   }
+
+  closeConnection(req: Request, res: Response, next: NextFunction) {
+    const result = this.milvusService.closeConnection();
+    res.send({ result });
+  }
 }
 }

+ 12 - 2
server/src/milvus/milvus.service.ts

@@ -6,6 +6,9 @@ import {
 import HttpErrors from 'http-errors';
 import HttpErrors from 'http-errors';
 import LruCache from 'lru-cache';
 import LruCache from 'lru-cache';
 import { HTTP_STATUS_CODE } from '../utils/Error';
 import { HTTP_STATUS_CODE } from '../utils/Error';
+import { DEFAULT_MILVUS_PORT } from '../utils/Const';
+import { connectivityState } from '@grpc/grpc-js';
+
 export class MilvusService {
 export class MilvusService {
   // Share with all instances, so activeAddress is static
   // Share with all instances, so activeAddress is static
   static activeAddress: string;
   static activeAddress: string;
@@ -41,7 +44,9 @@ export class MilvusService {
   }
   }
 
 
   static formatAddress(address: string) {
   static formatAddress(address: string) {
-    return address.replace(/(http|https):\/\//, '');
+    // remove http or https prefix from address
+    const ip = address.replace(/(http|https):\/\//, '');
+    return ip.includes(':') ? ip : `${ip}:${DEFAULT_MILVUS_PORT}`;
   }
   }
 
 
   checkMilvus() {
   checkMilvus() {
@@ -69,7 +74,7 @@ export class MilvusService {
     const milvusAddress = MilvusService.formatAddress(address);
     const milvusAddress = MilvusService.formatAddress(address);
     const hasAuth = username !== undefined && password !== undefined;
     const hasAuth = username !== undefined && password !== undefined;
     try {
     try {
-      const milvusClient = hasAuth
+      const milvusClient: MilvusClient = hasAuth
         ? new MilvusClient(milvusAddress, ssl, username, password)
         ? new MilvusClient(milvusAddress, ssl, username, password)
         : new MilvusClient(milvusAddress, ssl);
         : new MilvusClient(milvusAddress, ssl);
       await milvusClient.collectionManager.hasCollection({
       await milvusClient.collectionManager.hasCollection({
@@ -101,4 +106,9 @@ export class MilvusService {
     });
     });
     return res;
     return res;
   }
   }
+
+  closeConnection(): connectivityState {
+    const res = MilvusService.activeMilvusClient.closeConnection();
+    return res;
+  }
 }
 }

+ 20 - 10
server/src/milvus/swagger.yml

@@ -1,18 +1,18 @@
 paths:
 paths:
   /milvus/connect:
   /milvus/connect:
     post:
     post:
-      tags: 
+      tags:
         - Milvus
         - Milvus
-      description: Connect milvus
+      description: Connect to milvus server
       requestBody:
       requestBody:
-        description: Milvus address 
+        description: Milvus address
         required: true
         required: true
         content:
         content:
           application/json:
           application/json:
             schema:
             schema:
               type: object
               type: object
               required:
               required:
-                - "address"
+                - 'address'
               properties:
               properties:
                 address:
                 address:
                   type: string
                   type: string
@@ -22,9 +22,19 @@ paths:
           schema:
           schema:
             type: object
             type: object
 
 
+  /milvus/disconnect:
+    post:
+      tags:
+        - Milvus
+      description: Disconnect to milvus server
+      responses:
+        200:
+          schema:
+            type: object
+
   /milvus/check:
   /milvus/check:
     get:
     get:
-      tags: 
+      tags:
         - Milvus
         - Milvus
       description: Check milvus alive or not.
       description: Check milvus alive or not.
       parameters:
       parameters:
@@ -38,9 +48,9 @@ paths:
 
 
   /milvus/metrics:
   /milvus/metrics:
     get:
     get:
-      tags: 
+      tags:
         - Milvus
         - Milvus
-      description: Get milvus metrics     
+      description: Get milvus metrics
       responses:
       responses:
         200:
         200:
           schema:
           schema:
@@ -48,7 +58,7 @@ paths:
 
 
   /milvus/flush:
   /milvus/flush:
     post:
     post:
-      tags: 
+      tags:
         - Milvus
         - Milvus
       description: Flush data in milvus
       description: Flush data in milvus
       requestBody:
       requestBody:
@@ -59,7 +69,7 @@ paths:
             schema:
             schema:
               type: object
               type: object
               required:
               required:
-                - "collection_names"
+                - 'collection_names'
               properties:
               properties:
                 collection_names:
                 collection_names:
                   type: array
                   type: array
@@ -67,4 +77,4 @@ paths:
       responses:
       responses:
         200:
         200:
           schema:
           schema:
-            type: object
+            type: object

+ 2 - 0
server/src/utils/Const.ts

@@ -21,3 +21,5 @@ export enum WS_EVENTS_TYPE {
   START,
   START,
   STOP,
   STOP,
 }
 }
+
+export const DEFAULT_MILVUS_PORT = 19530;