Browse Source

merge main

Signed-off-by: nameczz <zizhao.chen@zilliz.com>
nameczz 3 years ago
parent
commit
ed2081bd94
100 changed files with 3314 additions and 1421 deletions
  1. 14 0
      .github/workflows/dev.yml
  2. 2 0
      README.md
  3. 11 2
      client/README.md
  4. 4 0
      client/package.json
  5. 5 1
      client/src/App.tsx
  6. 28 39
      client/src/components/advancedSearch/Condition.tsx
  7. 2 2
      client/src/components/advancedSearch/Types.ts
  8. 77 0
      client/src/components/customDatePicker/CustomDatePicker.tsx
  9. 8 0
      client/src/components/customDatePicker/Types.ts
  10. 4 0
      client/src/components/icons/Icons.tsx
  11. 3 1
      client/src/components/icons/Types.ts
  12. 19 11
      client/src/components/layout/Layout.tsx
  13. 6 3
      client/src/components/menu/Types.ts
  14. 3 0
      client/src/consts/Milvus.tsx
  15. 31 0
      client/src/consts/Util.ts
  16. 14 3
      client/src/hooks/Navigation.ts
  17. 33 0
      client/src/hooks/TimeTravel.ts
  18. 13 2
      client/src/http/Collection.ts
  19. 2 2
      client/src/http/Field.ts
  20. 4 0
      client/src/i18n/cn/collection.ts
  21. 14 0
      client/src/i18n/cn/common.ts
  22. 2 0
      client/src/i18n/cn/search.ts
  23. 2 1
      client/src/i18n/cn/systemView.ts
  24. 5 2
      client/src/i18n/en/collection.ts
  25. 5 0
      client/src/i18n/en/common.ts
  26. 2 0
      client/src/i18n/en/search.ts
  27. 2 1
      client/src/i18n/en/systemView.ts
  28. 25 24
      client/src/pages/collections/Collections.tsx
  29. 5 0
      client/src/pages/collections/Constants.ts
  30. 17 10
      client/src/pages/collections/Types.ts
  31. 121 12
      client/src/pages/query/Query.tsx
  32. 10 1
      client/src/pages/query/Styles.ts
  33. 2 1
      client/src/pages/query/Types.ts
  34. 71 5
      client/src/pages/schema/Create.tsx
  35. 1 0
      client/src/pages/schema/IndexTypeElement.tsx
  36. 5 2
      client/src/pages/schema/Schema.tsx
  37. 72 0
      client/src/pages/schema/SizingInfo.tsx
  38. 10 3
      client/src/pages/schema/Types.ts
  39. 0 78
      client/src/pages/system/BaseCard.tsx
  40. 0 195
      client/src/pages/system/DataCard.tsx
  41. 0 125
      client/src/pages/system/LineChartCard.tsx
  42. 0 86
      client/src/pages/system/MiniTopology.tsx
  43. 0 151
      client/src/pages/system/NodeListView.tsx
  44. 0 30
      client/src/pages/system/Progress.tsx
  45. 0 28
      client/src/pages/system/ProgressCard.tsx
  46. 0 154
      client/src/pages/system/SystemView.tsx
  47. 0 178
      client/src/pages/system/Topology.tsx
  48. 0 71
      client/src/pages/system/Types.ts
  49. 0 0
      client/src/plugins/search/Constants.ts
  50. 0 0
      client/src/plugins/search/SearchParams.tsx
  51. 0 0
      client/src/plugins/search/Styles.ts
  52. 8 4
      client/src/plugins/search/Types.ts
  53. 19 1
      client/src/plugins/search/VectorSearch.tsx
  54. 13 0
      client/src/plugins/search/config.json
  55. 29 9
      client/src/plugins/system/DataCard.tsx
  56. 12 2
      client/src/plugins/system/NodeListView.tsx
  57. 9 7
      client/src/plugins/system/SystemView.tsx
  58. 25 11
      client/src/plugins/system/Topology.tsx
  59. 3 0
      client/src/plugins/system/Types.ts
  60. 1 0
      client/src/plugins/system/config.json
  61. 6 6
      client/src/router/Config.ts
  62. 3 1
      client/src/router/Types.ts
  63. 61 0
      client/src/styles/theme.ts
  64. 70 0
      client/src/types/SearchTypes.ts
  65. 2 2
      client/src/utils/Form.ts
  66. 48 5
      client/src/utils/Format.ts
  67. 139 0
      client/src/utils/SizingTool.ts
  68. 21 11
      client/src/utils/search.ts
  69. 2 1
      client/tsconfig.paths.json
  70. 57 2
      client/yarn.lock
  71. 30 0
      codecov.yml
  72. 7 0
      express/.prettierrc
  73. 31 5
      express/package.json
  74. 165 0
      express/src/__tests__/__mocks__/consts.ts
  75. 353 0
      express/src/__tests__/__mocks__/milvus/milvusClient.ts
  76. 9 0
      express/src/__tests__/__mocks__/milvus/milvusService.ts
  77. 302 0
      express/src/__tests__/collections/collections.service.test.ts
  78. 160 0
      express/src/__tests__/crons/crons.service.test.ts
  79. 54 0
      express/src/__tests__/milvus/milvus.controller.test.ts
  80. 82 0
      express/src/__tests__/milvus/milvus.service.test.ts
  81. 144 0
      express/src/__tests__/partitions/partitions.service.test.ts
  82. 114 0
      express/src/__tests__/schema/schema.service.test.ts
  83. 17 0
      express/src/__tests__/utils/constants.ts
  84. 14 0
      express/src/__tests__/utils/mock.util.ts
  85. 1 1
      express/src/app.ts
  86. 41 38
      express/src/collections/collections.controller.ts
  87. 15 9
      express/src/collections/collections.service.ts
  88. 247 0
      express/src/collections/swagger.yml
  89. 28 18
      express/src/crons/crons.service.ts
  90. 28 0
      express/src/crons/swagger.yml
  91. 9 4
      express/src/middlewares/validation.ts
  92. 3 6
      express/src/milvus/milvus.controller.ts
  93. 7 7
      express/src/milvus/milvus.service.ts
  94. 70 0
      express/src/milvus/swagger.yml
  95. 17 21
      express/src/partitions/partitions.controller.ts
  96. 4 4
      express/src/partitions/partitions.service.ts
  97. 97 0
      express/src/partitions/swagger.yml
  98. 4 21
      express/src/schema/schema.controller.ts
  99. 78 0
      express/src/schema/swagger.yml
  100. 1 1
      express/src/swagger.ts

+ 14 - 0
.github/workflows/dev.yml

@@ -16,6 +16,20 @@ jobs:
         with:
           node-version: 12
 
+      - name: Run Express tests
+        run: |
+          cd express
+          yarn install
+          yarn test:cov
+
+      - name: Upload coverage to Codecov
+        uses: codecov/codecov-action@v2
+        with:
+          # public repo needn't pass token
+          # token: ${{ secrets.CODECOV_TOKEN }}
+          # only upload express test coverage
+          flags: express
+
       - name: Login to DockerHub
         uses: docker/login-action@v1
         with:

+ 2 - 0
README.md

@@ -1,6 +1,8 @@
 # Milvus insight
+
 [![typescript](https://badges.aleen42.com/src/typescript.svg)](https://badges.aleen42.com/src/typescript.svg)
 [![downloads](https://img.shields.io/docker/pulls/milvusdb/milvus-insight)](https://img.shields.io/docker/pulls/milvusdb/milvus-insight)
+[![codecov](https://codecov.io/gh/zilliztech/milvus-insight/branch/main/graph/badge.svg?token=jvIEVF9IwW)](https://codecov.io/gh/zilliztech/milvus-insight)
 
 Milvus insight provides an intuitive and efficient GUI for Milvus, allowing you to interact with your databases and manage your data with just few clicks.
 

+ 11 - 2
client/README.md

@@ -16,7 +16,8 @@
       ├── hooks                   # React hooks
       ├── http                    # Http request api. And we have http interceptor in GlobalEffect.tsx file
       ├── i18n                    # Language i18n
-      ├── pages                   # All pages , business components and types.
+      ├── pages                   # All pages, business components and types.
+      ├── plugins                 # All import plugins.
       ├── router                  # React router, control the page auth.
       ├── styles                  # Styles, normally we use material to control styles.
       ├── types                   # Global types
@@ -60,6 +61,14 @@ Like utils / consts / utils / hooks , we dont want put all functions or datas in
 
 So when we need to create new file , treat the file like Class then name it.
 
+### Plugins Folder
+
+You can deploy any plugin developed by [template](https://github.com/zilliztech/insight-plugin-template). All client plugins should be placed at `src/plugins` folder. We have transferred `System View` and `Vector Search` to plugins. For more plugins development details please refer to [template repo](https://github.com/zilliztech/insight-plugin-template).
+
+### Alias Map
+
+As `react-app-rewire-alias` in `config-overrides.js`, we can use alias import. `insight_src/` is equal to `client/src` .
+
 ### Icon
 
 We put all icons in components/icons file. Normally we use material icon.
@@ -74,4 +83,4 @@ We use react-app-rewired to change webpack config.
 
 If we want to change the webpack config, we can edit config-overrides.js file.
 
-And we use milvus insight server to host our client site. So our build path is `../server/build` .
+Our build path is `./build`. And we use milvus insight server to host our client site.

+ 4 - 0
client/package.json

@@ -6,10 +6,12 @@
   "bugs": "https://github.com/milvus-io/milvus-insight/issues",
   "private": true,
   "dependencies": {
+    "@date-io/dayjs": "1.x",
     "@loadable/component": "^5.15.0",
     "@material-ui/core": "4.11.4",
     "@material-ui/icons": "^4.11.2",
     "@material-ui/lab": "4.0.0-alpha.58",
+    "@material-ui/pickers": "^3.3.10",
     "@mui/x-data-grid": "^4.0.0",
     "@testing-library/jest-dom": "^5.11.4",
     "@testing-library/react": "^11.1.0",
@@ -24,6 +26,7 @@
     "@types/react-syntax-highlighter": "^13.5.2",
     "axios": "^0.21.3",
     "dayjs": "^1.10.5",
+    "file-saver": "^2.0.5",
     "i18next": "^20.3.1",
     "papaparse": "^5.3.1",
     "react": "^17.0.2",
@@ -74,6 +77,7 @@
   },
   "devDependencies": {
     "@testing-library/react-hooks": "^7.0.1",
+    "@types/file-saver": "^2.0.4",
     "@types/loadable__component": "^5.13.4",
     "@types/webpack-env": "^1.16.3",
     "prettier": "2.3.2"

+ 5 - 1
client/src/App.tsx

@@ -1,3 +1,5 @@
+import { MuiPickersUtilsProvider } from '@material-ui/pickers';
+import DayjsUtils from '@date-io/dayjs';
 import Router from './router/Router';
 import { RootProvider } from './context/Root';
 import { NavProvider } from './context/Navigation';
@@ -10,7 +12,9 @@ function App() {
       <AuthProvider>
         <WebSocketProvider>
           <NavProvider>
-            <Router></Router>
+            <MuiPickersUtilsProvider utils={DayjsUtils}>
+              <Router></Router>
+            </MuiPickersUtilsProvider>
           </NavProvider>
         </WebSocketProvider>
       </AuthProvider>

+ 28 - 39
client/src/components/advancedSearch/Condition.tsx

@@ -1,4 +1,4 @@
-import React, { useState, useEffect, FC } from 'react';
+import React, { useState, useEffect, FC, useMemo } from 'react';
 import {
   makeStyles,
   Theme,
@@ -9,39 +9,8 @@ import {
 import CloseIcon from '@material-ui/icons/Close';
 import { ConditionProps, Field } from './Types';
 import CustomSelector from '../customSelector/CustomSelector';
-
-// Todo: Move to corrsponding Constant file.
-// Static logical operators.
-const LogicalOperators = [
-  {
-    value: '<',
-    label: '<',
-  },
-  {
-    value: '<=',
-    label: '<=',
-  },
-  {
-    value: '>',
-    label: '>',
-  },
-  {
-    value: '>=',
-    label: '>=',
-  },
-  {
-    value: '==',
-    label: '==',
-  },
-  {
-    value: '!=',
-    label: '!=',
-  },
-  {
-    value: 'in',
-    label: 'in',
-  },
-];
+import { LOGICAL_OPERATORS } from '../../consts/Util';
+import { DataTypeStringEnum } from '../../pages/collections/Types';
 
 const Condition: FC<ConditionProps> = props => {
   const {
@@ -54,9 +23,9 @@ const Condition: FC<ConditionProps> = props => {
     ...others
   } = props;
   const [operator, setOperator] = useState(
-    initData?.op || LogicalOperators[0].value
+    initData?.op || LOGICAL_OPERATORS[0].value
   );
-  const [conditionField, setConditionField] = useState<Field | any>(
+  const [conditionField, setConditionField] = useState<Field>(
     initData?.field || fields[0] || {}
   );
   const [conditionValue, setConditionValue] = useState(initData?.value || '');
@@ -80,16 +49,26 @@ const Condition: FC<ConditionProps> = props => {
     const conditionValueWithNoSpace = conditionValue.replaceAll(' ', '');
 
     switch (type) {
-      case 'int':
+      case DataTypeStringEnum.Int8:
+      case DataTypeStringEnum.Int16:
+      case DataTypeStringEnum.Int32:
+      case DataTypeStringEnum.Int64:
+        // case DataTypeStringEnum:
         isLegal = isIn
           ? regIntInterval.test(conditionValueWithNoSpace)
           : regInt.test(conditionValueWithNoSpace);
         break;
-      case 'float':
+      case DataTypeStringEnum.Float:
+      case DataTypeStringEnum.Double:
+      case DataTypeStringEnum.FloatVector:
         isLegal = isIn
           ? regFloatInterval.test(conditionValueWithNoSpace)
           : regFloat.test(conditionValueWithNoSpace);
         break;
+      case DataTypeStringEnum.Bool:
+        const legalValues = ['false', 'true'];
+        isLegal = legalValues.includes(conditionValueWithNoSpace);
+        break;
       default:
         isLegal = false;
         break;
@@ -108,6 +87,16 @@ const Condition: FC<ConditionProps> = props => {
 
   const classes = useStyles();
 
+  const logicalOperators = useMemo(() => {
+    if (conditionField.type === DataTypeStringEnum.Bool) {
+      const data = LOGICAL_OPERATORS.filter(v => v.value === '==');
+      setOperator(data[0].value);
+      // bool only support ==
+      return data;
+    }
+    return LOGICAL_OPERATORS;
+  }, [conditionField]);
+
   // Logic operator input change.
   const handleOpChange = (event: React.ChangeEvent<{ value: unknown }>) => {
     setOperator(event.target.value);
@@ -140,7 +129,7 @@ const Condition: FC<ConditionProps> = props => {
         label="Logic"
         value={operator}
         onChange={handleOpChange}
-        options={LogicalOperators}
+        options={logicalOperators}
         variant="filled"
         wrapperClass={classes.logic}
       />

+ 2 - 2
client/src/components/advancedSearch/Types.ts

@@ -1,4 +1,4 @@
-// import { ReactElement } from 'react';
+import { DataTypeStringEnum } from '../../pages/collections/Types';
 
 export interface ConditionProps {
   others?: object;
@@ -12,7 +12,7 @@ export interface ConditionProps {
 
 export interface Field {
   name: string;
-  type: 'int' | 'float';
+  type: DataTypeStringEnum;
 }
 
 export interface TriggerChangeData {

+ 77 - 0
client/src/components/customDatePicker/CustomDatePicker.tsx

@@ -0,0 +1,77 @@
+import { FC, useState } from 'react';
+import { DateTimePicker } from '@material-ui/pickers';
+import Icons from '../icons/Icons';
+import { makeStyles, Theme, Typography } from '@material-ui/core';
+import { MaterialUiPickersDate } from '@material-ui/pickers/typings/date';
+import { DatePickerType } from './Types';
+import { useTranslation } from 'react-i18next';
+
+const useStyles = makeStyles((theme: Theme) => ({
+  wrapper: {
+    display: 'flex',
+    alignItems: 'center',
+    marginLeft: '10px',
+    cursor: 'pointer',
+  },
+  icon: {
+    color: (props: any) =>
+      props.date ? theme.palette.primary.main : '#82838E',
+  },
+  label: {
+    marginLeft: '4px',
+    color: (props: any) =>
+      props.date ? theme.palette.primary.main : '#82838E',
+    fontWeight: 'bold',
+  },
+  picker: {
+    width: 0,
+  },
+  clear: {
+    fontSize: '14px',
+    color: theme.palette.primary.main,
+    marginLeft: '4px',
+  },
+}));
+
+export const CustomDatePicker: FC<DatePickerType> = props => {
+  const { onChange, label, date, setDate } = props;
+  const [open, setOpen] = useState(false);
+  const classes = useStyles({ date });
+  const { t: btnTrans } = useTranslation('btn');
+
+  const DatePickerIcon = Icons.datePicker;
+  const ClearIcon = Icons.clear;
+
+  const handleChange = (value: MaterialUiPickersDate) => {
+    setDate(value);
+    onChange(value);
+  };
+
+  const handleClear = (e: any) => {
+    e.stopPropagation();
+    handleChange(null);
+  };
+  return (
+    <>
+      <div
+        className={classes.wrapper}
+        onClick={() => {
+          setOpen(true);
+        }}
+      >
+        <DatePickerIcon className={classes.icon} />
+        <Typography className={classes.label}>{label}</Typography>
+        {date && <ClearIcon onClick={handleClear} className={classes.clear} />}
+      </div>
+
+      <DateTimePicker
+        className={classes.picker}
+        value={date}
+        open={open}
+        onChange={handleChange}
+        onClose={() => setOpen(false)}
+        okLabel={btnTrans('confirm')}
+      ></DateTimePicker>
+    </>
+  );
+};

+ 8 - 0
client/src/components/customDatePicker/Types.ts

@@ -0,0 +1,8 @@
+import { MaterialUiPickersDate } from '@material-ui/pickers/typings/date';
+
+export type DatePickerType = {
+  label: string;
+  onChange: (value: any) => void;
+  date: MaterialUiPickersDate;
+  setDate: (value: MaterialUiPickersDate) => void;
+};

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

@@ -22,6 +22,8 @@ import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown';
 import CachedIcon from '@material-ui/icons/Cached';
 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 { SvgIcon } from '@material-ui/core';
 import { ReactComponent as MilvusIcon } from '../../assets/icons/milvus.svg';
 import { ReactComponent as OverviewIcon } from '../../assets/icons/overview.svg';
@@ -60,6 +62,8 @@ const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
   refresh: (props = {}) => <CachedIcon {...props} />,
   filter: (props = {}) => <FilterListIcon {...props} />,
   alias: (props = {}) => <AlternateEmailIcon {...props} />,
+  datePicker: (props = {}) => <DatePicker {...props} />,
+  download: (props = {}) => <GetAppIcon {...props} />,
 
   milvus: (props = {}) => (
     <SvgIcon viewBox="0 0 44 31" component={MilvusIcon} {...props} />

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

@@ -33,4 +33,6 @@ export type IconsType =
   | 'refresh'
   | 'filter'
   | 'copyExpression'
-  | 'alias';
+  | 'alias'
+  | 'datePicker'
+  | 'download';

+ 19 - 11
client/src/components/layout/Layout.tsx

@@ -9,6 +9,7 @@ import { useTranslation } from 'react-i18next';
 import { useHistory, useLocation } from 'react-router-dom';
 import { authContext } from '../../context/Auth';
 import { IconsType } from '../icons/Types';
+import loadable from '@loadable/component';
 
 const PLUGIN_DEV = process.env?.REACT_APP_PLUGIN_DEV;
 
@@ -81,16 +82,16 @@ const Layout = (props: any) => {
       label: navTrans('collection'),
       onClick: () => history.push('/collections'),
     },
-    {
-      icon: icons.navSearch,
-      label: navTrans('search'),
-      onClick: () => history.push('/search'),
-      iconActiveClass: 'activeSearchIcon',
-      iconNormalClass: 'normalSearchIcon',
-    },
+    // {
+    //   icon: icons.navSearch,
+    //   label: navTrans('search'),
+    //   onClick: () => history.push('/search'),
+    //   iconActiveClass: 'activeSearchIcon',
+    //   iconNormalClass: 'normalSearchIcon',
+    // },
   ];
 
-  function importAll(r: any) {
+  function importAll(r: any, outOfRoot = false) {
     r.keys().forEach((key: any) => {
       const content = r(key);
       const pathName = content.client?.path;
@@ -99,11 +100,18 @@ const Layout = (props: any) => {
         icon: icons.navOverview,
         label: content.client?.label || 'PLGUIN',
       };
-      result.onClick = () => history.push(`${pathName}`);
+      result.onClick = () => history.push(`/${pathName}`);
       const iconName: IconsType = content.client?.iconName;
+      const iconEntry = content.client?.icon;
+      const dirName = key.split('/config.json').shift().split('/')[1];
+      // const fileEntry = content.client?.entry;
       if (iconName) {
-        // TODO: support custom icon
         result.icon = icons[iconName];
+      } else if (iconEntry) {
+        const customIcon = outOfRoot
+          ? loadable(() => import(`all_plugins/${dirName}/client/${iconEntry}`))
+          : loadable(() => import(`../../plugins/${dirName}/${iconEntry}`));
+        result.icon = customIcon;
       }
       content.client?.iconActiveClass &&
         (result.iconActiveClass = content.client?.iconActiveClass);
@@ -114,7 +122,7 @@ const Layout = (props: any) => {
   }
   importAll(require.context('../../plugins', true, /config\.json$/));
   PLUGIN_DEV &&
-    importAll(require.context('all_plugins/', true, /config\.json$/));
+    importAll(require.context('all_plugins/', true, /config\.json$/), true);
 
   return (
     <div className={classes.root}>

+ 6 - 3
client/src/components/menu/Types.ts

@@ -1,5 +1,6 @@
 import { ButtonProps } from '@material-ui/core/Button';
 import { ReactElement } from 'react';
+import { LoadableClassComponent } from '@loadable/component';
 
 export type SimpleMenuType = {
   label: string;
@@ -14,10 +15,12 @@ export type SimpleMenuType = {
   menuItemWidth?: string;
 };
 
+type CustomIcon = (
+  props?: any
+) => React.ReactElement<any, string | React.JSXElementConstructor<any>>;
+
 export type NavMenuItem = {
-  icon: (
-    props?: any
-  ) => React.ReactElement<any, string | React.JSXElementConstructor<any>>;
+  icon: CustomIcon | LoadableClassComponent<any>;
   iconActiveClass?: string;
   iconNormalClass?: string;
   label: string;

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

@@ -206,3 +206,6 @@ export enum LOADING_STATE {
   LOADING,
   UNLOADED,
 }
+
+export const DEFAULT_VECTORS = 100000;
+export const DEFAULT_SEFMENT_FILE_SIZE = 1024;

+ 31 - 0
client/src/consts/Util.ts

@@ -4,3 +4,34 @@ export const BYTE_UNITS: { [x: string]: number } = {
   m: 1024 * 1024,
   g: 1024 * 1024 * 1024,
 };
+
+export const LOGICAL_OPERATORS = [
+  {
+    value: '<',
+    label: '<',
+  },
+  {
+    value: '<=',
+    label: '<=',
+  },
+  {
+    value: '>',
+    label: '>',
+  },
+  {
+    value: '>=',
+    label: '>=',
+  },
+  {
+    value: '==',
+    label: '==',
+  },
+  {
+    value: '!=',
+    label: '!=',
+  },
+  {
+    value: 'in',
+    label: 'in',
+  },
+];

+ 14 - 3
client/src/hooks/Navigation.ts

@@ -6,12 +6,15 @@ import { ALL_ROUTER_TYPES, NavInfo } from '../router/Types';
 export const useNavigationHook = (
   type: ALL_ROUTER_TYPES,
   extraParam?: {
-    collectionName: string;
+    collectionName?: string;
+    title?: string;
   }
 ) => {
   const { t: navTrans } = useTranslation('nav');
   const { setNavInfo } = useContext(navContext);
-  const { collectionName } = extraParam || { collectionName: '' };
+  const { collectionName = '', title = 'PLUGIN TITLE' } = extraParam || {
+    collectionName: '',
+  };
 
   useEffect(() => {
     switch (type) {
@@ -55,8 +58,16 @@ export const useNavigationHook = (
         setNavInfo(navInfo);
         break;
       }
+      case ALL_ROUTER_TYPES.PLUGIN: {
+        const navInfo: NavInfo = {
+          navTitle: title,
+          backPath: '',
+        };
+        setNavInfo(navInfo);
+        break;
+      }
       default:
         break;
     }
-  }, [type, navTrans, setNavInfo, collectionName]);
+  }, [type, navTrans, setNavInfo, collectionName, title]);
 };

+ 33 - 0
client/src/hooks/TimeTravel.ts

@@ -0,0 +1,33 @@
+import dayjs from 'dayjs';
+import { formatUtcToMilvus } from '../utils/Format';
+import { useMemo, useState } from 'react';
+import { MaterialUiPickersDate } from '@material-ui/pickers/typings/date';
+import { useTranslation } from 'react-i18next';
+
+export const useTimeTravelHook = () => {
+  const [timeTravel, setTimeTravel] = useState<MaterialUiPickersDate>(null);
+  const { t: searchTrans } = useTranslation('search');
+
+  const timeTravelInfo = useMemo(() => {
+    const timestamp = dayjs(timeTravel).valueOf();
+    return {
+      label: timeTravel
+        ? ` ${searchTrans('timeTravelPrefix')} ${dayjs(timeTravel).format(
+            'YYYY-MM-DD HH:mm:ss'
+          )}`
+        : searchTrans('timeTravel'),
+      timestamp: timeTravel ? formatUtcToMilvus(timestamp) : undefined,
+    };
+  }, [searchTrans, timeTravel]);
+
+  const handleDateTimeChange = (value: MaterialUiPickersDate) => {
+    setTimeTravel(value);
+  };
+
+  return {
+    timeTravel,
+    setTimeTravel,
+    handleDateTimeChange,
+    timeTravelInfo,
+  };
+};

+ 13 - 2
client/src/http/Collection.ts

@@ -1,7 +1,11 @@
 import { ChildrenStatusType } from '../components/status/Types';
-import { CollectionView, InsertDataParam } from '../pages/collections/Types';
+import {
+  CollectionView,
+  DeleteEntitiesReq,
+  InsertDataParam,
+} from '../pages/collections/Types';
 import { Field } from '../pages/schema/Types';
-import { VectorSearchParam } from '../pages/seach/Types';
+import { VectorSearchParam } from '../types/SearchTypes';
 import { QueryParam } from '../pages/query/Types';
 import { IndexState, ShowCollectionsType } from '../types/Milvus';
 import { formatNumber } from '../utils/Common';
@@ -85,6 +89,13 @@ export class CollectionHttp extends BaseModel implements CollectionView {
     });
   }
 
+  static deleteEntities(collectionName: string, param: DeleteEntitiesReq) {
+    return super.update({
+      path: `${this.COLLECTIONS_URL}/${collectionName}/entities`,
+      data: param,
+    });
+  }
+
   static vectorSearchData(collectionName: string, params: VectorSearchParam) {
     return super.query({
       path: `${this.COLLECTIONS_URL}/${collectionName}/search`,

+ 2 - 2
client/src/http/Field.ts

@@ -1,9 +1,9 @@
-import { DataType } from '../pages/collections/Types';
+import { DataTypeStringEnum } from '../pages/collections/Types';
 import { FieldData } from '../pages/schema/Types';
 import BaseModel from './BaseModel';
 
 export class FieldHttp extends BaseModel implements FieldData {
-  data_type!: DataType;
+  data_type!: DataTypeStringEnum;
   fieldID!: string;
   type_params!: { key: string; value: string }[];
   is_primary_key!: true;

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

@@ -10,8 +10,11 @@ const collectionTrans = {
   deleteTooltip: 'Please select at least one item to delete.',
   alias: 'alias',
   aliasTooltip: 'Please select one collection to create alias',
+  download: 'Download',
+  downloadTooltip: 'Download all query results',
 
   collection: 'Collection',
+  entites: 'entites',
 
   // table
   id: 'ID',
@@ -54,6 +57,7 @@ const collectionTrans = {
   // delete dialog
   deleteWarning:
     'You are trying to delete a collection with data. This action cannot be undone.',
+  deleteDataWarning: `You are trying to delete entites. This action cannot be undone.`,
 
   // collection tabs
   partitionTab: 'Partitions',

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

@@ -38,6 +38,20 @@ const commonTrans = {
     join: 'Join our growing social community today',
     get: 'Get insight, tips and share ideas',
   },
+
+  capacity: {
+    b: 'B',
+    kb: 'KB',
+    mb: 'MB',
+    gb: 'GB',
+    tb: 'TB',
+    pb: 'PB',
+  },
+
+  size: 'Approximate size',
+  tip: 'Use 100k vectors and 1024 segment file size as example',
+  disk: 'Disk',
+  memory: 'Memory',
 };
 
 export default commonTrans;

+ 2 - 0
client/src/i18n/cn/search.ts

@@ -13,6 +13,8 @@ const searchTrans = {
   filter: 'Advanced Filter',
   vectorValueWarning:
     'Vector value should be an array of length {{dimension}}(dimension)',
+  timeTravel: 'Time Travel',
+  timeTravelPrefix: 'Datas before',
 };
 
 export default searchTrans;

+ 2 - 1
client/src/i18n/cn/systemView.ts

@@ -2,8 +2,9 @@ const systemViewTrans = {
   diskTitle: 'disk',
   memoryTitle: 'memory',
   qpsTitle: 'qps',
-  letencyTitle: 'letency',
+  latencyTitle: 'latency',
   hardwareTitle: 'hardware',
+  configTitle: 'config',
   valueTitle: 'value',
   systemTitle: 'system',
   thName: 'Node Name',

+ 5 - 2
client/src/i18n/en/collection.ts

@@ -10,8 +10,11 @@ const collectionTrans = {
   deleteTooltip: 'Please select at least one item to delete.',
   alias: 'alias',
   aliasTooltip: 'Please select one collection to create alias',
+  download: 'Download',
+  downloadTooltip: 'Download all query results',
 
   collection: 'Collection',
+  entites: 'entites',
 
   // table
   id: 'ID',
@@ -52,8 +55,8 @@ const collectionTrans = {
   releaseConfirmLabel: 'Release',
 
   // delete dialog
-  deleteWarning:
-    'You are trying to delete a collection with data. This action cannot be undone.',
+  deleteWarning: `You are trying to delete a collection with data. This action cannot be undone.`,
+  deleteDataWarning: `You are trying to delete entites. This action cannot be undone.`,
 
   // collection tabs
   partitionTab: 'Partitions',

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

@@ -46,6 +46,11 @@ const commonTrans = {
     tb: 'TB',
     pb: 'PB',
   },
+
+  size: 'Approximate size',
+  tip: 'Use 100k vectors and 1024 segment file size as example',
+  disk: 'Disk',
+  memory: 'Memory',
 };
 
 export default commonTrans;

+ 2 - 0
client/src/i18n/en/search.ts

@@ -12,6 +12,8 @@ const searchTrans = {
   topK: 'TopK {{number}}',
   filter: 'Advanced Filter',
   vectorValueWarning: 'Vector value should be an array of length {{dimension}}',
+  timeTravel: 'Time Travel',
+  timeTravelPrefix: 'Datas before',
 };
 
 export default searchTrans;

+ 2 - 1
client/src/i18n/en/systemView.ts

@@ -2,8 +2,9 @@ const systemViewTrans = {
   diskTitle: 'disk',
   memoryTitle: 'memory',
   qpsTitle: 'qps',
-  letencyTitle: 'letency',
+  latencyTitle: 'latency',
   hardwareTitle: 'hardware',
+  configTitle: 'config',
   valueTitle: 'value',
   systemTitle: 'system',
   thName: 'Node Name',

+ 25 - 24
client/src/pages/collections/Collections.tsx

@@ -36,7 +36,7 @@ import { LOADING_STATE } from '../../consts/Milvus';
 import { webSokcetContext } from '../../context/WebSocket';
 import { WS_EVENTS, WS_EVENTS_TYPE } from '../../consts/Http';
 import { checkIndexBuilding, checkLoading } from '../../utils/Validation';
-import CreateAlias from './CreateAlias';
+// import CreateAlias from './CreateAlias';
 
 const useStyles = makeStyles((theme: Theme) => ({
   emptyWrapper: {
@@ -309,29 +309,30 @@ const Collections = () => {
       disabledTooltip: collectionTrans('deleteTooltip'),
       disabled: data => data.length === 0,
     },
-    {
-      type: 'iconBtn',
-      onClick: () => {
-        setDialog({
-          open: true,
-          type: 'custom',
-          params: {
-            component: (
-              <CreateAlias
-                collectionName={selectedCollections[0]._name}
-                cb={() => {
-                  setSelectedCollections([]);
-                }}
-              />
-            ),
-          },
-        });
-      },
-      label: collectionTrans('alias'),
-      icon: 'alias',
-      disabledTooltip: collectionTrans('aliasTooltip'),
-      disabled: data => data.length !== 1,
-    },
+    // Todo: hide alias after we can get all alias from milvus.
+    // {
+    //   type: 'iconBtn',
+    //   onClick: () => {
+    //     setDialog({
+    //       open: true,
+    //       type: 'custom',
+    //       params: {
+    //         component: (
+    //           <CreateAlias
+    //             collectionName={selectedCollections[0]._name}
+    //             cb={() => {
+    //               setSelectedCollections([]);
+    //             }}
+    //           />
+    //         ),
+    //       },
+    //     });
+    //   },
+    //   label: collectionTrans('alias'),
+    //   icon: 'alias',
+    //   disabledTooltip: collectionTrans('aliasTooltip'),
+    //   disabled: data => data.length !== 1,
+    // },
     {
       label: 'Search',
       icon: 'search',

+ 5 - 0
client/src/pages/collections/Constants.ts

@@ -38,6 +38,11 @@ export const ALL_OPTIONS: KeyValuePair[] = [
     label: 'Double',
     value: DataTypeEnum.Double,
   },
+
+  {
+    label: 'Boolean',
+    value: DataTypeEnum.Bool,
+  },
 ];
 
 export const AUTO_ID_OPTIONS: KeyValuePair[] = [

+ 17 - 10
client/src/pages/collections/Types.ts

@@ -32,6 +32,7 @@ export interface CollectionCreateParam {
 }
 
 export enum DataTypeEnum {
+  Bool = 1,
   Int8 = 2,
   Int16 = 3,
   Int32 = 4,
@@ -41,16 +42,17 @@ export enum DataTypeEnum {
   BinaryVector = 100,
   FloatVector = 101,
 }
-
-export type DataType =
-  | 'Int8'
-  | 'Int16'
-  | 'Int32'
-  | 'Int64'
-  | 'Float'
-  | 'Double'
-  | 'BinaryVector'
-  | 'FloatVector';
+export enum DataTypeStringEnum {
+  Bool = 'Bool',
+  Int8 = 'Int8',
+  Int16 = 'Int16',
+  Int32 = 'Int32',
+  Int64 = 'Int64',
+  Float = 'Float',
+  Double = 'Double',
+  BinaryVector = 'BinaryVector',
+  FloatVector = 'FloatVector',
+}
 
 export interface Field {
   name: string | null;
@@ -91,3 +93,8 @@ export interface InsertDataParam {
   // e.g. [{vector: [1,2,3], age: 10}]
   fields_data: any[];
 }
+
+export interface DeleteEntitiesReq {
+  expr: string;
+  partition_name?: string;
+}

+ 121 - 12
client/src/pages/query/Query.tsx

@@ -1,16 +1,27 @@
-import { FC, useEffect, useState, useRef, useMemo } from 'react';
+import { FC, useEffect, useState, useRef, useMemo, useContext } from 'react';
 import { useTranslation } from 'react-i18next';
 
+import { rootContext } from '../../context/Root';
+
 import EmptyCard from '../../components/cards/EmptyCard';
 import icons from '../../components/icons/Icons';
 import CustomButton from '../../components/customButton/CustomButton';
 import MilvusGrid from '../../components/grid/Grid';
+import { ToolBarConfig } from '../../components/grid/Types';
 import { getQueryStyles } from './Styles';
 import Filter from '../../components/advancedSearch';
 import { CollectionHttp } from '../../http/Collection';
 import { FieldHttp } from '../../http/Field';
 import { usePaginationHook } from '../../hooks/Pagination';
+// import { useTimeTravelHook } from '../../hooks/TimeTravel';
+
 import CopyButton from '../../components/advancedSearch/CopyButton';
+import DeleteTemplate from '../../components/customDialog/DeleteDialogTemplate';
+import CustomToolBar from '../../components/grid/ToolBar';
+// import { CustomDatePicker } from '../../components/customDatePicker/CustomDatePicker';
+import { saveAs } from 'file-saver';
+import { generateCsvData } from '../../utils/Format';
+import { DataTypeStringEnum } from '../collections/Types';
 
 const Query: FC<{
   collectionName: string;
@@ -19,10 +30,16 @@ const Query: FC<{
   const [expression, setExpression] = useState('');
   const [tableLoading, setTableLoading] = useState<any>();
   const [queryResult, setQueryResult] = useState<any>();
+  const [selectedDatas, setSelectedDatas] = useState<any[]>([]);
+  const [primaryKey, setPrimaryKey] = useState<string>('');
 
+  const { setDialog, handleCloseDialog, openSnackBar } =
+    useContext(rootContext);
   const VectorSearchIcon = icons.vectorSearch;
   const ResetIcon = icons.refresh;
 
+  const { t: dialogTrans } = useTranslation('dialog');
+  const { t: successTrans } = useTranslation('success');
   const { t: searchTrans } = useTranslation('search');
   const { t: collectionTrans } = useTranslation('collection');
   const { t: btnTrans } = useTranslation('btn');
@@ -31,6 +48,9 @@ const Query: FC<{
 
   const classes = getQueryStyles();
 
+  // const { timeTravel, setTimeTravel, timeTravelInfo, handleDateTimeChange } =
+  //   useTimeTravelHook();
+
   // Format result list
   const queryResultMemo = useMemo(
     () =>
@@ -39,7 +59,7 @@ const Query: FC<{
         const tmp = Object.keys(resultItem).reduce(
           (prev: { [key: string]: any }, item: string) => {
             if (Array.isArray(resultItem[item])) {
-              const list2Str = `[${resultItem[item]}]`;
+              const list2Str = JSON.stringify(resultItem[item]);
               prev[item] = (
                 <div className={classes.vectorTableCell}>
                   <div>{list2Str}</div>
@@ -51,7 +71,7 @@ const Query: FC<{
                 </div>
               );
             } else {
-              prev[item] = resultItem[item];
+              prev[item] = `${resultItem[item]}`;
             }
             return prev;
           },
@@ -62,6 +82,14 @@ const Query: FC<{
     [queryResult, classes.vectorTableCell, classes.copyBtn, copyTrans.label]
   );
 
+  const csvDataMemo = useMemo(() => {
+    const headers: string[] = fields?.map(i => i.name);
+    if (headers?.length && queryResult?.length) {
+      return generateCsvData(headers, queryResult);
+    }
+    return '';
+  }, [fields, queryResult]);
+
   const {
     pageSize,
     handlePageSize,
@@ -80,11 +108,18 @@ const Query: FC<{
 
   const getFields = async (collectionName: string) => {
     const schemaList = await FieldHttp.getFields(collectionName);
-    const nameList = schemaList.map(i => ({
-      name: i.name,
-      type: i.data_type.includes('Int') ? 'int' : 'float',
+    const nameList = schemaList.map(v => ({
+      name: v.name,
+      type: v.data_type,
     }));
-    setFields(nameList);
+    const primaryKey =
+      schemaList.find(v => v._isPrimaryKey === true)?._fieldName || '';
+    setPrimaryKey(primaryKey);
+    // Temporarily hide bool field due to incorrect return from SDK.
+    const fieldWithoutBool = nameList.filter(
+      i => i.type !== DataTypeStringEnum.Bool
+    );
+    setFields(fieldWithoutBool);
   };
 
   // Get fields at first or collection name changed.
@@ -100,17 +135,21 @@ const Query: FC<{
     setExpression('');
     setTableLoading(null);
     setQueryResult(null);
+    handleCurrentPage(0);
   };
+
   const handleFilterSubmit = (expression: string) => {
     setExpression(expression);
     setQueryResult(null);
   };
+
   const handleQuery = async () => {
     setTableLoading(true);
     try {
       const res = await CollectionHttp.queryData(collectionName, {
         expr: expression,
         output_fields: fields.map(i => i.name),
+        // travel_timestamp: timeTravelInfo.timestamp,
       });
       const result = res.data;
       setQueryResult(result);
@@ -121,22 +160,90 @@ const Query: FC<{
     }
   };
 
+  const handleSelectChange = (value: any) => {
+    setSelectedDatas(value);
+  };
+
+  const handleDelete = async () => {
+    await CollectionHttp.deleteEntities(collectionName, {
+      expr: `${primaryKey} in [${selectedDatas.map(v => v.id).join(',')}]`,
+    });
+    handleCloseDialog();
+    openSnackBar(successTrans('delete', { name: collectionTrans('entites') }));
+    handleQuery();
+  };
+
+  const toolbarConfigs: ToolBarConfig[] = [
+    {
+      type: 'iconBtn',
+      onClick: () => {
+        setDialog({
+          open: true,
+          type: 'custom',
+          params: {
+            component: (
+              <DeleteTemplate
+                label={btnTrans('delete')}
+                title={dialogTrans('deleteTitle', {
+                  type: collectionTrans('entites'),
+                })}
+                text={collectionTrans('deleteDataWarning')}
+                handleDelete={handleDelete}
+              />
+            ),
+          },
+        });
+      },
+      label: collectionTrans('delete'),
+      icon: 'delete',
+      // tooltip: collectionTrans('deleteTooltip'),
+      disabledTooltip: collectionTrans('deleteTooltip'),
+      disabled: () => selectedDatas.length === 0,
+    },
+    {
+      type: 'iconBtn',
+      onClick: () => {
+        const csvData = new Blob([csvDataMemo.toString()], {
+          type: 'text/csv;charset=utf-8',
+        });
+        saveAs(csvData, 'query_result.csv');
+      },
+      label: collectionTrans('delete'),
+      icon: 'download',
+      tooltip: collectionTrans('download'),
+      disabledTooltip: collectionTrans('downloadTooltip'),
+      disabled: () => !queryResult?.length,
+    },
+  ];
+
   return (
     <div className={classes.root}>
+      <CustomToolBar toolbarConfigs={toolbarConfigs} />
       <div className={classes.toolbar}>
         <div className="left">
-          <div>{`${
-            expression || collectionTrans('exprPlaceHolder')
-          }`}</div>
+          {/* <div className="expression"> */}
+          <div>{`${expression || collectionTrans('exprPlaceHolder')}`}</div>
           <Filter
             ref={filterRef}
             title="Advanced Filter"
-            fields={fields}
+            fields={fields.filter(
+              i =>
+                i.type !== DataTypeStringEnum.FloatVector &&
+                i.type !== DataTypeStringEnum.BinaryVector
+            )}
             filterDisabled={false}
             onSubmit={handleFilterSubmit}
             showTitle={false}
             showTooltip={false}
           />
+          {/* </div> */}
+
+          {/* <CustomDatePicker
+            label={timeTravelInfo.label}
+            onChange={handleDateTimeChange}
+            date={timeTravel}
+            setDate={setTimeTravel}
+          /> */}
         </div>
         <div className="right">
           <CustomButton className="btn" onClick={handleFilterReset}>
@@ -162,10 +269,12 @@ const Query: FC<{
             label: i.name,
           }))}
           primaryKey={fields.find(i => i.is_primary_key)?.name}
-          openCheckBox={false}
+          openCheckBox={true}
           isLoading={!!tableLoading}
           rows={result}
           rowCount={total}
+          selected={selectedDatas}
+          setSelected={handleSelectChange}
           page={currentPage}
           onChangePage={handlePageChange}
           rowsPerPage={pageSize}

+ 10 - 1
client/src/pages/query/Styles.ts

@@ -24,10 +24,19 @@ export const getQueryStyles = makeStyles((theme: Theme) => ({
       display: 'flex',
       justifyContent: 'space-between',
       alignItems: 'center',
-      width: 'calc(100% - 206px)',
+      flex: 1,
       padding: theme.spacing(0, 0, 0, 2),
       fontSize: theme.spacing(2),
       backgroundColor: '#F9F9F9',
+
+      '& .expression': {
+        display: 'flex',
+        justifyContent: 'space-between',
+        flex: 1,
+        alignItems: 'center',
+        padding: theme.spacing(0, 1.5),
+        backgroundColor: '#F9F9F9',
+      },
     },
 
     '& .right': {

+ 2 - 1
client/src/pages/query/Types.ts

@@ -2,4 +2,5 @@ export interface QueryParam {
   expr: string;
   partitions_names?: string[];
   output_fields?: string[];
-}
+  travel_timestamp?: string;
+}

+ 71 - 5
client/src/pages/schema/Create.tsx

@@ -4,6 +4,8 @@ import { CodeLanguageEnum, CodeViewData } from '../../components/code/Types';
 import DialogTemplate from '../../components/customDialog/DialogTemplate';
 import CustomSwitch from '../../components/customSwitch/CustomSwitch';
 import {
+  DEFAULT_SEFMENT_FILE_SIZE,
+  DEFAULT_VECTORS,
   INDEX_CONFIG,
   INDEX_OPTIONS_MAP,
   MetricType,
@@ -14,21 +16,31 @@ import { getCreateIndexJSCode } from '../../utils/code/Js';
 import { getCreateIndexPYCode } from '../../utils/code/Py';
 import { formatForm, getMetricOptions } from '../../utils/Form';
 import { getEmbeddingType } from '../../utils/search';
-import { DataType } from '../collections/Types';
+import { computMilvusRecommonds, formatSize } from '../../utils/SizingTool';
+import { DataTypeStringEnum } from '../collections/Types';
 import CreateForm from './CreateForm';
+import SizingInfo from './SizingInfo';
 import { IndexType, IndexExtraParam, INDEX_TYPES_ENUM } from './Types';
 
 const CreateIndex = (props: {
   collectionName: string;
-  fieldType: DataType;
+  fieldType: DataTypeStringEnum;
   handleCreate: (params: IndexExtraParam) => void;
   handleCancel: () => void;
 
   // used for code mode
   fieldName: string;
+  // used for sizing info
+  dimension: number;
 }) => {
-  const { collectionName, fieldType, handleCreate, handleCancel, fieldName } =
-    props;
+  const {
+    collectionName,
+    fieldType,
+    handleCreate,
+    handleCancel,
+    fieldName,
+    dimension,
+  } = props;
 
   const { t: indexTrans } = useTranslation('index');
   const { t: dialogTrans } = useTranslation('dialog');
@@ -84,7 +96,7 @@ const CreateIndex = (props: {
     });
 
     const { index_type, metric_type } = indexSetting;
-  
+
     const extraParams: IndexExtraParam = {
       index_type,
       metric_type,
@@ -108,6 +120,58 @@ const CreateIndex = (props: {
     return form;
   }, [indexSetting, indexCreateParams]);
 
+  // sizing info needed param
+  const sizingInfo = useMemo(() => {
+    const { index_type } = indexSetting;
+    const { nlist, m } = indexSetting;
+    const floatTypes = [
+      INDEX_TYPES_ENUM.IVF_FLAT,
+      INDEX_TYPES_ENUM.IVF_PQ,
+      INDEX_TYPES_ENUM.IVF_SQ8,
+      INDEX_TYPES_ENUM.IVF_SQ8_HYBRID,
+      INDEX_TYPES_ENUM.FLAT,
+    ];
+    const bytesTyps = [
+      INDEX_TYPES_ENUM.BIN_FLAT,
+      INDEX_TYPES_ENUM.BIN_IVF_FLAT,
+    ];
+    const supportedTypes = [...floatTypes, ...bytesTyps];
+    // check param validation
+    if (!supportedTypes.includes(index_type)) {
+      return null;
+    }
+
+    if (!nlist) {
+      return null;
+    }
+    if (index_type === INDEX_TYPES_ENUM.IVF_PQ && !m) {
+      return null;
+    }
+    // vector 100000, segment file size 1024 as default value
+    const milvusRecommends = computMilvusRecommonds(
+      DEFAULT_VECTORS,
+      dimension,
+      Number(nlist),
+      Number(m),
+      DEFAULT_SEFMENT_FILE_SIZE * 1024 * 1024
+    );
+
+    let memoryType = 'byteMemorySize';
+    let diskType = 'byteDiskSize';
+    if (floatTypes.includes(index_type)) {
+      memoryType = 'memorySize';
+      diskType = 'diskSize';
+    }
+
+    const memorySize = milvusRecommends[memoryType][index_type];
+    const diskSize = milvusRecommends[diskType][index_type];
+
+    return {
+      memory: formatSize(memorySize),
+      disk: formatSize(diskSize),
+    };
+  }, [dimension, indexSetting]);
+
   /**
    * create index code mode
    */
@@ -200,6 +264,8 @@ const CreateIndex = (props: {
         indexParams={indexCreateParams}
         indexTypeChange={onIndexTypeChange}
       />
+
+      <SizingInfo info={sizingInfo} />
     </DialogTemplate>
   );
 };

+ 1 - 0
client/src/pages/schema/IndexTypeElement.tsx

@@ -175,6 +175,7 @@ const IndexTypeElement: FC<{
             collectionName={collectionName}
             fieldName={data._fieldName}
             fieldType={data._fieldType}
+            dimension={Number(data._dimension)}
             handleCancel={handleCloseDialog}
             handleCreate={requestCreateIndex}
           />

+ 5 - 2
client/src/pages/schema/Schema.tsx

@@ -9,7 +9,7 @@ import CustomToolTip from '../../components/customToolTip/CustomToolTip';
 import { FieldHttp } from '../../http/Field';
 import { FieldView } from './Types';
 import IndexTypeElement from './IndexTypeElement';
-import { DataType } from '../collections/Types';
+import { DataTypeStringEnum } from '../collections/Types';
 import { IndexHttp } from '../../http/Index';
 
 const useStyles = makeStyles((theme: Theme) => ({
@@ -77,7 +77,10 @@ const Schema: FC<{
   const fetchSchemaListWithIndex = async (
     collectionName: string
   ): Promise<FieldView[]> => {
-    const vectorTypes: DataType[] = ['BinaryVector', 'FloatVector'];
+    const vectorTypes: DataTypeStringEnum[] = [
+      DataTypeStringEnum.BinaryVector,
+      DataTypeStringEnum.FloatVector,
+    ];
     const indexList = await IndexHttp.getIndexInfo(collectionName);
     const schemaList = await FieldHttp.getFields(collectionName);
     let fields: FieldView[] = [];

+ 72 - 0
client/src/pages/schema/SizingInfo.tsx

@@ -0,0 +1,72 @@
+import { makeStyles, Theme, Typography } from '@material-ui/core';
+import { FC } from 'react';
+import { useTranslation } from 'react-i18next';
+import CustomToolTip from '../../components/customToolTip/CustomToolTip';
+import icons from '../../components/icons/Icons';
+import { SizingInfoParam } from './Types';
+
+const useStyles = makeStyles((theme: Theme) => ({
+  wrapper: {
+    display: 'flex',
+    alignItems: 'flex-start',
+    justifyContent: 'space-between',
+  },
+  header: {
+    display: 'flex',
+
+    '& .title': {
+      color: theme.palette.milvusGrey.dark,
+    },
+  },
+  icon: {
+    fontSize: '20px',
+    marginLeft: theme.spacing(1),
+  },
+  info: {
+    display: 'flex',
+    flexDirection: 'column',
+    alignItems: 'flex-end',
+  },
+  pair: {
+    display: 'flex',
+
+    '& .key': {
+      marginRight: theme.spacing(2),
+      color: theme.palette.milvusGrey.dark,
+    },
+  },
+}));
+
+const SizingInfo: FC<SizingInfoParam> = props => {
+  const { info } = props;
+  const { t: commonTrans } = useTranslation();
+  const InfoIcon = icons.info;
+
+  const classes = useStyles();
+
+  return (
+    info && (
+      <section className={classes.wrapper}>
+        <div className={classes.header}>
+          <Typography className="title">{commonTrans('size')}</Typography>
+          <CustomToolTip title={commonTrans('tip')} placement="top">
+            <InfoIcon classes={{ root: classes.icon }} />
+          </CustomToolTip>
+        </div>
+
+        <div className={classes.info}>
+          <div className={classes.pair}>
+            <Typography className="key">{commonTrans('memory')}</Typography>
+            <Typography>{info.memory}</Typography>
+          </div>
+          <div className={classes.pair}>
+            <Typography className="key">{commonTrans('disk')}</Typography>
+            <Typography>{info.disk}</Typography>
+          </div>
+        </div>
+      </section>
+    )
+  );
+};
+
+export default SizingInfo;

+ 10 - 3
client/src/pages/schema/Types.ts

@@ -1,6 +1,6 @@
 import { ReactElement } from 'react';
 import { MetricType } from '../../consts/Milvus';
-import { DataType } from '../collections/Types';
+import { DataTypeStringEnum } from '../collections/Types';
 
 export enum INDEX_TYPES_ENUM {
   IVF_FLAT = 'IVF_FLAT',
@@ -16,7 +16,7 @@ export enum INDEX_TYPES_ENUM {
 }
 
 export interface Field {
-  data_type: DataType;
+  data_type: DataTypeStringEnum;
   fieldID: string;
   type_params: { key: string; value: string }[];
   is_primary_key: true;
@@ -30,7 +30,7 @@ export interface FieldData {
   _isAutoId: boolean;
   _fieldName: string;
   _fieldNameElement?: ReactElement;
-  _fieldType: DataType;
+  _fieldType: DataTypeStringEnum;
   _dimension: string;
   _desc: string;
 }
@@ -78,3 +78,10 @@ export interface IndexExtraParam {
   metric_type: string;
   params: string;
 }
+
+export interface SizingInfoParam {
+  info: {
+    memory: string;
+    disk: string;
+  } | null;
+}

+ 0 - 78
client/src/pages/system/BaseCard.tsx

@@ -1,78 +0,0 @@
-
-import { FC } from 'react';
-import { makeStyles } from '@material-ui/core';
-import { SvgIcon } from '@material-ui/core';
-import { BaseCardProps } from './Types';
-import { ReactComponent } from '../../assets/imgs/pic.svg'
-
-const getStyles = makeStyles(() => ({
-  root: {
-    backgroundColor: 'white',
-    borderRadius: '8px',
-    boxShadow: '3px 3px 10px rgba(0, 0, 0, 0.05)',
-    boxSizing: 'border-box',
-    height: '150px',
-    padding: '16px',
-  },
-  title: {
-    color: '#82838E',
-    fontSize: '14px',
-    marginBottom: '5px',
-    textTransform: 'capitalize',
-  },
-  content: {
-    color: '#010E29',
-    fontSize: '20px',
-    fontWeight: 600,
-    lineHeight: '36px',
-  },
-  desc: {
-    color: '#82838E',
-    fontSize: '14px',
-    lineHeight: '36px',
-    marginLeft: "8px",
-  },
-  emptyRoot: {
-    alignItems: 'center',
-    display: 'flex',
-    flexDirection: 'column',
-    justifyContent: 'flex-start',
-
-    '& > svg': {
-      marginTop: '10px',
-      width: '100%',
-    }
-  },
-  emptyTitle: {
-    fontSize: '14px',
-    marginTop: '14px',
-    textTransform: 'capitalize',
-  },
-  emptyDesc: {
-    fontSize: '10px',
-    color: '#82838E',
-    marginTop: '8px',
-  },
-}));
-
-const BaseCard: FC<BaseCardProps> = (props) => {
-  const classes = getStyles();
-  const { children, title, content, desc } = props;
-  return (
-    <div className={classes.root}>
-      <div className={classes.title}>{title}</div>
-      {content && <span className={classes.content}>{content}</span>}
-      {desc && <span className={classes.desc}>{desc}</span>}
-      {!content && !desc && (
-        <div className={classes.emptyRoot}>
-          <SvgIcon viewBox="0 0 101 26" component={ReactComponent} {...props} />
-          <span className={classes.emptyTitle}>no data available</span>
-          <span className={classes.emptyDesc}>There is no data to show you right now.</span>
-        </div>
-      )}
-      {children}
-    </div>
-  );
-};
-
-export default BaseCard;

+ 0 - 195
client/src/pages/system/DataCard.tsx

@@ -1,195 +0,0 @@
-
-import { FC } from 'react';
-import { useTranslation } from 'react-i18next';
-import { makeStyles } from '@material-ui/core';
-import Progress from './Progress';
-import { getByteString } from '../../utils/Format';
-import { DataProgressProps, DataSectionProps, DataCardProps } from './Types';
-
-const getStyles = makeStyles(() => ({
-  root: {
-    backgroundColor: '#F6F6F6',
-    borderTopRightRadius: '8px',
-    borderBottomRightRadius: '8px',
-    height: '100%',
-    padding: '20px 16px',
-    boxSizing: 'border-box',
-  },
-
-  title: {
-    display: 'flex',
-    justifyContent: 'space-between',
-  },
-
-  content: {
-    color: '#010E29',
-    fontSize: '20px',
-    fontWeight: 600,
-    lineHeight: '36px',
-  },
-
-  desc: {
-    color: '#82838E',
-    fontSize: '14px',
-    lineHeight: '36px',
-    marginLeft: "8px",
-  },
-
-  rootName: {
-    color: '#82838E',
-    fontSize: '20px',
-    lineHeight: '24px',
-  },
-
-  childName: {
-    color: '#06AFF2',
-    fontSize: '20px',
-    lineHeight: '24px',
-  },
-
-  ip: {
-    color: '#010E29',
-    fontSize: '16px',
-    lineHeight: '24px',
-  },
-
-  sectionRoot: {
-    borderSpacing: '0 1px',
-    display: 'table',
-    marginTop: '24px',
-    width: '100%'
-  },
-
-  sectionRow: {
-    display: 'table-row',
-  },
-
-  sectionHeaderCell: {
-    display: 'table-cell',
-    color: '#82838E',
-    fontSize: '12px',
-    lineHeight: '24px',
-    padding: '8px 16px',
-    textTransform: 'uppercase',
-    width: '50%',
-  },
-
-  sectionCell: {
-    backgroundColor: 'white',
-    color: '#010E29',
-    display: 'table-cell',
-    fontSize: '14px',
-    lineHeight: '24px',
-    padding: '12px 16px',
-    textTransform: 'capitalize',
-    verticalAlign: 'middle',
-    width: '50%',
-  },
-  progressTitle: {
-    fontSize: '14px',
-    color: '#010E29',
-    lineHeight: '24px',
-    display: 'flex',
-    justifyContent: 'space-between',
-  }
-}));
-
-const DataSection: FC<DataSectionProps> = (props) => {
-  const classes = getStyles();
-  const { titles, contents } = props;
-
-  return (
-    <div className={classes.sectionRoot}>
-      <div className={classes.sectionRow}>
-        {titles.map((titleEntry) => <div key={titleEntry} className={classes.sectionHeaderCell}>{titleEntry}</div>)}
-      </div>
-      {contents.map((contentEntry) => {
-        return (
-          <div key={contentEntry.label} className={classes.sectionRow}>
-            <div className={classes.sectionCell}>
-              {contentEntry.label}
-            </div>
-            <div className={classes.sectionCell}>
-              {contentEntry.value}
-            </div>
-          </div>)
-      })}
-    </div>
-  );
-}
-
-const DataProgress: FC<DataProgressProps> = ({ percent = 0, desc = '' }) => {
-  const classes = getStyles();
-  return (
-    <div>
-      <div className={classes.progressTitle}>
-        <span>{`${Number(percent * 100).toFixed(2)}%`}</span>
-        <span>{desc}</span>
-      </div>
-      <Progress percent={percent} color='#06AFF2' />
-    </div>
-  )
-};
-
-const DataCard: FC<DataCardProps & React.HTMLAttributes<HTMLDivElement>> = (props) => {
-  const classes = getStyles();
-  const { t } = useTranslation('systemView');
-  const { t: commonTrans } = useTranslation();
-  const capacityTrans: { [key in string]: string } = commonTrans('capacity');
-  const { node, extend } = props;
-  const hardwareTitle = [t('hardwareTitle'), t('valueTitle')];
-  const hardwareContent = [];
-  const infos = node?.infos?.hardware_infos || {};
-
-  const {
-    cpu_core_count: cpu = 0,
-    cpu_core_usage: cpuUsage = 0,
-    memory = 1,
-    memory_usage: memoryUsage = 0,
-    disk = 1,
-    disk_usage: diskUsage = 0,
-  } = infos;
-
-  if (extend) {
-    hardwareContent.push({ label: t('thCPUCount'), value: cpu });
-    hardwareContent.push({
-      label: t('thCPUUsage'), value: <DataProgress percent={cpuUsage / 100} />
-    });
-    hardwareContent.push({
-      label: t('thMemUsage'), value: <DataProgress percent={memoryUsage / memory} desc={getByteString(memoryUsage, memory, capacityTrans)} />
-    });
-    hardwareContent.push({
-      label: t('thDiskUsage'), value: <DataProgress percent={diskUsage / disk} desc={getByteString(diskUsage, disk, capacityTrans)} />
-    });
-  }
-
-  const systemTitle = [t('systemTitle'), t('valueTitle')];
-  const systemContent = [];
-  const sysInfos = node?.infos?.system_info || {};
-  const {
-    system_version: version,
-    deploy_mode: mode = '',
-    created_time: create = '',
-    updated_time: update = '',
-  } = sysInfos;
-  systemContent.push({ label: t('thVersion'), value: version });
-  systemContent.push({ label: t('thDeployMode'), value: mode });
-  systemContent.push({ label: t('thCreateTime'), value: create });
-  systemContent.push({ label: t('thUpdateTime'), value: update });
-
-  return (
-    <div className={classes.root}>
-      <div className={classes.title}>
-        <div>
-          <span className={classes.rootName}>Milvus / </span>
-          <span className={classes.childName}>{node?.infos?.name}</span>
-        </div>
-        <div className={classes.ip}>{`${t('thIP')}:${infos?.ip || ''}`}</div>
-      </div>
-      {extend && <DataSection titles={hardwareTitle} contents={hardwareContent} />}
-      <DataSection titles={systemTitle} contents={systemContent} />
-    </div>
-  );
-};
-
-export default DataCard;

+ 0 - 125
client/src/pages/system/LineChartCard.tsx

@@ -1,125 +0,0 @@
-
-import { FC, useState, useEffect, useRef } from 'react';
-import { makeStyles } from '@material-ui/core';
-import BaseCard from './BaseCard';
-import { LineChartCardProps, LinceChartNode } from './Types';
-
-const getStyles = makeStyles(() => ({
-  root: {
-    transform: 'scaleY(-1)',
-    maxWidth: '90%',
-  },
-  ycoord: {
-    cursor: 'pointer',
-    "&:hover, &:focus": {
-      "& line": {
-        transition: 'all .25s',
-        opacity: 1,
-      },
-    },
-
-    "&:hover": {
-      "& circle": {
-        fill: '#06AFF2',
-      },
-    },
-
-    "&:focus": {
-      outline: 'none',
-
-      "& circle": {
-        fill: '#06F3AF',
-      },
-    },
-  }
-}));
-
-const LineChartCard: FC<LineChartCardProps> = (props) => {
-
-  const FULL_HEIGHT = 60;
-  const FULL_WIDTH = 300;
-  const ROUND = 5;
-  const STEP = 25;
-
-  const classes = getStyles();
-  const { title, value } = props;
-  const [displayNodes, setDisplayNodes] = useState<LinceChartNode[]>([]);
-  const [currentNode, setCurrentNode] = useState<LinceChartNode>({
-    percent: 0,
-    value: 0,
-    timestamp: Date.now(),
-  });
-
-  const max = useRef(1);
-  const isHover = useRef(false);
-  const nodes = useRef<LinceChartNode[]>([]);
-
-  useEffect(() => {
-    // show at most 10 nodes. so remove the earliest node when nodes exceed 10
-    if (nodes.current.length > 9) {
-      nodes.current.shift();
-    }
-
-    if (value && max.current) {
-      // calculate the y-axis max scale
-      let currentMax = max.current;
-      if (value > max.current) {
-        const pow = Math.ceil(Math.log10(value));
-        currentMax = Math.pow(10, pow);
-        max.current = currentMax;
-      }
-
-      // generate a new node and save in ref
-      if (nodes.current) {
-        const newNodes = nodes.current.slice(0);
-        const newNode = {
-          percent: value / currentMax * 100,
-          value,
-          timestamp: Date.now(),
-        }
-        newNodes.push(newNode);
-        nodes.current = newNodes;
-
-        // refresh nodes for display when mouse is not hovering on the chart
-        if (!isHover.current) {
-          setDisplayNodes(newNodes);
-          setCurrentNode(newNode);
-        }
-      }
-    }
-  }, [value]);
-
-  return (
-    nodes.current.length ? (
-      <BaseCard title={title} content={`${Math.round(currentNode.value)}ms`} desc={new Date(currentNode.timestamp).toLocaleString()}>
-        <svg className={classes.root} onMouseEnter={() => isHover.current = true} onMouseLeave={() => isHover.current = false} width={FULL_WIDTH} height={FULL_HEIGHT} viewBox={`0 5 ${FULL_WIDTH} ${FULL_HEIGHT}`} fill="white" xmlns="http://www.w3.org/2000/svg">
-          {
-            displayNodes.map((node, index) => {
-              const x1 = FULL_WIDTH - (displayNodes.length - index + 1) * STEP;
-              const y1 = node.percent * .5 + ROUND * 2;
-
-              let line = null;
-              if (index < displayNodes.length - 1) {
-                const x2 = FULL_WIDTH - (displayNodes.length - index) * STEP;
-                const y2 = displayNodes[index + 1]['percent'] * .5 + ROUND * 2;
-                line = <line x1={x1} y1={y1} x2={x2} y2={y2} stroke="#06AFF2" />;
-              }
-              return (
-                <g key={`${node.value}${index}`}>
-                  {line}
-                  <g className={classes.ycoord} onMouseOver={() => { setCurrentNode(node) }}>
-                    <circle cx={x1} cy={y1} r={ROUND} fill="white" stroke="#06AFF2" />
-                    <rect opacity="0" x={x1 - ROUND} y={0} width={ROUND * 2} height={FULL_HEIGHT} fill="#E9E9ED" />
-                    <line opacity="0" x1={x1} y1={0} x2={x1} y2={FULL_WIDTH} strokeWidth="2" stroke="#06AFF2" strokeDasharray="2.5" />
-                  </g>
-                </g>
-              )
-            })
-          }
-        </svg>
-      </BaseCard >
-    ) : <BaseCard title={title} />
-  );
-};
-
-export default LineChartCard;

+ 0 - 86
client/src/pages/system/MiniTopology.tsx

@@ -1,86 +0,0 @@
-import { FC } from 'react';
-import { makeStyles, Theme } from '@material-ui/core';
-import { MiniTopoProps } from './Types';
-
-const getStyles = makeStyles((theme: Theme) => ({
-  container: {
-    height: '100%',
-    width: 'auto',
-  },
-  childNode: {
-    transition: 'all .25s',
-    cursor: 'pointer',
-    transformOrigin: '50% 50%',
-    transformBox: 'fill-box',
-
-    '& circle': {
-      transition: 'all .25s',
-    },
-
-    '& text': {
-      transition: 'all .25s',
-    },
-
-    '&:hover, &:focus': {
-      transform: 'scale(1.1)',
-      filter: 'drop-shadow(3px 3px 5px rgba(0, 0, 0, .2))',
-    },
-
-    '&:focus': {
-      outline: 'none',
-
-      '& svg path': {
-        fill: 'white',
-      },
-
-      '& circle': {
-        fill: '#06AFF2',
-        stroke: '#06AFF2',
-      },
-
-      '& text': {
-        fill: 'white',
-      }
-    }
-  },
-}));
-
-const capitalize = (s: string) => {
-  return s.charAt(0).toUpperCase() + s.slice(1);
-}
-
-const MiniTopo: FC<MiniTopoProps> = (props) => {
-  const classes = getStyles();
-  const { selectedCord, selectedChildNode, setCord } = props;
-
-  const WIDTH = 400;                // width for svg
-  const HEIGHT = 400;               // height for svg
-  const LINE = 80;                // line lenght from lv2 node
-  const ANGLE = 10;                // angle offset for lv2 node
-  const R1 = 45;                    // root node radius
-  const R2 = 30;                    // lv1 node radius
-  const W3 = 20;                    // width of child rect
-
-  const childNodeCenterX = WIDTH / 2 + LINE * Math.cos(ANGLE * Math.PI / 180);
-  const childNodeCenterY = HEIGHT / 2 + LINE * Math.sin(ANGLE * Math.PI / 180);
-
-  return (
-    <svg className={classes.container} width={WIDTH} height={HEIGHT} viewBox={`0 0 ${WIDTH} ${HEIGHT}`} xmlns="http://www.w3.org/2000/svg">
-      <rect width="100%" height="100%" fill="white" />
-      <line x1={`${WIDTH / 3}`} y1={`${HEIGHT / 3}`} x2={childNodeCenterX} y2={childNodeCenterY} stroke="#06AFF2" />
-      <g className={classes.childNode} onClick={() => { setCord(null) }}>
-        <circle cx={`${WIDTH / 3}`} cy={`${HEIGHT / 3}`} r={R1} fill="white" stroke="#06AFF2" />
-        <text fontFamily="Roboto" alignmentBaseline="middle" textAnchor="middle" fill="#06AFF2" fontWeight="700" fontSize="12" x={`${WIDTH / 3}`} y={`${HEIGHT / 3}`}>{selectedCord ? capitalize(selectedCord.infos?.name) : ''}</text>
-      </g>
-      <g>
-        <svg width="60" height="60" viewBox="0 0 60 60" x={childNodeCenterX - 30} y={childNodeCenterY - 30}>
-          <circle cx={R2} cy={R2} r={R2} fill="#06AFF2" stroke="white" />
-          <rect className="selected" x={R2 - W3 / 2} y={R2 - W3 / 2} width={W3} height={W3} fill="white" />
-        </svg>
-        <text fontFamily="Roboto" textAnchor="middle" fill="#82838E" fontSize="12" x={childNodeCenterX} y={childNodeCenterY + 50}>{`${selectedChildNode ? selectedChildNode.infos?.name : ''}`}</text>
-      </g>
-    </svg >
-  );
-};
-
-export default MiniTopo;

+ 0 - 151
client/src/pages/system/NodeListView.tsx

@@ -1,151 +0,0 @@
-import { FC, useState, useEffect } from 'react';
-import { useTranslation } from 'react-i18next';
-import { makeStyles, Theme } from '@material-ui/core';
-import KeyboardArrowDown from '@material-ui/icons/KeyboardArrowDown';
-import { DataGrid } from '@mui/x-data-grid';
-import { useNavigationHook } from '../../hooks/Navigation';
-import { ALL_ROUTER_TYPES } from '../../router/Types';
-import MiniTopo from './MiniTopology';
-import { getByteString } from '../../utils/Format';
-import DataCard from './DataCard';
-import { NodeListViewProps, Node } from './Types';
-
-const getStyles = makeStyles((theme: Theme) => ({
-  root: {
-    fontFamily: 'Roboto',
-    margin: '14px 40px',
-    display: 'grid',
-    gridTemplateColumns: 'auto 400px',
-    gridTemplateRows: '40px 400px auto',
-    gridTemplateAreas:
-      `"a a"
-       "b ."
-       "b d"`,
-    height: 'calc(100% - 28px)',
-  },
-  cardContainer: {
-    display: 'grid',
-    gap: '16px',
-    gridTemplateColumns: 'repeat(4, minmax(300px, 1fr))',
-  },
-  contentContainer: {
-    borderRadius: '8px',
-    boxShadow: '3px 3px 10px rgba(0, 0, 0, 0.05)',
-    display: 'grid',
-    marginTop: '14px',
-  },
-  childView: {
-    height: '100%',
-    width: '100%',
-    transition: 'all .25s',
-    position: 'absolute',
-    zIndex: 1000,
-    backgroundColor: 'white',
-  },
-  childCloseBtn: {
-    border: 0,
-    backgroundColor: 'white',
-    gridArea: 'a',
-    cursor: 'pointer',
-    width: '100%',
-  },
-  gridContainer: {
-    gridArea: 'b',
-    display: 'flex',
-  },
-  dataCard: {
-    gridArea: 'd',
-  }
-}));
-
-const NodeListView: FC<NodeListViewProps> = (props) => {
-  useNavigationHook(ALL_ROUTER_TYPES.SYSTEM);
-  const { t } = useTranslation('systemView');
-  const { t: commonTrans } = useTranslation();
-  const capacityTrans: { [key in string]: string } = commonTrans('capacity');
-
-  const classes = getStyles();
-  const [selectedChildNode, setSelectedChildNode] = useState<Node | undefined>();
-  const [rows, setRows] = useState<any[]>([]);
-  const { selectedCord, childNodes, setCord } = props;
-
-  let columns: any[] = [
-    {
-      field: 'name',
-      headerName: t('thName'),
-      flex: 1,
-    },
-    {
-      field: 'ip',
-      headerName: t('thIP'),
-      flex: 1,
-    },
-    {
-      field: 'cpuCore',
-      headerName: t('thCPUCount'),
-      flex: 1,
-    },
-    {
-      field: 'cpuUsage',
-      headerName: t('thCPUUsage'),
-      flex: 1,
-    },
-    {
-      field: 'diskUsage',
-      headerName: t('thDiskUsage'),
-      flex: 1,
-    },
-    {
-      field: 'memUsage',
-      headerName: t('thMemUsage'),
-      flex: 1,
-    },
-  ];
-
-  useEffect(() => {
-    if (selectedCord) {
-      const connectedIds = selectedCord.connected.map(node => node.connected_identifier);
-      const newRows: any[] = [];
-      childNodes.forEach(node => {
-        if (connectedIds.includes(node.identifier)) {
-          const dataRow = {
-            id: node?.identifier,
-            ip: node?.infos?.hardware_infos.ip,
-            cpuCore: node?.infos?.hardware_infos.cpu_core_count,
-            cpuUsage: node?.infos?.hardware_infos.cpu_core_usage,
-            diskUsage: getByteString(node?.infos?.hardware_infos.disk_usage, node?.infos?.hardware_infos.disk, capacityTrans),
-            memUsage: getByteString(node?.infos?.hardware_infos.memory_usage, node?.infos?.hardware_infos.memory, capacityTrans),
-            name: node?.infos?.name,
-          }
-          newRows.push(dataRow);
-        }
-      })
-      setRows(newRows);
-    }
-  }, [selectedCord, childNodes, capacityTrans]);
-
-  return (
-    <div className={classes.root}>
-      <button className={classes.childCloseBtn} onClick={() => setCord(null)}>
-        <KeyboardArrowDown />
-      </button>
-      <div className={classes.gridContainer}>
-        <DataGrid
-          rows={rows}
-          columns={columns}
-          hideFooterPagination
-          hideFooterSelectedRowCount
-          onRowClick={(rowData) => {
-            const selectedNode = childNodes.find(node => rowData.row.id === node.identifier);
-            setSelectedChildNode(selectedNode);
-          }}
-        />
-      </div>
-      <MiniTopo selectedCord={selectedCord} setCord={setCord} selectedChildNode={selectedChildNode} />
-      <DataCard className={classes.dataCard} node={selectedChildNode} />
-    </div>
-
-  );
-};
-
-export default NodeListView;

+ 0 - 30
client/src/pages/system/Progress.tsx

@@ -1,30 +0,0 @@
-
-import { FC } from 'react';
-import { makeStyles } from '@material-ui/core';
-import { ProgressProps } from './Types';
-
-const getStyles = makeStyles(() => ({
-  root: {
-    height: 'auto',
-    transform: 'scaleY(-1)',
-    width: '100%',
-
-    "& line": {
-      transformOrigin: '10px 15px',
-    },
-  },
-}));
-
-const Progress: FC<ProgressProps> = (props) => {
-  const classes = getStyles();
-  const { percent = 0, color = '#06F3AF' } = props;
-
-  return (
-    <svg className={classes.root} width="300" height="30" viewBox="0 0 300 30" fill="none" xmlns="http://www.w3.org/2000/svg">
-      <line x1={10} y1={15} x2={290} y2={15} vectorEffect="non-scaling-stroke" strokeWidth="12" stroke="#AEAEBB" strokeLinecap="round" />
-      <line x1={10} y1={15} x2={290} y2={15} vectorEffect="non-scaling-stroke" transform={`scale(${percent}, 1)`} strokeWidth="12" stroke={color} strokeLinecap="round" />
-    </svg >
-  );
-};
-
-export default Progress;

+ 0 - 28
client/src/pages/system/ProgressCard.tsx

@@ -1,28 +0,0 @@
-
-import { FC } from 'react';
-import { useTranslation } from 'react-i18next';
-import BaseCard from './BaseCard';
-import Progress from './Progress';
-import { getByteString } from '../../utils/Format';
-import { ProgressCardProps } from './Types';
-
-const color1 = '#06F3AF';
-const color2 = '#635DCE';
-
-const ProgressCard: FC<ProgressCardProps> = (props) => {
-  const { title, total, usage } = props;
-  const { t } = useTranslation('systemView');
-  const { t: commonTrans } = useTranslation();
-  const capacityTrans: { [key in string]: string } = commonTrans('capacity');
-
-  const color = title === t('diskTitle') ? color1 : color2;
-  const percent = (usage && total) ? (usage / total) : 0;
-
-  return (
-    <BaseCard title={title} content={`${getByteString(usage, total, capacityTrans)} (${Math.floor(percent * 100)}%)`}>
-      <Progress percent={percent} color={color} />
-    </BaseCard>
-  );
-};
-
-export default ProgressCard;

+ 0 - 154
client/src/pages/system/SystemView.tsx

@@ -1,154 +0,0 @@
-import { useState, useEffect, useRef } from 'react';
-import { useTranslation } from 'react-i18next';
-import { makeStyles, Theme } from '@material-ui/core';
-import clsx from 'clsx';
-import { useNavigationHook } from '../../hooks/Navigation';
-import { ALL_ROUTER_TYPES } from '../../router/Types';
-import { MilvusHttp } from '../../http/Milvus';
-import { useInterval } from '../../hooks/SystemView';
-import Topo from './Topology';
-import NodeListView from './NodeListView';
-import LineChartCard from './LineChartCard';
-import ProgressCard from './ProgressCard';
-import DataCard from './DataCard';
-
-const getStyles = makeStyles((theme: Theme) => ({
-  root: {
-    fontFamily: 'Roboto',
-    margin: '14px 40px',
-    position: 'relative',
-    height: 'calc(100vh - 80px)',
-    display: 'flex',
-    flexDirection: 'column',
-  },
-  cardContainer: {
-    display: 'grid',
-    gap: '16px',
-    gridTemplateColumns: 'repeat(4, minmax(300px, 1fr))',
-  },
-  transparent: {
-    opacity: 0,
-    transition: 'opacity .5s',
-  },
-  contentContainer: {
-    borderRadius: '8px',
-    boxShadow: '3px 3px 10px rgba(0, 0, 0, 0.05)',
-    display: 'grid',
-    gridTemplateColumns: '1fr auto',
-    marginTop: '14px',
-    height: '100%',
-  },
-  childView: {
-    height: '100%',
-    width: '100%',
-    transition: 'all .25s',
-    position: 'absolute',
-    zIndex: 1000,
-    backgroundColor: 'white',
-  },
-  showChildView: {
-    top: 0,
-    maxHeight: 'auto',
-  },
-  hideChildView: {
-    top: '1000px',
-    maxHeight: 0,
-  },
-  childCloseBtn: {
-    border: 0,
-    backgroundColor: 'white',
-    width: '100%',
-  }
-}));
-
-
-const parseJson = (jsonData: any) => {
-  const nodes: any[] = [];
-  const childNodes: any[] = [];
-
-  const system = {
-    // qps: Math.random() * 1000,
-    letency: Math.random() * 1000,
-    disk: 0,
-    diskUsage: 0,
-    memory: 0,
-    memoryUsage: 0,
-  }
-
-  jsonData?.response?.nodes_info.forEach((node: any) => {
-    const type = node?.infos?.type;
-    // coordinator node
-    if (type.includes("Coord")) {
-      nodes.push(node);
-      // other nodes
-    } else {
-      childNodes.push(node);
-    }
-
-    const info = node.infos.hardware_infos;
-    system.memory += info.memory;
-    system.memoryUsage += info.memory_usage;
-    system.disk += info.disk;
-    system.diskUsage += info.disk_usage;
-  });
-  return { nodes, childNodes, system };
-}
-
-
-const SystemView: any = () => {
-  useNavigationHook(ALL_ROUTER_TYPES.SYSTEM);
-  const { t } = useTranslation('systemView');
-
-  const classes = getStyles();
-  const INTERVAL = 10000;
-
-  const [data, setData] = useState<{ nodes: any, childNodes: any, system: any }>({ nodes: [], childNodes: [], system: {} });
-  const [selectedNode, setNode] = useState<any>();
-  const [selectedCord, setCord] = useState<any>();
-  const { nodes, childNodes, system } = data;
-
-  useInterval(async () => {
-    if (!selectedCord) {
-      const res = await MilvusHttp.getMetrics();
-      setData(parseJson(res));
-    }
-  }, INTERVAL);
-
-  useEffect(() => {
-    async function fetchData() {
-      const res = await MilvusHttp.getMetrics();
-      setData(parseJson(res));
-    }
-    fetchData();
-  }, []);
-
-  let qps = system?.qps || 0;
-  const letency = system?.letency || 0;
-  const childView = useRef<HTMLInputElement>(null);
-
-  return (
-    <div className={classes.root}>
-      <div className={clsx(classes.cardContainer, selectedCord && classes.transparent)}>
-        <ProgressCard title={t('diskTitle')} usage={system.diskUsage} total={system.disk} />
-        <ProgressCard title={t('memoryTitle')} usage={system.memoryUsage} total={system.memory} />
-        <LineChartCard title={t('qpsTitle')} value={qps} />
-        <LineChartCard title={t('letencyTitle')} value={letency} />
-      </div>
-      <div className={classes.contentContainer}>
-        <Topo nodes={nodes} setNode={setNode} setCord={setCord} />
-        <DataCard node={selectedNode} extend />
-      </div>
-
-      <div
-        ref={childView}
-        className={clsx(classes.childView,
-          selectedCord ? classes.showChildView : classes.hideChildView)}
-      >
-        {selectedCord && (<NodeListView selectedCord={selectedCord} childNodes={childNodes} setCord={setCord} />)}
-      </div>
-    </div >
-
-  );
-};
-
-export default SystemView;

File diff suppressed because it is too large
+ 0 - 178
client/src/pages/system/Topology.tsx


+ 0 - 71
client/src/pages/system/Types.ts

@@ -1,71 +0,0 @@
-import { ReactNode } from "react";
-
-export interface Node {
-  infos: {
-    hardware_infos: any,
-    system_info: any,
-    name: string,
-  },
-  connected: {
-    connected_identifier: number,
-  }[],
-  identifier: number,
-}
-
-export interface ProgressProps {
-  percent: number,
-  color: string,
-}
-
-export interface ProgressCardProps {
-  title: string,
-  total: number,
-  usage: number,
-}
-
-export interface BaseCardProps {
-  children?: ReactNode,
-  title: string,
-  content?: string,
-  desc?: string,
-}
-
-export interface LineChartCardProps {
-  title: string,
-  value: number,
-}
-
-export interface DataProgressProps {
-  percent: number,
-  desc?: string,
-}
-
-export interface DataSectionProps {
-  titles: string[],
-  contents: { label: string, value: string }[],
-}
-
-export interface DataCardProps {
-  node?: Node,
-  extend?: boolean
-}
-
-export interface LinceChartNode {
-  percent: number,
-  value: number,
-  timestamp: number,
-}
-
-type SetCord = (arg1: Node | null) => void;
-
-export interface MiniTopoProps {
-  selectedCord: Node,
-  selectedChildNode: Node | undefined,
-  setCord: SetCord,
-}
-
-export interface NodeListViewProps {
-  selectedCord: Node,
-  childNodes: Node[],
-  setCord: SetCord,
-}

+ 0 - 0
client/src/pages/seach/Constants.ts → client/src/plugins/search/Constants.ts


+ 0 - 0
client/src/pages/seach/SearchParams.tsx → client/src/plugins/search/SearchParams.tsx


+ 0 - 0
client/src/pages/seach/Styles.ts → client/src/plugins/search/Styles.ts


+ 8 - 4
client/src/pages/seach/Types.ts → client/src/plugins/search/Types.ts

@@ -1,7 +1,10 @@
 import { Option } from '../../components/customSelector/Types';
 import { searchKeywordsType } from '../../consts/Milvus';
-import { DataType, DataTypeEnum } from '../collections/Types';
-import { IndexView } from '../schema/Types';
+import {
+  DataTypeEnum,
+  DataTypeStringEnum,
+} from 'insight_src/pages/collections/Types';
+import { IndexView } from 'insight_src/pages/schema/Types';
 
 export interface SearchParamsProps {
   // if user created index, pass metric type choosed when creating
@@ -30,7 +33,7 @@ export interface SearchResultView {
 }
 
 export interface FieldOption extends Option {
-  fieldType: DataType;
+  fieldType: DataTypeStringEnum;
   // used to get metric type, index type and index params for search params
   // if user doesn't create index, default value is null
   indexInfo: IndexView | null;
@@ -60,7 +63,8 @@ export interface VectorSearchParam {
   };
   vectors: any;
   output_fields: string[];
-  vector_type: number | DataTypeEnum;
+  vector_type: DataTypeEnum;
+  travel_timestamp?: string;
 }
 
 export interface SearchResult {

+ 19 - 1
client/src/pages/seach/VectorSearch.tsx → client/src/plugins/search/VectorSearch.tsx

@@ -16,7 +16,10 @@ 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 '../collections/Types';
+import {
+  CollectionData,
+  DataTypeEnum,
+} from 'insight_src/pages/collections/Types';
 import { IndexHttp } from '../../http/Index';
 import { getVectorSearchStyles } from './Styles';
 import { parseValue } from '../../utils/Insert';
@@ -33,6 +36,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';
 
 const VectorSearch = () => {
   useNavigationHook(ALL_ROUTER_TYPES.SEARCH);
@@ -51,6 +56,7 @@ const VectorSearch = () => {
   // fields for advanced filter
   const [filterFields, setFilterFields] = useState<Field[]>([]);
   const [selectedField, setSelectedField] = useState<string>('');
+
   // search params form
   const [searchParam, setSearchParam] = useState<{ [key in string]: number }>(
     {}
@@ -78,6 +84,9 @@ const VectorSearch = () => {
     handleGridSort,
   } = usePaginationHook(searchResult || []);
 
+  const { timeTravel, setTimeTravel, timeTravelInfo, handleDateTimeChange } =
+    useTimeTravelHook();
+
   const collectionOptions: Option[] = useMemo(
     () =>
       collections.map(c => ({
@@ -282,7 +291,9 @@ const VectorSearch = () => {
     setSearchResult(null);
     setFilterFields([]);
     setExpression('');
+    setTimeTravel(null);
   };
+
   const handleSearch = async (topK: number, expr = expression) => {
     const searhParamPairs = {
       params: JSON.stringify(searchParam),
@@ -298,6 +309,7 @@ const VectorSearch = () => {
       search_params: searhParamPairs,
       vectors: [parseValue(vectors)],
       vector_type: fieldType,
+      travel_timestamp: timeTravelInfo.timestamp,
     };
 
     setTableLoading(true);
@@ -457,6 +469,12 @@ const VectorSearch = () => {
             filterDisabled={selectedField === '' || selectedCollection === ''}
             onSubmit={handleAdvancedFilterChange}
           />
+          <CustomDatePicker
+            label={timeTravelInfo.label}
+            onChange={handleDateTimeChange}
+            date={timeTravel}
+            setDate={setTimeTravel}
+          />
         </div>
         <div className="right">
           <CustomButton className="btn" onClick={handleReset}>

+ 13 - 0
client/src/plugins/search/config.json

@@ -0,0 +1,13 @@
+{
+  "name": "search",
+  "version": "0.1.0",
+  "client": {
+    "path": "search",
+    "entry": "VectorSearch.tsx",
+    "label": "Vector Search",
+    "iconName": "navSearch",
+    "auth": true,
+    "iconActiveClass": "activeSearchIcon",
+    "iconNormalClass": "normalSearchIcon"
+  }
+}

+ 29 - 9
client/src/plugins/system/DataCard.tsx

@@ -137,9 +137,23 @@ const DataCard: FC<DataCardProps & React.HTMLAttributes<HTMLDivElement>> = (prop
   const { t: commonTrans } = useTranslation();
   const capacityTrans: { [key in string]: string } = commonTrans('capacity');
   const { node, extend } = props;
+
   const hardwareTitle = [t('hardwareTitle'), t('valueTitle')];
   const hardwareContent = [];
-  const infos = node?.infos?.hardware_infos || {};
+
+  const configTitle = [t('configTitle'), t('valueTitle')];
+  const systemConfig: { label: string; value: any; }[] = [];
+
+  const systemTitle = [t('systemTitle'), t('valueTitle')];
+  const systemContent = [];
+
+  const {
+    created_time: createTime,
+    updated_time: updateTime,
+    system_info = {},
+    hardware_infos: infos = {},
+    system_configurations,
+  } = node?.infos || {};
 
   const {
     cpu_core_count: cpu = 0,
@@ -163,19 +177,20 @@ const DataCard: FC<DataCardProps & React.HTMLAttributes<HTMLDivElement>> = (prop
     });
   }
 
-  const systemTitle = [t('systemTitle'), t('valueTitle')];
-  const systemContent = [];
-  const sysInfos = node?.infos?.system_info || {};
+  if (system_configurations) {
+    Object.keys(system_configurations).forEach(key => {
+      systemConfig.push({ label: key, value: system_configurations[key] });
+    });
+  }
+
   const {
     system_version: version,
     deploy_mode: mode = '',
-    created_time: create = '',
-    updated_time: update = '',
-  } = sysInfos;
+  } = system_info;
   systemContent.push({ label: t('thVersion'), value: version });
   systemContent.push({ label: t('thDeployMode'), value: mode });
-  systemContent.push({ label: t('thCreateTime'), value: create });
-  systemContent.push({ label: t('thUpdateTime'), value: update });
+  systemContent.push({ label: t('thCreateTime'), value: createTime ? new Date(createTime.substr(0, 37)).toLocaleString() : '' });
+  systemContent.push({ label: t('thUpdateTime'), value: updateTime ? new Date(updateTime.substr(0, 37)).toLocaleString() : '' });
 
   return (
     <div className={classes.root}>
@@ -188,6 +203,11 @@ const DataCard: FC<DataCardProps & React.HTMLAttributes<HTMLDivElement>> = (prop
       </div>
       {extend && <DataSection titles={hardwareTitle} contents={hardwareContent} />}
       <DataSection titles={systemTitle} contents={systemContent} />
+      {systemConfig.length ?
+        <DataSection titles={configTitle} contents={systemConfig} />
+        :
+        null
+      }
     </div>
   );
 };

+ 12 - 2
client/src/plugins/system/NodeListView.tsx

@@ -21,7 +21,6 @@ const getStyles = makeStyles((theme: Theme) => ({
       `"a a"
        "b ."
        "b d"`,
-    height: 'calc(100% - 28px)',
   },
   cardContainer: {
     display: 'grid',
@@ -69,7 +68,7 @@ const NodeListView: FC<NodeListViewProps> = (props) => {
   const [rows, setRows] = useState<any[]>([]);
   const { selectedCord, childNodes, setCord } = props;
 
-  let columns: any[] = [
+  const columns: any[] = [
     {
       field: 'name',
       headerName: t('thName'),
@@ -124,6 +123,17 @@ const NodeListView: FC<NodeListViewProps> = (props) => {
     }
   }, [selectedCord, childNodes, capacityTrans]);
 
+  // select first node
+  useEffect(() => {
+    const timeoutID = window.setTimeout(() => {
+      const el = document.querySelectorAll<HTMLElement>(".MuiDataGrid-row")[0];
+      if (el instanceof HTMLElement) {
+        el.click();
+      }
+    }, 300);
+    return () => window.clearTimeout(timeoutID);
+  }, [childNodes]);
+
   return (
     <div className={classes.root}>
       <button className={classes.childCloseBtn} onClick={() => setCord(null)}>

+ 9 - 7
client/src/plugins/system/SystemView.tsx

@@ -17,7 +17,7 @@ const getStyles = makeStyles((theme: Theme) => ({
     fontFamily: 'Roboto',
     margin: '14px 40px',
     position: 'relative',
-    height: 'calc(100vh - 80px)',
+    height: 'fit-content',
     display: 'flex',
     flexDirection: 'column',
   },
@@ -48,10 +48,11 @@ const getStyles = makeStyles((theme: Theme) => ({
   },
   showChildView: {
     top: 0,
-    maxHeight: 'auto',
+    minHeight: '100%',
+    height: 'fit-content',
   },
   hideChildView: {
-    top: '1000px',
+    top: '1500px',
     maxHeight: 0,
   },
   childCloseBtn: {
@@ -68,7 +69,7 @@ const parseJson = (jsonData: any) => {
 
   const system = {
     // qps: Math.random() * 1000,
-    letency: Math.random() * 1000,
+    latency: Math.random() * 1000,
     disk: 0,
     diskUsage: 0,
     memory: 0,
@@ -94,7 +95,6 @@ const parseJson = (jsonData: any) => {
   return { nodes, childNodes, system };
 }
 
-
 const SystemView: any = () => {
   useNavigationHook(ALL_ROUTER_TYPES.SYSTEM);
   const { t } = useTranslation('systemView');
@@ -123,16 +123,18 @@ const SystemView: any = () => {
   }, []);
 
   let qps = system?.qps || 0;
-  const letency = system?.letency || 0;
+  const latency = system?.latency || 0;
   const childView = useRef<HTMLInputElement>(null);
 
+
+
   return (
     <div className={classes.root}>
       <div className={clsx(classes.cardContainer, selectedCord && classes.transparent)}>
         <ProgressCard title={t('diskTitle')} usage={system.diskUsage} total={system.disk} />
         <ProgressCard title={t('memoryTitle')} usage={system.memoryUsage} total={system.memory} />
         <LineChartCard title={t('qpsTitle')} value={qps} />
-        <LineChartCard title={t('letencyTitle')} value={letency} />
+        <LineChartCard title={t('latencyTitle')} value={latency} />
       </div>
       <div className={classes.contentContainer}>
         <Topo nodes={nodes} setNode={setNode} setCord={setCord} />

+ 25 - 11
client/src/plugins/system/Topology.tsx

@@ -25,14 +25,13 @@ const getStyles = makeStyles((theme: Theme) => ({
       transition: 'all .25s',
     },
 
-    '&:hover, &:focus': {
+    '&:hover, &.selectedNode': {
       transform: 'scale(1.1)',
       filter: 'drop-shadow(3px 3px 5px rgba(0, 0, 0, .2))',
-    },
-
-    '&:focus': {
       outline: 'none',
+    },
 
+    '&.selectedNode': {
       '& circle': {
         fill: '#06AFF2',
         stroke: '#06AFF2',
@@ -57,14 +56,13 @@ const getStyles = makeStyles((theme: Theme) => ({
       transition: 'all .25s',
     },
 
-    '&:hover, &:focus': {
+    '&:hover, &.selectedNode': {
       transform: 'scale(1.1)',
       filter: 'drop-shadow(3px 3px 5px rgba(0, 0, 0, .2))',
-    },
-
-    '&:focus': {
       outline: 'none',
+    },
 
+    '&.selectedNode': {
       '& svg path': {
         fill: 'white',
       },
@@ -113,6 +111,20 @@ const capitalize = (s: string) => {
   return s.charAt(0).toUpperCase() + s.slice(1);
 }
 
+const setSelected = (el: any) => {
+  const nodes = document.querySelectorAll<HTMLElement>('.selectedNode');
+  nodes.forEach(n => n.classList.remove('selectedNode'));
+
+  function getGParent(e: any): any {
+    if (e.tagName === 'g') {
+      return e;
+    } else {
+      return getGParent(e.parentElement);
+    }
+  }
+  getGParent(el).classList.add('selectedNode');
+}
+
 const Topo = (props: any) => {
   const classes = getStyles();
   const { nodes, setNode, setCord } = props;
@@ -221,8 +233,9 @@ const Topo = (props: any) => {
               <g key={`${node?.infos?.name}`}>
                 <line x1={`${WIDTH / 2}`} y1={`${HEIGHT / 2}`} x2={nodeCenterX} y2={nodeCenterY} stroke="#06AFF2" />
                 {connectedLength && (<line x1={nodeCenterX} y1={nodeCenterY} x2={childNodeCenterX} y2={childNodeCenterY} stroke="#06AFF2" />)}
-                <g className={classes.childNode} tabIndex={0} onClick={() => {
+                <g className={classes.childNode} tabIndex={0} onClick={e => {
                   setNode(node);
+                  setSelected(e.target);
                 }}
                 >
                   <circle cx={nodeCenterX} cy={nodeCenterY} r={R2} fill="white" stroke="#06AFF2" />
@@ -255,9 +268,10 @@ const Topo = (props: any) => {
           }
           return null;
         })}
-        <g id="center" className={classes.rootNode} tabIndex={0} onClick={() => {
+        <g id="center" className={classes.rootNode} tabIndex={0} onClick={e => {
           setNode(centerNode);
-        }} >
+          setSelected(e.target);
+        }}>
           <circle cx={`${WIDTH / 2}`} cy={`${HEIGHT / 2}`} r={R1} fill="white" stroke="#06AFF2" />
           <text fontFamily="Roboto" textAnchor="middle" alignmentBaseline="middle" fill="#06AFF2" fontWeight="700" fontSize="24" x={`${WIDTH / 2}`} y={`${HEIGHT / 2}`}>Milvus</text>
         </g>

+ 3 - 0
client/src/plugins/system/Types.ts

@@ -5,6 +5,9 @@ export interface Node {
     hardware_infos: any,
     system_info: any,
     name: string,
+    created_time: string,
+    updated_time: string,
+    system_configurations: any,
   },
   connected: {
     connected_identifier: number,

+ 1 - 0
client/src/plugins/system/config.json

@@ -3,6 +3,7 @@
   "version": "0.1.0",
   "client": {
     "path": "system",
+    "auth": true,
     "entry": "SystemView.tsx",
     "label": "System View",
     "iconName": "navSystem"

+ 6 - 6
client/src/router/Config.ts

@@ -2,7 +2,7 @@ import Collection from '../pages/collections/Collection';
 import Collections from '../pages/collections/Collections';
 import Connect from '../pages/connect/Connect';
 import Overview from '../pages/overview/Overview';
-import VectorSearch from '../pages/seach/VectorSearch';
+// import VectorSearch from '../pages/seach/VectorSearch';
 import { RouterConfigType } from './Types';
 import loadable from '@loadable/component';
 
@@ -29,11 +29,11 @@ const RouterConfig: RouterConfigType[] = [
     component: Collection,
     auth: true,
   },
-  {
-    path: '/search',
-    component: VectorSearch,
-    auth: true,
-  },
+  // {
+  //   path: '/search',
+  //   component: VectorSearch,
+  //   auth: true,
+  // },
 ];
 
 function importAll(r: any, outOfRoot = false) {

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

@@ -8,7 +8,9 @@ export enum ALL_ROUTER_TYPES {
   // 'search'
   SEARCH = 'search',
   // 'system'
-  SYSTEM = 'system'
+  SYSTEM = 'system',
+  // plugins
+  PLUGIN = 'plugin',
 }
 
 export type NavInfo = {

+ 61 - 0
client/src/styles/theme.ts

@@ -2,7 +2,15 @@ import {
   // for strict mode
   unstable_createMuiStrictModeTheme as createMuiTheme,
 } from '@material-ui/core/styles';
+import { MuiPickersOverrides } from '@material-ui/pickers/typings/overrides';
 
+type overridesNameToClassKey = {
+  [P in keyof MuiPickersOverrides]: keyof MuiPickersOverrides[P];
+};
+
+declare module '@material-ui/core/styles/overrides' {
+  export interface ComponentNameToClassKey extends overridesNameToClassKey {}
+}
 declare module '@material-ui/core/styles/createPalette' {
   interface Palette {
     milvusBlue: Palette['primary'];
@@ -172,5 +180,58 @@ export const theme = createMuiTheme({
         },
       },
     },
+
+    // Date time picker theme overrides
+    MuiPickersToolbar: {
+      toolbar: {
+        '& .MuiTypography-h3': {
+          fontSize: '3rem',
+          lineHeight: 1.04,
+        },
+        '& .MuiTypography-h4': {
+          fontSize: '1.5rem',
+          lineHeight: 1.17,
+        },
+      },
+    },
+    MuiPickerDTTabs: {
+      tabs: {
+        backgroundColor: '#fff',
+        '& .MuiTabs-indicator': {
+          backgroundColor: commonThemes.palette.primary.main,
+        },
+      },
+    },
+    MuiPickersCalendarHeader: {
+      switchHeader: {
+        '& .MuiTypography-body1': {
+          fontSize: '0.85rem',
+        },
+      },
+      daysHeader: {
+        '& .MuiTypography-caption': {
+          fontSize: '0.85rem',
+        },
+      },
+    },
+
+    MuiPickersDay: {
+      day: {
+        '& .MuiTypography-body2': {
+          fontSize: '0.85rem',
+        },
+      },
+      daySelected: {
+        backgroundColor: commonThemes.palette.primary.main,
+        color: '#fff',
+      },
+      dayDisabled: {},
+      current: {},
+    },
+    MuiPickersModal: {
+      dialogAction: {
+        color: commonThemes.palette.primary.main,
+      },
+    },
   },
 });

+ 70 - 0
client/src/types/SearchTypes.ts

@@ -0,0 +1,70 @@
+import { Option } from '../components/customSelector/Types';
+import { searchKeywordsType } from '../consts/Milvus';
+import { DataTypeEnum, DataTypeStringEnum } from '../pages/collections/Types';
+import { IndexView } from '../pages/schema/Types';
+
+export interface SearchParamsProps {
+  // if user created index, pass metric type choosed when creating
+  // else pass empty string
+  metricType: string;
+  // used for getting metric type options
+  embeddingType: DataTypeEnum.FloatVector | DataTypeEnum.BinaryVector;
+  // default index type is FLAT
+  indexType: string;
+  // index extra params, e.g. nlist
+  indexParams: { key: string; value: string }[];
+  searchParamsForm: {
+    [key in string]: number;
+  };
+  topK: number;
+  handleFormChange: (form: { [key in string]: number }) => void;
+  wrapperClass?: string;
+  setParamsDisabled: (isDisabled: boolean) => void;
+}
+
+export interface SearchResultView {
+  // dynamic field names
+  [key: string]: string | number;
+  rank: number;
+  distance: number;
+}
+
+export interface FieldOption extends Option {
+  fieldType: DataTypeStringEnum;
+  // used to get metric type, index type and index params for search params
+  // if user doesn't create index, default value is null
+  indexInfo: IndexView | null;
+  // used for check vector input validation
+  dimension: number;
+}
+
+export interface SearchParamInputConfig {
+  label: string;
+  key: searchKeywordsType;
+  min: number;
+  max: number;
+  isInt?: boolean;
+  // no value: empty string
+  value: number | string;
+  handleChange: (value: number) => void;
+  className?: string;
+}
+
+export interface VectorSearchParam {
+  expr?: string;
+  search_params: {
+    anns_field: string; // your vector field name
+    topk: string | number;
+    metric_type: string;
+    params: string;
+  };
+  vectors: any;
+  output_fields: string[];
+  vector_type: DataTypeEnum;
+}
+
+export interface SearchResult {
+  // dynamic field names
+  [key: string]: string | number;
+  score: number;
+}

+ 2 - 2
client/src/utils/Form.ts

@@ -1,7 +1,7 @@
 import { Option } from '../components/customSelector/Types';
 import { METRIC_TYPES_VALUES } from '../consts/Milvus';
 import { IForm } from '../hooks/Form';
-import { DataType } from '../pages/collections/Types';
+import { DataTypeStringEnum } from '../pages/collections/Types';
 import { IndexType } from '../pages/schema/Types';
 
 interface IInfo {
@@ -22,7 +22,7 @@ export const formatForm = (info: IInfo): IForm[] => {
 
 export const getMetricOptions = (
   indexType: IndexType,
-  fieldType: DataType
+  fieldType: DataTypeStringEnum
 ): Option[] => {
   const baseFloatOptions = [
     {

+ 48 - 5
client/src/utils/Format.ts

@@ -144,7 +144,11 @@ export const getCreateFieldType = (config: Field): CreateFieldType => {
 export const formatAddress = (address: string): string => address.trim();
 
 // generate a sting like 20.22/98.33MB with proper unit
-export const getByteString = (value1: number, value2: number, capacityTrans: { [key in string]: string }) => {
+export const getByteString = (
+  value1: number,
+  value2: number,
+  capacityTrans: { [key in string]: string }
+) => {
   if (!value1 || !value2) return `0${capacityTrans.b}`;
   const power = Math.round(Math.log(value1) / Math.log(1024));
   let unit = '';
@@ -168,8 +172,47 @@ export const getByteString = (value1: number, value2: number, capacityTrans: { [
       unit = capacityTrans.b;
       break;
   }
-  const byteValue1 = value1 / (1024 ** power);
-  const byteValue2 = value2 / (1024 ** power);
+  const byteValue1 = value1 / 1024 ** power;
+  const byteValue2 = value2 / 1024 ** power;
 
-  return `${(byteValue1).toFixed(2)}/${(byteValue2).toFixed(2)} ${unit}`;
-}
+  return `${byteValue1.toFixed(2)}/${byteValue2.toFixed(2)} ${unit}`;
+};
+
+/**
+ * When number is larger than js max number, transform to string by BigInt.
+ * @param bigNumber
+ * @returns
+ */
+export const formatUtcToMilvus = (bigNumber: number) => {
+  const milvusTimeStamp = BigInt(bigNumber) << BigInt(18);
+  return milvusTimeStamp.toString();
+};
+
+/**
+ * Convert headers and rows to csv string.
+ * @param headers csv headers: string[]
+ * @param rows csv data rows: {[key in headers]: any}[]
+ * @returns csv string
+ */
+export const generateCsvData = (headers: string[], rows: any[]) => {
+  const rowsData = rows.reduce((prev, item: any[]) => {
+    headers.forEach((colName: any, idx: number) => {
+      const val = item[colName];
+      if (typeof val === 'string') {
+        prev += val;
+      } else if (typeof val === 'object') {
+        // Use double quote to ensure csv display correctly
+        prev += `"${JSON.stringify(val)}"`;
+      } else {
+        prev += `${val}`;
+      }
+      if (idx === headers.length - 1) {
+        prev += '\n';
+      } else {
+        prev += ',';
+      }
+    });
+    return prev;
+  }, '');
+  return headers.join(',') + '\n' + rowsData;
+};

+ 139 - 0
client/src/utils/SizingTool.ts

@@ -0,0 +1,139 @@
+import { INDEX_TYPES_ENUM } from '../pages/schema/Types';
+
+const commonValueCalculator = (
+  vector: number,
+  dimensions: number,
+  nlistArg: number,
+  fileSize: number
+) => {
+  const vectorCount = Math.min(fileSize / (dimensions * 4), vector);
+  const segmentCount = Math.round(vector / vectorCount);
+  const nlist = Math.min(nlistArg, vectorCount / 40);
+  return {
+    vectorCount,
+    segmentCount,
+    nlist,
+  };
+};
+
+const pqCalculator = (
+  vectorCount: number,
+  segmentCount: number,
+  dimensions: number,
+  m: number,
+  nlist: number
+) => {
+  const singleDiskSize =
+    nlist * dimensions * 4 + m * vectorCount + 256 * dimensions * 4;
+  const singleMemorySize = singleDiskSize + 256 * m * nlist * 4;
+  return {
+    pq_diskSize: singleDiskSize * segmentCount,
+    pq_memorySize: singleMemorySize * segmentCount,
+  };
+};
+
+export const computMilvusRecommonds = (
+  vector: number,
+  dimensions: number,
+  nlistArg: number,
+  m: number,
+  fileSize: number
+): { [key in string]: any } => {
+  const { vectorCount, segmentCount, nlist } = commonValueCalculator(
+    vector,
+    dimensions,
+    nlistArg,
+    fileSize
+  );
+
+  const { pq_diskSize, pq_memorySize } = pqCalculator(
+    vectorCount,
+    segmentCount,
+    dimensions,
+    m,
+    nlist
+  );
+
+  const size = vector * dimensions * 4;
+  const nlistSize = dimensions * 4 * nlist;
+  const byteSize = (dimensions / 8) * vector;
+
+  const rawFileSize = {
+    [INDEX_TYPES_ENUM.FLAT]: size,
+    [INDEX_TYPES_ENUM.IVF_FLAT]: size,
+    [INDEX_TYPES_ENUM.IVF_SQ8]: size,
+    [INDEX_TYPES_ENUM.IVF_SQ8_HYBRID]: size,
+    [INDEX_TYPES_ENUM.IVF_PQ]: size,
+  };
+
+  const memorySize = {
+    [INDEX_TYPES_ENUM.FLAT]: size,
+    [INDEX_TYPES_ENUM.IVF_FLAT]: size + nlistSize * segmentCount,
+    [INDEX_TYPES_ENUM.IVF_SQ8]: size * 0.25 + nlistSize * segmentCount,
+    [INDEX_TYPES_ENUM.IVF_SQ8_HYBRID]: size * 0.25 + nlistSize * segmentCount,
+    [INDEX_TYPES_ENUM.IVF_PQ]: pq_memorySize,
+  };
+
+  const diskSize = {
+    [INDEX_TYPES_ENUM.FLAT]: size,
+    [INDEX_TYPES_ENUM.IVF_FLAT]:
+      rawFileSize[INDEX_TYPES_ENUM.IVF_FLAT] +
+      memorySize[INDEX_TYPES_ENUM.IVF_FLAT],
+    [INDEX_TYPES_ENUM.IVF_SQ8]:
+      rawFileSize[INDEX_TYPES_ENUM.IVF_SQ8] +
+      memorySize[INDEX_TYPES_ENUM.IVF_SQ8],
+    [INDEX_TYPES_ENUM.IVF_SQ8_HYBRID]:
+      rawFileSize[INDEX_TYPES_ENUM.IVF_SQ8_HYBRID] +
+      memorySize[INDEX_TYPES_ENUM.IVF_SQ8_HYBRID],
+    [INDEX_TYPES_ENUM.IVF_PQ]:
+      rawFileSize[INDEX_TYPES_ENUM.IVF_PQ] + pq_diskSize,
+  };
+
+  const byteRawFileSize = {
+    [INDEX_TYPES_ENUM.BIN_FLAT]: byteSize,
+    [INDEX_TYPES_ENUM.BIN_IVF_FLAT]: byteSize,
+  };
+
+  const byteMemorySize = {
+    [INDEX_TYPES_ENUM.BIN_FLAT]: byteSize,
+    [INDEX_TYPES_ENUM.BIN_IVF_FLAT]: dimensions * nlist + byteSize,
+  };
+
+  const byteDiskSize = {
+    [INDEX_TYPES_ENUM.BIN_FLAT]: byteSize,
+    [INDEX_TYPES_ENUM.BIN_IVF_FLAT]:
+      byteRawFileSize[INDEX_TYPES_ENUM.BIN_IVF_FLAT] +
+      byteMemorySize[INDEX_TYPES_ENUM.BIN_IVF_FLAT],
+  };
+
+  return {
+    rawFileSize,
+    memorySize,
+    diskSize,
+    byteRawFileSize,
+    byteMemorySize,
+    byteDiskSize,
+  };
+};
+
+export const formatSize = (size: number) => {
+  let sizeStatus = 1;
+  let status = 'BYTE';
+  while (sizeStatus < 4 && size > 4096) {
+    size = size / 1024;
+    sizeStatus++;
+  }
+  sizeStatus === 2
+    ? (status = 'KB')
+    : sizeStatus === 3
+    ? (status = 'MB')
+    : sizeStatus === 4
+    ? (status = 'GB')
+    : sizeStatus === 5
+    ? (status = 'TB')
+    : (status = 'KB');
+
+  size = Math.ceil(size);
+
+  return `${size} ${status}`;
+};

+ 21 - 11
client/src/utils/search.ts

@@ -1,5 +1,5 @@
 import { Field } from '../components/advancedSearch/Types';
-import { DataType, DataTypeEnum } from '../pages/collections/Types';
+import { DataTypeEnum, DataTypeStringEnum } from '../pages/collections/Types';
 import {
   FieldData,
   IndexType,
@@ -10,7 +10,7 @@ import {
   FieldOption,
   SearchResult,
   SearchResultView,
-} from '../pages/seach/Types';
+} from '../types/SearchTypes';
 
 /**
  * Do not change  vector search result default sort  by ourself.
@@ -21,11 +21,19 @@ import {
 export const transferSearchResult = (
   result: SearchResult[]
 ): SearchResultView[] => {
-  const resultView = result.map((r, index) => ({
-    rank: index + 1,
-    ...r,
-    distance: r.score,
-  }));
+  const resultView = result.map((r, index) => {
+    const { rank, distance, ...others } = r;
+    const data: any = {
+      rank: index + 1,
+      distance: r.score,
+    };
+    // When value is boolean ,table will not render bool value.
+    // So we need to use toString() here.
+    Object.keys(others).forEach(v => {
+      data[v] = others[v].toString();
+    });
+    return data;
+  });
 
   return resultView;
 };
@@ -35,7 +43,7 @@ export const transferSearchResult = (
  * @param fieldType only vector type fields: 'BinaryVector' or 'FloatVector'
  */
 export const getEmbeddingType = (
-  fieldType: DataType
+  fieldType: DataTypeStringEnum
 ): DataTypeEnum.BinaryVector | DataTypeEnum.FloatVector => {
   const type =
     fieldType === 'BinaryVector'
@@ -64,7 +72,10 @@ export const getDefaultIndexType = (embeddingType: DataTypeEnum): IndexType => {
 export const classifyFields = (
   fields: FieldData[]
 ): { vectorFields: FieldData[]; nonVectorFields: FieldData[] } => {
-  const vectorTypes: DataType[] = ['BinaryVector', 'FloatVector'];
+  const vectorTypes: DataTypeStringEnum[] = [
+    DataTypeStringEnum.BinaryVector,
+    DataTypeStringEnum.FloatVector,
+  ];
   return fields.reduce(
     (result, cur) => {
       const changedFieldType = vectorTypes.includes(cur._fieldType)
@@ -101,9 +112,8 @@ export const getVectorFieldOptions = (
 };
 
 export const getNonVectorFieldsForFilter = (fields: FieldData[]): Field[] => {
-  const intTypes: DataType[] = ['Int8', 'Int16', 'Int32', 'Int64'];
   return fields.map(f => ({
     name: f._fieldName,
-    type: intTypes.includes(f._fieldType) ? 'int' : 'float',
+    type: f._fieldType,
   }));
 };

+ 2 - 1
client/tsconfig.paths.json

@@ -2,7 +2,8 @@
   "compilerOptions": {
     "baseUrl": ".",
     "paths": {
-      "all_plugins/*": ["src/plugins"]
+      "all_plugins/*": ["src/plugins"],
+      "insight_src/*": ["src/*"]
     }
   }
 }

+ 57 - 2
client/yarn.lock

@@ -1172,6 +1172,13 @@
   dependencies:
     regenerator-runtime "^0.13.4"
 
+"@babel/runtime@^7.6.0":
+  version "7.16.3"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.3.tgz#b86f0db02a04187a3c17caa77de69840165d42d5"
+  integrity sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==
+  dependencies:
+    regenerator-runtime "^0.13.4"
+
 "@babel/template@^7.10.4", "@babel/template@^7.16.0", "@babel/template@^7.3.3":
   version "7.16.0"
   resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.0.tgz#d16a35ebf4cd74e202083356fab21dd89363ddd6"
@@ -1227,6 +1234,18 @@
   resolved "https://registry.yarnpkg.com/@csstools/normalize.css/-/normalize.css-10.1.0.tgz#f0950bba18819512d42f7197e56c518aa491cf18"
   integrity sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg==
 
+"@date-io/core@1.x", "@date-io/core@^1.3.13":
+  version "1.3.13"
+  resolved "https://registry.yarnpkg.com/@date-io/core/-/core-1.3.13.tgz#90c71da493f20204b7a972929cc5c482d078b3fa"
+  integrity sha512-AlEKV7TxjeK+jxWVKcCFrfYAk8spX9aCyiToFIiLPtfQbsjmRGLIhb5VZgptQcJdHtLXo7+m0DuurwFgUToQuA==
+
+"@date-io/dayjs@1.x":
+  version "1.3.13"
+  resolved "https://registry.yarnpkg.com/@date-io/dayjs/-/dayjs-1.3.13.tgz#3a9edf5a7227b31b0f00a4f640f8715626833a61"
+  integrity sha512-nD39xWYwQjDMIdpUzHIcADHxY9m1hm1DpOaRn3bc2rBdgmwQC0PfW0WYaHyGGP/6LEzEguINRbHuotMhf+T9Sg==
+  dependencies:
+    "@date-io/core" "^1.3.13"
+
 "@emotion/hash@^0.8.0":
   version "0.8.0"
   resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413"
@@ -1541,6 +1560,18 @@
     prop-types "^15.7.2"
     react-is "^16.8.0 || ^17.0.0"
 
+"@material-ui/pickers@^3.3.10":
+  version "3.3.10"
+  resolved "https://registry.yarnpkg.com/@material-ui/pickers/-/pickers-3.3.10.tgz#f1b0f963348cc191645ef0bdeff7a67c6aa25485"
+  integrity sha512-hS4pxwn1ZGXVkmgD4tpFpaumUaAg2ZzbTrxltfC5yPw4BJV+mGkfnQOB4VpWEYZw2jv65Z0wLwDE/piQiPPZ3w==
+  dependencies:
+    "@babel/runtime" "^7.6.0"
+    "@date-io/core" "1.x"
+    "@types/styled-jsx" "^2.2.8"
+    clsx "^1.0.2"
+    react-transition-group "^4.0.0"
+    rifm "^0.7.0"
+
 "@material-ui/styles@^4.11.4":
   version "4.11.4"
   resolved "https://registry.yarnpkg.com/@material-ui/styles/-/styles-4.11.4.tgz#eb9dfccfcc2d208243d986457dff025497afa00d"
@@ -1931,6 +1962,11 @@
   resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
   integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==
 
+"@types/file-saver@^2.0.4":
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/@types/file-saver/-/file-saver-2.0.4.tgz#aaf9b96296150d737b2fefa535ced05ed8013d84"
+  integrity sha512-sPZYQEIF/SOnLAvaz9lTuydniP+afBMtElRTdYkeV1QtEgvtJ7qolCPjly6O32QI8CbEmP5O/fztMXEDWfEcrg==
+
 "@types/glob@^7.1.1":
   version "7.2.0"
   resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb"
@@ -2152,6 +2188,13 @@
   resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"
   integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==
 
+"@types/styled-jsx@^2.2.8":
+  version "2.2.9"
+  resolved "https://registry.yarnpkg.com/@types/styled-jsx/-/styled-jsx-2.2.9.tgz#e50b3f868c055bcbf9bc353eca6c10fdad32a53f"
+  integrity sha512-W/iTlIkGEyTBGTEvZCey8EgQlQ5l0DwMqi3iOXlLs2kyBwYTXHKEiU6IZ5EwoRwngL8/dGYuzezSup89ttVHLw==
+  dependencies:
+    "@types/react" "*"
+
 "@types/tapable@^1", "@types/tapable@^1.0.5":
   version "1.0.8"
   resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.8.tgz#b94a4391c85666c7b73299fd3ad79d4faa435310"
@@ -3644,7 +3687,7 @@ cliui@^6.0.0:
     strip-ansi "^6.0.0"
     wrap-ansi "^6.2.0"
 
-clsx@^1.0.4, clsx@^1.1.1:
+clsx@^1.0.2, clsx@^1.0.4, clsx@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188"
   integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==
@@ -5358,6 +5401,11 @@ file-loader@6.1.1:
     loader-utils "^2.0.0"
     schema-utils "^3.0.0"
 
+file-saver@^2.0.5:
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-2.0.5.tgz#d61cfe2ce059f414d899e9dd6d4107ee25670c38"
+  integrity sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==
+
 file-uri-to-path@1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
@@ -9901,7 +9949,7 @@ react-syntax-highlighter@^15.4.4:
     prismjs "^1.22.0"
     refractor "^3.2.0"
 
-react-transition-group@^4.4.0:
+react-transition-group@^4.0.0, react-transition-group@^4.4.0:
   version "4.4.2"
   resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.2.tgz#8b59a56f09ced7b55cbd53c36768b922890d5470"
   integrity sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg==
@@ -10252,6 +10300,13 @@ rgba-regex@^1.0.0:
   resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3"
   integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=
 
+rifm@^0.7.0:
+  version "0.7.0"
+  resolved "https://registry.yarnpkg.com/rifm/-/rifm-0.7.0.tgz#debe951a9c83549ca6b33e5919f716044c2230be"
+  integrity sha512-DSOJTWHD67860I5ojetXdEQRIBvF6YcpNe53j0vn1vp9EUb9N80EiZTxgP+FkDKorWC8PZw052kTF4C1GOivCQ==
+  dependencies:
+    "@babel/runtime" "^7.3.1"
+
 rimraf@^2.5.4, rimraf@^2.6.3:
   version "2.7.1"
   resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"

+ 30 - 0
codecov.yml

@@ -0,0 +1,30 @@
+coverage:
+  status:
+    project:
+      default:
+        flags:
+          - client
+          - express
+      client:
+        target: auto
+        flags:
+          - client
+      express:
+        target: auto
+        flags:
+          - express
+
+comment:
+  layout: "reach, diff, flags, files"
+  behavior: default
+  require_changes: true
+
+flags:
+  client:
+    paths:
+      - client/
+    carryforward: true
+  express:
+    paths:
+      - express/
+    carryforward: true

+ 7 - 0
express/.prettierrc

@@ -0,0 +1,7 @@
+{
+  "tabWidth": 2,
+  "semi": true,
+  "singleQuote": true,
+  "trailingComma": "es5",
+  "bracketSpacing": true
+}

+ 31 - 5
express/package.json

@@ -21,18 +21,40 @@
     "swagger-jsdoc": "^6.1.0",
     "swagger-ui-express": "^4.1.6"
   },
+  "jest": {
+    "testEnvironment": "node",
+    "testTimeout": 10000,
+    "coveragePathIgnorePatterns": [
+      "/node_modules/"
+    ],
+    "rootDir": "src",
+    "testRegex": ".*\\.test\\.ts$",
+    "collectCoverageFrom": [
+      "**/*.service.{js,ts}"
+    ],
+    "transform": {
+      "^.+\\.(t|j)s$": "ts-jest"
+    },
+    "coverageDirectory": "../coverage/"
+  },
   "devDependencies": {
-    "@types/swagger-jsdoc": "^6.0.1",
     "@types/chalk": "^2.2.0",
     "@types/cors": "^2.8.12",
     "@types/express": "^4.17.13",
     "@types/glob": "^7.2.0",
+    "@types/jest": "^27.0.2",
     "@types/morgan": "^1.9.3",
     "@types/node": "^16.11.6",
     "@types/node-cron": "^3.0.0",
+    "@types/supertest": "^2.0.11",
+    "@types/swagger-jsdoc": "^6.0.1",
     "@types/swagger-ui-express": "^4.1.3",
     "@types/ws": "^8.2.0",
+    "jest": "^27.3.1",
     "nodemon": "^2.0.14",
+    "prettier": "^2.4.1",
+    "supertest": "^6.1.6",
+    "ts-jest": "^27.0.7",
     "ts-node": "^10.4.0",
     "tslint": "^6.1.3",
     "typescript": "^4.4.4"
@@ -44,8 +66,12 @@
     "start": "nodemon ./src/app",
     "start:plugin": "yarn build && cross-env PLUGIN_DEV=1 node dist/milvus-insight/express/src/app.js",
     "start:prod": "node dist/app.js",
-    "test": "echo \"Error: no test specified\" && exit 1",
-    "clean": "rimraf dist"
+    "test": "cross-env NODE_ENV=test jest --passWithNoTests",
+    "test:watch": "jest --watch",
+    "test:cov": "cross-env NODE_ENV=test jest --passWithNoTests --coverage",
+    "test:report": "cross-env NODE_ENV=test jest --watchAll=false --coverage --coverageReporters='text-summary'",
+    "clean": "rimraf dist",
+    "format": "prettier --write '**/*.{ts,js}'"
   },
   "nodemonConfig": {
     "ignore": [
@@ -58,6 +84,6 @@
     "watch": [
       "src"
     ],
-    "ext": "ts"
+    "ext": "ts yml"
   }
-}
+}

+ 165 - 0
express/src/__tests__/__mocks__/consts.ts

@@ -0,0 +1,165 @@
+// mock data
+export const mockAddress = '127.0.0.1';
+export const mockCollectionNames = [{ name: 'c1' }, { name: 'c2' }];
+export const mockCollections = [
+  {
+    name: 'c1',
+    collectionID: 1,
+    schema: {
+      fields: [
+        {
+          name: 'vector_field',
+          data_type: 'data_type',
+          type_params: [
+            {
+              key: 'dim',
+              value: '4',
+            },
+          ],
+        },
+        {
+          is_primary_key: true,
+          autoID: true,
+          name: 'age',
+          data_type: 'data_type',
+          type_params: [] as any[],
+        },
+      ],
+      description: 'mock schema description 1',
+    },
+    created_utc_timestamp: '123456',
+  },
+  {
+    name: 'c2',
+    collectionID: 2,
+    schema: {
+      fields: [
+        {
+          name: 'vector_field',
+          data_type: 'data_type',
+          type_params: [
+            {
+              key: 'dim',
+              value: '4',
+            },
+          ],
+        },
+        {
+          name: 'age',
+          data_type: 'data_type',
+          type_params: [] as any[],
+        },
+      ],
+      description: 'mock schema description 2',
+    },
+    created_utc_timestamp: '1234567',
+  },
+];
+export const mockLoadedCollections = [
+  {
+    id: 1,
+    name: 'c1',
+    loadedPercentage: '100',
+  },
+];
+// index state is finished
+export const mockIndexState = [
+  { collection_name: 'c1', state: 3 },
+  { collection_name: 'c2', state: 2 },
+];
+
+export const mockPartition = {
+  partition_names: ['p1', 'p2'],
+  partitionIDs: [1, 2],
+  created_timestamps: ['12345', '12354'],
+  created_utc_timestamps: ['12345', '12354'],
+};
+
+// mock results
+export const mockGetAllCollectionsData = [
+  {
+    collection_name: 'c2',
+    schema: {
+      fields: [
+        {
+          name: 'vector_field',
+          data_type: 'data_type',
+          type_params: [
+            {
+              key: 'dim',
+              value: '4',
+            },
+          ],
+        },
+        {
+          name: 'age',
+          data_type: 'data_type',
+          type_params: [] as any[],
+        },
+      ],
+      description: 'mock schema description 2',
+    },
+    description: 'mock schema description 2',
+    autoID: undefined as boolean,
+    rowCount: 7,
+    id: 2,
+    loadedPercentage: '-1',
+    createdTime: 1234567,
+    index_status: 2,
+  },
+  {
+    collection_name: 'c1',
+    schema: {
+      fields: [
+        {
+          name: 'vector_field',
+          data_type: 'data_type',
+          type_params: [
+            {
+              key: 'dim',
+              value: '4',
+            },
+          ],
+        },
+        {
+          is_primary_key: true,
+          autoID: true,
+          name: 'age',
+          data_type: 'data_type',
+          type_params: [] as any[],
+        },
+      ],
+      description: 'mock schema description 1',
+    },
+    description: 'mock schema description 1',
+    autoID: true,
+    rowCount: 7,
+    id: 1,
+    loadedPercentage: '100',
+    createdTime: 123456,
+    index_status: 3,
+  },
+];
+
+export const mockLoadedCollectionsData = [
+  {
+    collection_name: 'c1',
+    id: 1,
+    rowCount: 7,
+  },
+];
+
+export const mockGetPartitionsInfoData = [
+  {
+    name: 'p1',
+    id: 1,
+    createdTime: '12345',
+    rowCount: 7,
+  },
+  {
+    name: 'p2',
+    id: 2,
+    createdTime: '12354',
+    rowCount: 7,
+  },
+];

+ 353 - 0
express/src/__tests__/__mocks__/milvus/milvusClient.ts

@@ -0,0 +1,353 @@
+import {
+  AlterAliasReq,
+  CreateAliasReq,
+  CreateCollectionReq,
+  CreateIndexReq,
+  CreatePartitionReq,
+  DescribeCollectionReq,
+  DescribeIndexReq,
+  DropAliasReq,
+  DropCollectionReq,
+  DropIndexReq,
+  DropPartitionReq,
+  FlushReq,
+  GetCollectionStatisticsReq,
+  GetIndexBuildProgressReq,
+  GetIndexStateReq,
+  GetPartitionStatisticsReq,
+  InsertReq,
+  LoadCollectionReq,
+  LoadPartitionsReq,
+  ReleaseLoadCollectionReq,
+  ReleasePartitionsReq,
+  SearchReq,
+  ShowCollectionsReq,
+  ShowPartitionsReq,
+} from '@zilliz/milvus2-sdk-node/dist/milvus/types';
+import { DeleteEntitiesReq } from '@zilliz/milvus2-sdk-node/dist/milvus/types/Data';
+import { QueryDto } from '../../../collections/dto';
+import {
+  CodeEnum,
+  ERR_NO_ADDRESS,
+  ERR_NO_ALIAS,
+  ERR_NO_COLLECTION,
+  ERR_NO_INDEX,
+  ERR_NO_PARAM,
+} from '../../utils/constants';
+import { mockStatusInfo } from '../../utils/mock.util';
+import {
+  mockCollectionNames,
+  mockCollections,
+  mockIndexState,
+  mockLoadedCollections,
+  mockPartition,
+} from '../consts';
+
+const mockMilvusClient = jest.fn().mockImplementation((address: string) => {
+  return {
+    collectionManager: {
+      hasCollection: (param: { collection_name: string }) => {
+        const { collection_name } = param;
+        if (address === '') {
+          throw new Error(ERR_NO_ADDRESS);
+        }
+        return collection_name;
+      },
+      showCollections: (param?: ShowCollectionsReq) => {
+        if (!param) {
+          return {
+            status: mockStatusInfo(CodeEnum.success),
+            data: mockCollectionNames,
+          };
+        }
+        const { collection_names, type } = param;
+        // loaded type
+        if (type === 1) {
+          return {
+            status: mockStatusInfo(CodeEnum.success),
+            data: mockLoadedCollections,
+          };
+        }
+        return collection_names && collection_names.length > 0
+          ? {
+              status: mockStatusInfo(CodeEnum.success),
+              data: collection_names,
+            }
+          : { status: mockStatusInfo(CodeEnum.error, ERR_NO_PARAM) };
+      },
+      createCollection: (param: CreateCollectionReq) => {
+        const { collection_name, fields } = param;
+        if (!collection_name) {
+          return mockStatusInfo(CodeEnum.error, ERR_NO_COLLECTION);
+        }
+        return { ...mockStatusInfo(CodeEnum.success), data: fields };
+      },
+      describeCollection: (param: DescribeCollectionReq) => {
+        const { collection_name } = param;
+        if (!collection_name) {
+          return {
+            status: mockStatusInfo(CodeEnum.error, ERR_NO_COLLECTION),
+          };
+        }
+        const res =
+          mockCollections.find((c) => c.name === collection_name) || {};
+        return {
+          status: mockStatusInfo(CodeEnum.success),
+          ...res,
+        };
+      },
+      dropCollection: (param: DropCollectionReq) => {
+        const { collection_name } = param;
+        if (!collection_name) {
+          return mockStatusInfo(CodeEnum.error, ERR_NO_COLLECTION);
+        }
+        return { ...mockStatusInfo(CodeEnum.success), data: collection_name };
+      },
+      loadCollection: (param: LoadCollectionReq) => {
+        const { collection_name } = param;
+        if (!collection_name) {
+          return mockStatusInfo(CodeEnum.error, ERR_NO_COLLECTION);
+        }
+        return { ...mockStatusInfo(CodeEnum.success), data: collection_name };
+      },
+      releaseCollection: (param: ReleaseLoadCollectionReq) => {
+        const { collection_name } = param;
+        if (!collection_name) {
+          return mockStatusInfo(CodeEnum.error, ERR_NO_COLLECTION);
+        }
+
+        return { ...mockStatusInfo(CodeEnum.success), data: collection_name };
+      },
+      getCollectionStatistics: (param: GetCollectionStatisticsReq) => {
+        const { collection_name } = param;
+        if (!collection_name) {
+          return { status: mockStatusInfo(CodeEnum.error, ERR_NO_COLLECTION) };
+        }
+        const data = {
+          name: collection_name,
+          stats: [{ key: 'row_count', value: 7 }],
+        };
+        return {
+          status: mockStatusInfo(CodeEnum.success),
+          ...data,
+        };
+      },
+      createAlias: (param: CreateAliasReq) => {
+        const { collection_name } = param;
+        if (!collection_name) {
+          return mockStatusInfo(CodeEnum.error, ERR_NO_COLLECTION);
+        }
+
+        return {
+          ...mockStatusInfo(CodeEnum.success),
+          data: param,
+        };
+      },
+      alterAlias: (param: AlterAliasReq) => {
+        const { collection_name } = param;
+        if (!collection_name) {
+          return mockStatusInfo(CodeEnum.error, ERR_NO_COLLECTION);
+        }
+
+        return {
+          ...mockStatusInfo(CodeEnum.success),
+          data: param,
+        };
+      },
+      dropAlias: (param: DropAliasReq) => {
+        const { alias } = param;
+        if (!alias) {
+          return mockStatusInfo(CodeEnum.error, ERR_NO_ALIAS);
+        }
+
+        return {
+          ...mockStatusInfo(CodeEnum.success),
+          data: alias,
+        };
+      },
+    },
+    partitionManager: {
+      createPartition: (param: CreatePartitionReq) => {
+        const { collection_name, partition_name } = param;
+        if (!collection_name) {
+          return mockStatusInfo(CodeEnum.error, ERR_NO_COLLECTION);
+        }
+        return { ...mockStatusInfo(CodeEnum.success), data: partition_name };
+      },
+      dropPartition: (param: DropPartitionReq) => {
+        const { collection_name } = param;
+        if (!collection_name) {
+          return mockStatusInfo(CodeEnum.error, ERR_NO_COLLECTION);
+        }
+        return { ...mockStatusInfo(CodeEnum.success), data: param };
+      },
+      loadPartitions: (param: LoadPartitionsReq) => {
+        const { collection_name } = param;
+        if (!collection_name) {
+          return mockStatusInfo(CodeEnum.error, ERR_NO_COLLECTION);
+        }
+        return { ...mockStatusInfo(CodeEnum.success), data: param };
+      },
+      releasePartitions: (param: ReleasePartitionsReq) => {
+        const { collection_name } = param;
+        if (!collection_name) {
+          return mockStatusInfo(CodeEnum.error, ERR_NO_COLLECTION);
+        }
+        return { ...mockStatusInfo(CodeEnum.success), data: param };
+      },
+      showPartitions: (param: ShowPartitionsReq) => {
+        const { collection_name } = param;
+        if (!collection_name) {
+          return { status: mockStatusInfo(CodeEnum.error, ERR_NO_COLLECTION) };
+        }
+        return {
+          status: mockStatusInfo(CodeEnum.success),
+          ...mockPartition,
+        };
+      },
+      getPartitionStatistics: (param: GetPartitionStatisticsReq) => {
+        const { collection_name, partition_name } = param;
+        if (!collection_name) {
+          return { status: mockStatusInfo(CodeEnum.error, ERR_NO_COLLECTION) };
+        }
+
+        const data = {
+          name: partition_name,
+          stats: [{ key: 'row_count', value: 7 }],
+        };
+
+        return {
+          status: mockStatusInfo(CodeEnum.success),
+          ...data,
+        };
+      },
+    },
+    indexManager: {
+      getIndexState: (param: GetIndexStateReq) => {
+        const { collection_name } = param;
+        if (!collection_name) {
+          return { status: mockStatusInfo(CodeEnum.error, ERR_NO_COLLECTION) };
+        }
+        const data =
+          mockIndexState.find((i) => i.collection_name === collection_name) ||
+          {};
+        return {
+          status: mockStatusInfo(CodeEnum.success),
+          ...data,
+        };
+      },
+      createIndex: (param: CreateIndexReq) => {
+        const { collection_name } = param;
+        if (!collection_name) {
+          return mockStatusInfo(CodeEnum.error, ERR_NO_COLLECTION);
+        }
+
+        return {
+          ...mockStatusInfo(CodeEnum.success),
+          data: param,
+        };
+      },
+      describeIndex: (param: DescribeIndexReq) => {
+        const { collection_name, field_name } = param;
+        if (!collection_name) {
+          return { status: mockStatusInfo(CodeEnum.error, ERR_NO_COLLECTION) };
+        }
+        if (!field_name) {
+          return {
+            status: mockStatusInfo(CodeEnum.indexNoExist, ERR_NO_INDEX),
+          };
+        }
+
+        return {
+          status: mockStatusInfo(CodeEnum.success),
+          data: param,
+        };
+      },
+      dropIndex: (param: DropIndexReq) => {
+        const { collection_name } = param;
+        if (!collection_name) {
+          return mockStatusInfo(CodeEnum.error, ERR_NO_COLLECTION);
+        }
+
+        return {
+          ...mockStatusInfo(CodeEnum.success),
+          data: param,
+        };
+      },
+      getIndexBuildProgress: (param: GetIndexBuildProgressReq) => {
+        const { collection_name } = param;
+        if (!collection_name) {
+          return { status: mockStatusInfo(CodeEnum.error, ERR_NO_COLLECTION) };
+        }
+
+        return {
+          status: mockStatusInfo(CodeEnum.success),
+          data: param,
+        };
+      },
+    },
+    dataManager: {
+      flush: (data: FlushReq) => ({
+        data,
+      }),
+      getMetric: (data: { request: { metric_type: string } }) => {
+        const {
+          request: { metric_type: type },
+        } = data;
+
+        return {
+          type,
+        };
+      },
+      insert: (param: InsertReq) => {
+        const { collection_name } = param;
+        if (!collection_name) {
+          return { status: mockStatusInfo(CodeEnum.error, ERR_NO_COLLECTION) };
+        }
+
+        return {
+          status: mockStatusInfo(CodeEnum.success),
+          data: param,
+        };
+      },
+      search: (param: SearchReq) => {
+        const { collection_name } = param;
+        if (!collection_name) {
+          return { status: mockStatusInfo(CodeEnum.error, ERR_NO_COLLECTION) };
+        }
+
+        return {
+          status: mockStatusInfo(CodeEnum.success),
+          data: param,
+        };
+      },
+      query: (
+        param: {
+          collection_name: string;
+        } & QueryDto
+      ) => {
+        const { collection_name } = param;
+        if (!collection_name) {
+          return { status: mockStatusInfo(CodeEnum.error, ERR_NO_COLLECTION) };
+        }
+
+        return {
+          status: mockStatusInfo(CodeEnum.success),
+          data: param,
+        };
+      },
+      deleteEntities: (param: DeleteEntitiesReq) => {
+        const { collection_name } = param;
+        if (!collection_name) {
+          return { status: mockStatusInfo(CodeEnum.error, ERR_NO_COLLECTION) };
+        }
+        return {
+          status: mockStatusInfo(CodeEnum.success),
+          data: param,
+        };
+      },
+    },
+  };
+});
+
+export default mockMilvusClient;

+ 9 - 0
express/src/__tests__/__mocks__/milvus/milvusService.ts

@@ -0,0 +1,9 @@
+const mockMivusService = jest.fn().mockImplementation(() => {
+  return {
+    connectMilvus: (address: string) => new Promise(() => !!address),
+    checkConnect: (address: string) => new Promise(() => address),
+    flush: (collectionName: string) => new Promise(() => collectionName),
+  };
+});
+
+export default mockMivusService;

+ 302 - 0
express/src/__tests__/collections/collections.service.test.ts

@@ -0,0 +1,302 @@
+import mockMilvusClient from '../__mocks__/milvus/milvusClient';
+import { CollectionsService } from '../../collections/collections.service';
+import { MilvusService } from '../../milvus/milvus.service';
+import {
+  ERR_NO_ALIAS,
+  ERR_NO_COLLECTION,
+  ERR_NO_PARAM,
+} from '../utils/constants';
+import {
+  mockAddress,
+  mockCollectionNames,
+  mockCollections,
+  mockGetAllCollectionsData,
+  mockLoadedCollections,
+  mockLoadedCollectionsData,
+} from '../__mocks__/consts';
+
+// mock Milvus client
+jest.mock('@zilliz/milvus2-sdk-node', () => {
+  return {
+    MilvusClient: mockMilvusClient,
+  };
+});
+
+describe('Test collections service', () => {
+  let milvusService: any;
+  let service: any;
+
+  beforeAll(async () => {
+    // setup Milvus service and connect to mock Milvus client
+    milvusService = new MilvusService();
+    await milvusService.connectMilvus(mockAddress);
+    service = new CollectionsService(milvusService);
+  });
+
+  afterAll(() => {
+    milvusService = null;
+    service = null;
+  });
+
+  test('test managers after connected to Milvus', () => {
+    expect(service.collectionManager).toBeDefined();
+    expect(service.dataManager).toBeDefined();
+    expect(service.indexManager).toBeDefined();
+  });
+
+  test('test getCollections method', async () => {
+    const res = await service.getCollections({
+      collection_names: ['c1', 'c2'],
+    });
+    expect(res.data.length).toBe(2);
+
+    const defaultRes = await service.getCollections();
+    expect(defaultRes.data).toEqual(mockCollectionNames);
+
+    const loadedRes = await service.getCollections({ type: 1 });
+    expect(loadedRes.data).toEqual(mockLoadedCollections);
+
+    try {
+      await service.getCollections({ collection_names: [] });
+    } catch (err) {
+      expect(err).toBe(ERR_NO_PARAM);
+    }
+  });
+
+  test('test createCollection method', async () => {
+    const res = await service.createCollection({
+      collection_name: 'c1',
+      fields: [],
+    });
+    expect(res.data.length).toBe(0);
+
+    try {
+      await service.createCollection({ collection_name: '', fields: [] });
+    } catch (err) {
+      expect(err).toBe(ERR_NO_COLLECTION);
+    }
+  });
+
+  test('test describeCollection method', async () => {
+    const res = await service.describeCollection({
+      collection_name: 'c1',
+    });
+    const { status, ...result } = res;
+    const [mockRes] = mockCollections;
+    expect(result).toEqual(mockRes);
+
+    try {
+      await service.describeCollection({ collection_name: '' });
+    } catch (err) {
+      expect(err).toBe(ERR_NO_COLLECTION);
+    }
+  });
+
+  test('test dropCollection method', async () => {
+    const res = await service.dropCollection({ collection_name: 'c1' });
+    expect(res.data).toBe('c1');
+
+    try {
+      await service.dropCollection({ collection_name: '' });
+    } catch (err) {
+      expect(err).toBe(ERR_NO_COLLECTION);
+    }
+  });
+
+  test('test loadCollection method', async () => {
+    const res = await service.loadCollection({ collection_name: 'c1' });
+    expect(res.data).toBe('c1');
+
+    try {
+      await service.loadCollection({ collection_name: '' });
+    } catch (err) {
+      expect(err).toBe(ERR_NO_COLLECTION);
+    }
+  });
+
+  test('test releaseCollection method', async () => {
+    const res = await service.releaseCollection({ collection_name: 'c1' });
+    expect(res.data).toBe('c1');
+
+    try {
+      await service.releaseCollection({ collection_name: '' });
+    } catch (err) {
+      expect(err).toBe(ERR_NO_COLLECTION);
+    }
+  });
+
+  test('test getCollectionStatistics method', async () => {
+    const res = await service.getCollectionStatistics({
+      collection_name: 'c1',
+    });
+    const { status, ...data } = res;
+    expect(data.name).toBe('c1');
+    expect(data.stats.length).toBe(1);
+
+    try {
+      await service.getCollectionStatistics({ collection_name: '' });
+    } catch (err) {
+      expect(err).toBe(ERR_NO_COLLECTION);
+    }
+  });
+
+  test('test insert method', async () => {
+    const mockParam = {
+      collection_name: 'c1',
+      fields_data: {
+        vector_field: [1, 2, 3, 4],
+        age: 7,
+      },
+    };
+    const res = await service.insert(mockParam);
+    expect(res.data).toEqual(mockParam);
+
+    try {
+      await service.insert({
+        collection_name: '',
+        fields_data: {
+          vector_field: [1, 2, 3, 4],
+        },
+      });
+    } catch (err) {
+      expect(err).toBe(ERR_NO_COLLECTION);
+    }
+  });
+
+  test('test vectorSearch method', async () => {
+    const mockParam = {
+      collection_name: 'c1',
+      search_params: {
+        anns_field: 'float_vector',
+        topk: '10',
+        metric_type: 'L2',
+        params: JSON.stringify({ nprobe: 1024 }),
+      },
+      vectors: [[1, 2, 3, 4]],
+      vector_type: 101,
+    };
+
+    const res = await service.vectorSearch(mockParam);
+    expect(res.data).toEqual(mockParam);
+
+    try {
+      await service.vectorSearch({ ...mockParam, collection_name: '' });
+    } catch (err) {
+      expect(err).toBe(ERR_NO_COLLECTION);
+    }
+  });
+
+  test('test createAlias method', async () => {
+    const mockParam = {
+      collection_name: 'c1',
+      alias: 'alias',
+    };
+
+    const res = await service.createAlias(mockParam);
+    expect(res.data).toEqual(mockParam);
+
+    try {
+      await service.createAlias({ collection_name: '', alias: '' });
+    } catch (err) {
+      expect(err).toBe(ERR_NO_COLLECTION);
+    }
+  });
+
+  test('test alterAlias method', async () => {
+    const mockParam = {
+      collection_name: 'c1',
+      alias: 'alias',
+    };
+
+    const res = await service.alterAlias(mockParam);
+    expect(res.data).toEqual(mockParam);
+
+    try {
+      await service.alterAlias({ collection_name: '', alias: '' });
+    } catch (err) {
+      expect(err).toBe(ERR_NO_COLLECTION);
+    }
+  });
+
+  test('test dropAlias method', async () => {
+    const res = await service.dropAlias({ alias: 'alias' });
+    expect(res.data).toBe('alias');
+
+    try {
+      await service.dropAlias({ alias: '' });
+    } catch (err) {
+      expect(err).toBe(ERR_NO_ALIAS);
+    }
+  });
+
+  test('test query method', async () => {
+    const mockParam = {
+      collection_name: 'c1',
+      expr: 'age > 7',
+    };
+    const res = await service.query(mockParam);
+    expect(res.data).toEqual(mockParam);
+
+    try {
+      await service.query({ collection_name: '', expr: '' });
+    } catch (err) {
+      expect(err).toBe(ERR_NO_COLLECTION);
+    }
+  });
+
+  test('test getIndexStatus method', async () => {
+    const res = await service.getIndexStatus({ collection_name: 'c1' });
+    const { status, ...data } = res;
+    expect(data).toEqual({ collection_name: 'c1', state: 3 });
+  });
+
+  test('test getAllCollections method', async () => {
+    const res = await service.getAllCollections();
+    expect(res).toEqual(mockGetAllCollectionsData);
+  });
+
+  test('test getLoadedCollections method', async () => {
+    const res = await service.getLoadedColletions();
+    expect(res).toEqual(mockLoadedCollectionsData);
+  });
+
+  test('test getStatistics method', async () => {
+    const res = await service.getStatistics();
+    expect(res).toEqual({
+      // 2 collections
+      collectionCount: 2,
+      // each collection 7 row counts
+      totalData: 14,
+    });
+  });
+
+  test('test getCollectionIndexStatus method', async () => {
+    const res = await service.getCollectionsIndexStatus();
+    expect(res).toEqual([
+      {
+        collection_name: 'c1',
+        index_status: 3,
+      },
+      {
+        collection_name: 'c2',
+        index_status: 2,
+      },
+    ]);
+  });
+
+  test('test deleteEntities method', async () => {
+    const mockParam = {
+      collection_name: 'c1',
+      expr: 'age > 7',
+    };
+
+    const res = await service.deleteEntities(mockParam);
+    expect(res.data).toEqual(mockParam);
+
+    try {
+      await service.deleteEntities({ collection_name: '', expr: '' });
+    } catch (err) {
+      expect(err).toBe(ERR_NO_COLLECTION);
+    }
+  });
+});

+ 160 - 0
express/src/__tests__/crons/crons.service.test.ts

@@ -0,0 +1,160 @@
+import mockMilvusClient from '../__mocks__/milvus/milvusClient';
+import { schedule } from 'node-cron';
+import { CollectionsService } from '../../collections/collections.service';
+import { CronsService, SchedulerRegistry } from '../../crons/crons.service';
+import { MilvusService } from '../../milvus/milvus.service';
+import { WS_EVENTS, WS_EVENTS_TYPE } from '../../utils/Const';
+import { mockAddress } from '../__mocks__/consts';
+
+// mock Milvus client
+jest.mock('@zilliz/milvus2-sdk-node', () => {
+  return {
+    MilvusClient: mockMilvusClient,
+  };
+});
+
+// mock node-cron
+jest.mock('node-cron', () => {
+  return {
+    schedule: jest.fn(),
+  };
+});
+
+// mock variable
+const mockCronFrequency = '30 00 * * *';
+const mockCronEverySec = '* * * * * *';
+const mockCb = jest.fn();
+const mockErrCb = jest.fn(() => {
+  throw new Error('error');
+});
+const mockName = 'j1';
+const mockSecName = 'everySec';
+
+describe('test crons service', () => {
+  let milvusService: any;
+  let collectionService: any;
+  let cronsService: any;
+  let schedulerRegistry: any;
+
+  const handleStartTask = jest.fn();
+  const handleEndTask = jest.fn();
+
+  const setup = async () => {
+    milvusService = new MilvusService();
+    await milvusService.connectMilvus(mockAddress);
+    collectionService = new CollectionsService(milvusService);
+
+    schedulerRegistry = new SchedulerRegistry([]);
+    cronsService = new CronsService(collectionService, schedulerRegistry);
+  };
+
+  beforeAll(async () => {
+    // setup Milvus service and connect to mock Milvus client
+    await setup();
+  });
+
+  beforeEach(() => {
+    // mock schedule
+    (schedule as jest.Mock).mockImplementationOnce((frequency, callback) => {
+      callback();
+      return {
+        start: handleStartTask,
+        stop: handleEndTask,
+      };
+    });
+  });
+
+  afterAll(() => {
+    milvusService = null;
+    collectionService = null;
+    schedulerRegistry = null;
+    cronsService = null;
+  });
+
+  test('test SchedulerRegistry related methods', async () => {
+    schedulerRegistry.setCronJob(mockName, mockCronFrequency, () => mockCb());
+    expect(mockCb).toBeCalledTimes(1);
+    expect(schedule).toBeCalledWith(mockCronFrequency, expect.any(Function));
+
+    const job = schedulerRegistry.getCronJob(mockName);
+    expect(job).toEqual({
+      start: handleStartTask,
+      stop: handleEndTask,
+    });
+
+    schedulerRegistry.setCronJob(mockName, mockCronFrequency, () => mockCb());
+    expect(handleEndTask).toBeCalled();
+
+    schedulerRegistry.setCronJobEverySecond(mockSecName, () => mockCb());
+    expect(schedule).toBeCalledWith(mockCronEverySec, expect.any(Function));
+
+    schedulerRegistry.setCronJob(mockName, mockCronFrequency, () => mockCb());
+    expect(handleEndTask).toBeCalled();
+
+    schedulerRegistry.setCronJob(mockName, mockCronFrequency, () =>
+      mockErrCb()
+    );
+    expect(() => {
+      mockErrCb();
+    }).toThrow();
+  });
+
+  test('test CronService related methods', async () => {
+    try {
+      await cronsService.toggleCronJobByName({
+        name: WS_EVENTS.COLLECTION,
+        type: WS_EVENTS_TYPE.STOP,
+      });
+    } catch (err) {
+      expect(err.message).toBe('No existed job entity');
+    }
+
+    await cronsService.toggleCronJobByName({
+      name: WS_EVENTS.COLLECTION,
+      type: WS_EVENTS_TYPE.START,
+    });
+    expect(schedule).toBeCalledWith(mockCronEverySec, expect.any(Function));
+
+    schedulerRegistry.setCronJob(WS_EVENTS.COLLECTION, mockCronFrequency, () =>
+      mockCb()
+    );
+    await cronsService.toggleCronJobByName({
+      name: WS_EVENTS.COLLECTION,
+      type: WS_EVENTS_TYPE.START,
+    });
+
+    expect(handleStartTask).toBeCalled();
+    await cronsService.toggleCronJobByName({
+      name: WS_EVENTS.COLLECTION,
+      type: WS_EVENTS_TYPE.STOP,
+    });
+    expect(handleStartTask).toBeCalled();
+
+    try {
+      await cronsService.toggleCronJobByName({
+        name: mockName,
+        type: WS_EVENTS_TYPE.STOP,
+      });
+    } catch (err) {
+      expect(err.message).toBe('Unsupported event type');
+    }
+  });
+
+  test('test getCollections error', async () => {
+    // reset setup to trigger error
+    const newCollectionService = new CollectionsService(milvusService);
+    const newSchedulerRegistry = new SchedulerRegistry([]);
+    newCollectionService.getAllCollections = () => {
+      throw new Error('error');
+    };
+
+    const newCronsService = new CronsService(
+      newCollectionService,
+      newSchedulerRegistry
+    );
+
+    await newCronsService.getCollections(WS_EVENTS.COLLECTION);
+    expect(schedule).toBeCalledWith(mockCronEverySec, expect.any(Function));
+    expect(handleEndTask).toBeCalled();
+  });
+});

+ 54 - 0
express/src/__tests__/milvus/milvus.controller.test.ts

@@ -0,0 +1,54 @@
+import express from 'express';
+import http from 'http';
+import supertest from 'supertest';
+import { TransformResMiddlerware } from '../../middlewares';
+import { router as connectRouter } from '../../milvus/index';
+import { mockAddress } from '../__mocks__/consts';
+
+import MilvusService from '../__mocks__/milvus/milvusService';
+
+// mock Milvus client service
+jest.mock('../__mocks__/milvus/milvusService');
+
+describe('Test Milvus Module', () => {
+  let app: any;
+  let server: any;
+  let request: any;
+
+  // setup app and server
+  beforeAll((done) => {
+    app = express();
+    const router = express.Router();
+    router.use('/milvus', connectRouter);
+    app.use(TransformResMiddlerware);
+    app.use('/api/v1', router);
+    server = http.createServer(app);
+    server.listen(done);
+    request = supertest(server);
+  });
+
+  beforeEach(() => {
+    // Clear all instances and calls to constructor and all methods:
+    MilvusService.mockClear();
+  });
+
+  // close server
+  afterAll((done) => {
+    server.close(done);
+  });
+
+  test('check whether connected to Milvus with address', () => {
+    // with address param
+    request.get('/check').query({ address: mockAddress }).expect(200);
+    // without address param
+    request.get('/check').expect(404);
+  });
+
+  test('check request to connect to Milvus', () => {
+    request
+      .post('/connect')
+      .send({ address: mockAddress })
+      .set('Accept', 'application/json')
+      .expect(200);
+  });
+});

+ 82 - 0
express/src/__tests__/milvus/milvus.service.test.ts

@@ -0,0 +1,82 @@
+import mockMilvusClient from '../__mocks__/milvus/milvusClient';
+import { MilvusService } from '../../milvus/milvus.service';
+import { mockAddress } from '../__mocks__/consts';
+
+// mock Milvus client
+jest.mock('@zilliz/milvus2-sdk-node', () => {
+  return {
+    MilvusClient: mockMilvusClient,
+  };
+});
+
+describe('Test Milvus service', () => {
+  let service: any;
+
+  // init service
+  beforeEach(() => {
+    service = new MilvusService();
+  });
+
+  afterEach(() => {
+    service = null;
+  });
+
+  test('test connectMilvus method', async () => {
+    expect(service.milvusClientGetter).toBeUndefined();
+    expect(service.milvusAddressGetter).toBe('');
+
+    const res = await service.connectMilvus(mockAddress);
+    expect(res.address).toBe(mockAddress);
+    expect(service.milvusAddressGetter).toBe(mockAddress);
+    expect(service.milvusClientGetter).toBeDefined();
+  });
+
+  test('test connectMilvus method error', async () => {
+    try {
+      await service.connectMilvus('');
+    } catch (err) {
+      expect(err.message).toBe(
+        'Connect milvus failed, check your milvus address.'
+      );
+    }
+  });
+
+  test('test checkMilvus when not connect to Milvus', () => {
+    try {
+      service.checkMilvus();
+    } catch (err) {
+      expect(err.message).toBe('Please connect milvus first');
+    }
+  });
+
+  test('test checkConnect method', async () => {
+    // mock connect first
+    await service.connectMilvus(mockAddress);
+    // different address
+    const errorRes = await service.checkConnect('123');
+    expect(errorRes.connected).toBeFalsy();
+    const res = await service.checkConnect(mockAddress);
+    expect(res.connected).toBeTruthy();
+  });
+
+  test('test managers after connected', async () => {
+    await service.connectMilvus(mockAddress);
+    expect(service.collectionManager).toBeDefined();
+    expect(service.partitionManager).toBeDefined();
+    expect(service.indexManager).toBeDefined();
+    expect(service.dataManager).toBeDefined();
+  });
+
+  test('test flush method', async () => {
+    await service.connectMilvus(mockAddress);
+    const res = await service.flush({ collection_names: ['c1', 'c2'] });
+    const data = res.data.collection_names;
+    expect(data.length).toBe(2);
+  });
+
+  test('test getMetrics method', async () => {
+    await service.connectMilvus(mockAddress);
+    const res = await service.getMetrics();
+    expect(res.type).toBe('system_info');
+  });
+});

+ 144 - 0
express/src/__tests__/partitions/partitions.service.test.ts

@@ -0,0 +1,144 @@
+import mockMilvusClient from '../__mocks__/milvus/milvusClient';
+import { MilvusService } from '../../milvus/milvus.service';
+import { ERR_NO_COLLECTION } from '../utils/constants';
+import { PartitionsService } from '../../partitions/partitions.service';
+import {
+  mockAddress,
+  mockGetPartitionsInfoData,
+  mockPartition,
+} from '../__mocks__/consts';
+
+// mock Milvus client
+jest.mock('@zilliz/milvus2-sdk-node', () => {
+  return {
+    MilvusClient: mockMilvusClient,
+  };
+});
+
+describe('Test partitions service', () => {
+  let milvusService: any;
+  let service: any;
+
+  beforeAll(async () => {
+    // setup Milvus service and connect to mock Milvus client
+    milvusService = new MilvusService();
+    await milvusService.connectMilvus(mockAddress);
+    service = new PartitionsService(milvusService);
+  });
+
+  afterAll(() => {
+    milvusService = null;
+    service = null;
+  });
+
+  test('test manager after connected to Milvus', () => {
+    expect(service.partitionManager).toBeDefined();
+  });
+
+  test('test createPartition method', async () => {
+    const res = await service.createPartition({
+      collection_name: 'c1',
+      partition_name: 'p1',
+    });
+    expect(res.data).toBe('p1');
+
+    try {
+      await service.createPartition({
+        collection_name: '',
+        partition_name: 'p1',
+      });
+    } catch (err) {
+      expect(err).toBe(ERR_NO_COLLECTION);
+    }
+  });
+
+  test('test deletePartition method', async () => {
+    const mockParam = {
+      collection_name: 'c1',
+      partition_name: 'p1',
+    };
+    const res = await service.deletePartition(mockParam);
+    expect(res.data).toEqual(mockParam);
+
+    try {
+      await service.deletePartition({
+        collection_name: '',
+        partition_name: '',
+      });
+    } catch (err) {
+      expect(err).toBe(ERR_NO_COLLECTION);
+    }
+  });
+
+  test('test loadPartitions method', async () => {
+    const mockParam = {
+      collection_name: 'c1',
+      partition_names: ['p1', 'p2'],
+    };
+    const res = await service.loadPartitions(mockParam);
+    expect(res.data).toEqual(mockParam);
+
+    try {
+      await service.loadPartitions({
+        collection_name: '',
+        partition_names: [],
+      });
+    } catch (err) {
+      expect(err).toBe(ERR_NO_COLLECTION);
+    }
+  });
+
+  test('test releasePartitions method', async () => {
+    const mockParam = {
+      collection_name: 'c1',
+      partition_names: ['p1', 'p2'],
+    };
+    const res = await service.releasePartitions(mockParam);
+    expect(res.data).toEqual(mockParam);
+
+    try {
+      await service.releasePartitions({
+        collection_name: '',
+        partition_names: [],
+      });
+    } catch (err) {
+      expect(err).toBe(ERR_NO_COLLECTION);
+    }
+  });
+
+  test('test getPartitions method', async () => {
+    const res = await service.getPartitions({ collection_name: 'c1' });
+    const { status, ...data } = res;
+    expect(data).toEqual(mockPartition);
+
+    try {
+      await service.getPartitions({ collection_name: '' });
+    } catch (err) {
+      expect(err).toBe(ERR_NO_COLLECTION);
+    }
+  });
+
+  test('test getPartitionStatistics method', async () => {
+    const res = await service.getPartitionStatistics({
+      collection_name: 'c1',
+      partition_name: 'p1',
+    });
+    const { status, ...data } = res;
+    expect(data.name).toBe('p1');
+    expect(data.stats.length).toBe(1);
+
+    try {
+      await service.getPartitionStatistics({
+        collection_name: '',
+        partition_name: '',
+      });
+    } catch (err) {
+      expect(err).toBe(ERR_NO_COLLECTION);
+    }
+  });
+
+  test('test getPartitionsInfo method', async () => {
+    const res = await service.getPartitionsInfo({ collection_name: 'c1' });
+    expect(res).toEqual(mockGetPartitionsInfoData);
+  });
+});

+ 114 - 0
express/src/__tests__/schema/schema.service.test.ts

@@ -0,0 +1,114 @@
+import mockMilvusClient from '../__mocks__/milvus/milvusClient';
+import { MilvusService } from '../../milvus/milvus.service';
+import { CodeEnum, ERR_NO_COLLECTION } from '../utils/constants';
+import { SchemaService } from '../../schema/schema.service';
+import { mockAddress } from '../__mocks__/consts';
+
+// mock Milvus client
+jest.mock('@zilliz/milvus2-sdk-node', () => {
+  return {
+    MilvusClient: mockMilvusClient,
+  };
+});
+
+describe('Test schema service', () => {
+  let milvusService: any;
+  let service: any;
+
+  beforeAll(async () => {
+    // setup Milvus service and connect to mock Milvus client
+    milvusService = new MilvusService();
+    await milvusService.connectMilvus(mockAddress);
+    service = new SchemaService(milvusService);
+  });
+
+  afterAll(() => {
+    milvusService = null;
+    service = null;
+  });
+
+  test('test manager after connected to Milvus', () => {
+    expect(service.indexManager).toBeDefined();
+  });
+
+  test('test createIndex method', async () => {
+    const mockParam = {
+      collection_name: 'c1',
+      field_name: 'vector_field',
+      extra_params: {
+        index_type: 'BIN_FLAT',
+        metric_type: 'HAMMING',
+        params: JSON.stringify({ nlist: 1024 }),
+      },
+    };
+    const res = await service.createIndex(mockParam);
+    expect(res.data).toEqual(mockParam);
+
+    try {
+      await service.createIndex({ ...mockParam, collection_name: '' });
+    } catch (err) {
+      expect(err).toBe(ERR_NO_COLLECTION);
+    }
+  });
+
+  test('test describeIndex method', async () => {
+    const res = await service.describeIndex({
+      collection_name: 'c1',
+      field_name: 'f1',
+    });
+    expect(res.data).toEqual({ collection_name: 'c1', field_name: 'f1' });
+
+    const noExistRes = await service.describeIndex({ collection_name: 'c1' });
+    expect(noExistRes.status.error_code).toBe(CodeEnum.indexNoExist);
+
+    try {
+      await service.describeIndex({ collection_name: '' });
+    } catch (err) {
+      expect(err).toBe(ERR_NO_COLLECTION);
+    }
+  });
+
+  test('test dropIndex method', async () => {
+    const res = await service.dropIndex({
+      collection_name: 'c1',
+    });
+    expect(res.data).toEqual({ collection_name: 'c1' });
+
+    try {
+      await service.dropIndex({ collection_name: '' });
+    } catch (err) {
+      expect(err).toBe(ERR_NO_COLLECTION);
+    }
+  });
+
+  test('test getIndexState method', async () => {
+    const res = await service.getIndexState({ collection_name: 'c1' });
+    const { status, ...data } = res;
+    expect(data).toEqual({ collection_name: 'c1', state: 3 });
+
+    try {
+      await service.getIndexState({ collection_name: '' });
+    } catch (err) {
+      expect(err).toBe(ERR_NO_COLLECTION);
+    }
+  });
+
+  test('test getIndexBuildProgress method', async () => {
+    const mockParam = {
+      collection_name: 'c1',
+      field_name: 'f1',
+      index_name: 'i1',
+    };
+    const res = await service.getIndexBuildProgress(mockParam);
+    expect(res.data).toEqual(mockParam);
+
+    try {
+      await service.getIndexBuildProgress({
+        ...mockParam,
+        collection_name: '',
+      });
+    } catch (err) {
+      expect(err).toBe(ERR_NO_COLLECTION);
+    }
+  });
+});

+ 17 - 0
express/src/__tests__/utils/constants.ts

@@ -0,0 +1,17 @@
+export enum CodeEnum {
+  success = 'Success',
+  error = 'Error',
+  indexNoExist = 'IndexNotExist',
+}
+
+export type CodeStatus = {
+  error_code: CodeEnum;
+  reason?: string;
+};
+
+// error msgs
+export const ERR_NO_COLLECTION = 'collection name is invalid';
+export const ERR_NO_ADDRESS = 'no address';
+export const ERR_NO_PARAM = 'no valid param';
+export const ERR_NO_ALIAS = 'no valid alias';
+export const ERR_NO_INDEX = 'index not exist';

+ 14 - 0
express/src/__tests__/utils/mock.util.ts

@@ -0,0 +1,14 @@
+import { CodeEnum, CodeStatus } from './constants';
+
+export const mockStatusInfo = (type: CodeEnum, msg?: string): CodeStatus => {
+  if (type === CodeEnum.success) {
+    return {
+      error_code: type,
+      reason: 'success',
+    };
+  }
+  return {
+    error_code: type,
+    reason: msg,
+  };
+};

+ 1 - 1
express/src/app.ts

@@ -25,7 +25,7 @@ const PLUGIN_DEV = process.env?.PLUGIN_DEV;
 const SRC_PLUGIN_DIR = "src/plugins";
 const DEV_PLUGIN_DIR = "../../src/*/server";
 
-const app = express();
+export const app = express();
 const PORT = 3000;
 // initialize a simple http server
 const server = http.createServer(app);

+ 41 - 38
express/src/collections/collections.controller.ts

@@ -1,15 +1,14 @@
-import { NextFunction, Request, Response, Router } from "express";
-import { dtoValidationMiddleware } from "../middlewares/validation";
-import { milvusService } from "../milvus";
-import { CollectionsService } from "./collections.service";
+import { NextFunction, Request, Response, Router } from 'express';
+import { dtoValidationMiddleware } from '../middlewares/validation';
+import { milvusService } from '../milvus';
+import { CollectionsService } from './collections.service';
 import {
   CreateAliasDto,
   CreateCollectionDto,
   InsertDataDto,
-  ShowCollectionsDto,
   VectorSearchDto,
   QueryDto,
-} from "./dto";
+} from './dto';
 
 export class CollectionController {
   private collectionsService: CollectionsService;
@@ -26,67 +25,57 @@ export class CollectionController {
   }
 
   generateRoutes() {
-    /**
-     * @swagger
-     * /collections:
-     *   get:
-     *     description: Get all or loaded collection
-     *     responses:
-     *       200:
-     *         Collections List
-     */
-    this.router.get(
-      "/",
-      dtoValidationMiddleware(ShowCollectionsDto),
-      this.showCollections.bind(this)
-    );
+    this.router.get('/', this.showCollections.bind(this));
 
     this.router.post(
-      "/",
+      '/',
       dtoValidationMiddleware(CreateCollectionDto),
       this.createCollection.bind(this)
     );
 
-    this.router.get("/statistics", this.getStatistics.bind(this));
+    this.router.get('/statistics', this.getStatistics.bind(this));
 
     this.router.get(
-      "/:name/statistics",
+      '/:name/statistics',
       this.getCollectionStatistics.bind(this)
     );
 
     this.router.get(
-      "/indexes/status",
+      '/indexes/status',
       this.getCollectionsIndexStatus.bind(this)
     );
 
-    this.router.delete("/:name", this.dropCollection.bind(this));
+    this.router.delete('/:name', this.dropCollection.bind(this));
 
-    this.router.get("/:name", this.describeCollection.bind(this));
+    this.router.get('/:name', this.describeCollection.bind(this));
 
-    this.router.put("/:name/load", this.loadCollection.bind(this));
+    this.router.put('/:name/load', this.loadCollection.bind(this));
 
-    this.router.put("/:name/release", this.releaseCollection.bind(this));
+    this.router.put('/:name/release', this.releaseCollection.bind(this));
 
     this.router.post(
-      "/:name/insert",
+      '/:name/insert',
       dtoValidationMiddleware(InsertDataDto),
       this.insert.bind(this)
     );
 
+    // we need use req.body, so we can't use delete here
+    this.router.put('/:name/entities', this.deleteEntities.bind(this));
+
     this.router.post(
-      "/:name/search",
+      '/:name/search',
       dtoValidationMiddleware(VectorSearchDto),
       this.vectorSearch.bind(this)
     );
 
     this.router.post(
-      "/:name/query",
+      '/:name/query',
       dtoValidationMiddleware(QueryDto),
       this.query.bind(this)
     );
 
     this.router.post(
-      "/:name/alias",
+      '/:name/alias',
       dtoValidationMiddleware(CreateAliasDto),
       this.createAlias.bind(this)
     );
@@ -95,7 +84,7 @@ export class CollectionController {
   }
 
   async showCollections(req: Request, res: Response, next: NextFunction) {
-    const type = parseInt("" + req.query?.type, 10);
+    const type = parseInt('' + req.query?.type, 10);
     try {
       const result =
         type === 1
@@ -219,6 +208,20 @@ export class CollectionController {
     }
   }
 
+  async deleteEntities(req: Request, res: Response, next: NextFunction) {
+    const name = req.params?.name;
+    const data = req.body;
+    try {
+      const result = await this.collectionsService.deleteEntities({
+        collection_name: name,
+        ...data,
+      });
+      res.send(result);
+    } catch (error) {
+      next(error);
+    }
+  }
+
   async vectorSearch(req: Request, res: Response, next: NextFunction) {
     const name = req.params?.name;
     const data = req.body;
@@ -248,12 +251,12 @@ export class CollectionController {
         collection_name: name,
         ...data,
       });
-      const queryResultList = result.data;
+      // const queryResultList = result.data;
       const queryResultLength = result.data.length;
-      const startNum = page * limit;
-      const endNum = (page + 1) * limit;
-      const slicedResult = queryResultList.slice(startNum, endNum);
-      result.data = slicedResult;
+      // const startNum = page * limit;
+      // const endNum = (page + 1) * limit;
+      // const slicedResult = queryResultList.slice(startNum, endNum);
+      // result.data = slicedResult;
       res.send({ ...result, limit, page, total: queryResultLength });
     } catch (error) {
       next(error);

+ 15 - 9
express/src/collections/collections.service.ts

@@ -1,4 +1,4 @@
-import { MilvusService } from "../milvus/milvus.service";
+import { MilvusService } from '../milvus/milvus.service';
 import {
   CreateCollectionReq,
   DescribeCollectionReq,
@@ -9,18 +9,19 @@ import {
   LoadCollectionReq,
   ReleaseLoadCollectionReq,
   SearchReq,
-} from "@zilliz/milvus2-sdk-node/dist/milvus/types";
-import { throwErrorFromSDK } from "../utils/Error";
-import { findKeyValue } from "../utils/Helper";
-import { ROW_COUNT } from "../utils/Const";
+} from '@zilliz/milvus2-sdk-node/dist/milvus/types';
+import { throwErrorFromSDK } from '../utils/Error';
+import { findKeyValue } from '../utils/Helper';
+import { ROW_COUNT } from '../utils/Const';
 import {
   AlterAliasReq,
   CreateAliasReq,
   DropAliasReq,
   ShowCollectionsReq,
   ShowCollectionsType,
-} from "@zilliz/milvus2-sdk-node/dist/milvus/types/Collection";
-import { QueryDto } from "./dto";
+} from '@zilliz/milvus2-sdk-node/dist/milvus/types/Collection';
+import { QueryDto } from './dto';
+import { DeleteEntitiesReq } from '@zilliz/milvus2-sdk-node/dist/milvus/types/Data';
 
 export class CollectionsService {
   constructor(private milvusService: MilvusService) {}
@@ -85,6 +86,12 @@ export class CollectionsService {
     return res;
   }
 
+  async deleteEntities(data: DeleteEntitiesReq) {
+    const res = await this.dataManager.deleteEntities(data);
+    throwErrorFromSDK(res.status);
+    return res;
+  }
+
   async vectorSearch(data: SearchReq) {
     const res = await this.dataManager.search(data);
     throwErrorFromSDK(res.status);
@@ -144,7 +151,6 @@ export class CollectionsService {
     if (res.data.length > 0) {
       for (const item of res.data) {
         const { name } = item;
-
         const collectionInfo = await this.describeCollection({
           collection_name: name,
         });
@@ -166,7 +172,7 @@ export class CollectionsService {
         );
 
         const loadedPercentage = !loadCollection
-          ? "-1"
+          ? '-1'
           : loadCollection.loadedPercentage;
 
         data.push({

+ 247 - 0
express/src/collections/swagger.yml

@@ -0,0 +1,247 @@
+paths:
+  /collections:
+    get:
+      tags: 
+        - Collection
+      description: Get all or loaded collection
+      parameters:
+        - in: query
+          name: type
+          type: number
+          description: If type is 1 return loaded collections, otherwise return all collections.
+      responses:
+        200:
+          description: CollectionList
+          schema:
+            type: object
+    post:
+      tags: 
+        - Collection
+      description: Create collection in milvus
+      requestBody:
+        description: Create collection request body
+        required: true
+        content:
+          application/json:
+            schema:
+              $ref: '#/definitions/CreateCollection'
+         
+      responses:
+        200:
+          schema:
+            type: object
+  /collections/statistics:
+    get:
+      tags: 
+        - Collection
+      description: Get all collections statistics like row count
+      responses:
+        200:
+          schema:
+            type: object
+
+  /collections/{name}/statistics:
+    get:
+      tags: 
+        - Collection
+      description: Get single collection statistics like row count
+      parameters:
+        - $ref: '#/definitions/CollectionName'
+
+      responses:
+        200:
+          schema:
+            type: object
+  /collections/indexes/status:
+    get:
+      tags: 
+        - Collection
+      description: Get all collections index status
+
+      responses:
+        200:
+          schema:
+            type: object
+
+  /collections/{name}:
+    delete:
+      tags: 
+        - Collection
+      description: Delete collection by name
+      parameters:
+        - $ref: '#/definitions/CollectionName'
+      responses:
+        200:
+          schema:
+            type: object
+    get:
+      tags: 
+        - Collection
+      description: Get single collection informations like schema, name, id
+      parameters:
+        - $ref: '#/definitions/CollectionName'
+      responses:
+        200:
+          schema:
+            type: object
+  /collections/{name}/load:
+    put:
+      tags: 
+        - Collection
+      description: Load data to cache
+      parameters:
+        - $ref: '#/definitions/CollectionName'
+      responses:
+        200:
+          schema:
+            type: object
+  /collections/{name}/release:
+    put:
+      tags: 
+        - Collection
+      description: Release data from cache
+      parameters:
+        - $ref: '#/definitions/CollectionName'
+      responses:
+        200:
+          schema:
+            type: object
+  /collections/{name}/insert:
+    post:
+      tags: 
+        - Collection
+      description: Insert data into collection
+      parameters:
+        - $ref: '#/definitions/CollectionName'
+      requestBody:
+        description: Insert data into collection
+        required: true
+        content:
+          application/json:
+            schema:
+              $ref: '#/definitions/Insert'
+      responses:
+        200:
+          schema:
+            type: object
+            
+  /collections/{name}/search:
+    post:
+      tags: 
+        - Collection
+      description: Vector search 
+      parameters:
+        - $ref: '#/definitions/CollectionName'
+      requestBody:
+        description: Do vector search
+        required: true
+        content:
+          application/json:
+            schema:
+              $ref: '#/definitions/Search'
+      responses:
+        200:
+          schema:
+            type: object
+
+  /collections/{name}/query:
+    post:
+      tags: 
+        - Collection
+      description: query data
+      parameters:
+        - $ref: '#/definitions/CollectionName'
+      requestBody:
+        description: query data body
+        required: true
+        content:
+          application/json:
+            schema:
+              type: object
+              required:
+                - "expr"
+              properties:
+                expr:
+                  type: string
+                  example: id in [1]
+      responses:
+        200:
+          schema:
+            type: object      
+
+/collections/{name}/alias:
+    post:
+      tags: 
+        - Collection
+      description: Create alias for collection
+      parameters:
+        - $ref: '#/definitions/CollectionName'
+      requestBody:
+        description: alias name
+        required: true
+        content:
+          application/json:
+            schema:
+              type: object
+              required:
+                - "alias"
+              properties:
+                alias:
+                  type: string
+                  example: collection_alias
+      responses:
+        200:
+          schema:
+            type: object                   
+
+
+definitions:
+  CollectionName:
+    in: path
+    name: name
+    type: string
+    description: Collection name
+  CreateCollection:
+    type: object
+    required:
+      - "collection_name"
+      - "fields"
+    properties:
+      collection_name:
+        type: string
+        example: collection_01
+      fields:
+        type: array
+        example: []
+  Insert:
+    type: object
+    required:
+      - "fields_data"
+    properties:
+      partition_name:
+        type: string
+        example: _default
+      fields_data:
+        type: array
+        example: []
+      hash_keys:
+        type: array
+        example: []
+  Search:
+    type: object
+    required:
+      - "vectors"
+      - "vector_type"
+      - "search_params"
+    properties:
+      vectors:
+        type: array
+        example: []
+        
+      vector_type:
+        description: BinaryVector - 100 , FloatVector - 101
+        type: number
+        example: 100 
+      search_params:
+        type: object
+        example: {"anns_field":"","topk":10,"metric_type":"L2","params":""}

+ 28 - 18
express/src/crons/crons.service.ts

@@ -1,7 +1,7 @@
-import { CollectionsService } from "../collections/collections.service";
-import { WS_EVENTS, WS_EVENTS_TYPE } from "../utils/Const";
-import { schedule, ScheduledTask } from "node-cron";
-import { pubSub } from "../events";
+import { CollectionsService } from '../collections/collections.service';
+import { WS_EVENTS, WS_EVENTS_TYPE } from '../utils/Const';
+import { schedule, ScheduledTask } from 'node-cron';
+import { pubSub } from '../events';
 
 export class CronsService {
   constructor(
@@ -11,13 +11,22 @@ export class CronsService {
 
   async toggleCronJobByName(data: { name: string; type: WS_EVENTS_TYPE }) {
     const { name, type } = data;
-    const cronJobEntity = this.schedulerRegistry.getCronJob(name);
-    if (!cronJobEntity && Number(type) === WS_EVENTS_TYPE.START) {
-      return this.getCollections(WS_EVENTS.COLLECTION);
+
+    switch (name) {
+      case WS_EVENTS.COLLECTION:
+        const cronJobEntity = this.schedulerRegistry.getCronJob(name);
+        if (!cronJobEntity && Number(type) === WS_EVENTS_TYPE.START) {
+          return this.getCollections(WS_EVENTS.COLLECTION);
+        }
+        if (!cronJobEntity) {
+          return;
+        }
+        return Number(type) === WS_EVENTS_TYPE.STOP
+          ? cronJobEntity.stop()
+          : cronJobEntity.start();
+      default:
+        throw new Error('Unsupported event type');
     }
-    return Number(type) === WS_EVENTS_TYPE.STOP
-      ? cronJobEntity.stop()
-      : cronJobEntity.start();
   }
 
   async getCollections(name: string) {
@@ -26,17 +35,18 @@ export class CronsService {
         const res = await this.collectionService.getAllCollections();
         // TODO
         // this.eventService.server.emit("COLLECTION", res);
-        pubSub.emit("ws_pubsub", {
-          event: WS_EVENTS.COLLECTION + "",
+        pubSub.emit('ws_pubsub', {
+          event: WS_EVENTS.COLLECTION + '',
           data: res,
         });
         return res;
       } catch (error) {
         // When user not connect milvus, stop cron
-        this.toggleCronJobByName({
-          name: WS_EVENTS.COLLECTION,
-          type: WS_EVENTS_TYPE.STOP,
-        });
+        const cronJobEntity = this.schedulerRegistry.getCronJob(name);
+        if (cronJobEntity) {
+          cronJobEntity.stop();
+        }
+
         throw new Error(error);
       }
     };
@@ -54,7 +64,7 @@ export class SchedulerRegistry {
 
   setCronJobEverySecond(name: string, func: () => {}) {
     // The cron job will run every second
-    this.setCronJob(name, "* * * * * *", func);
+    this.setCronJob(name, '* * * * * *', func);
   }
 
   // ┌────────────── second (optional)
@@ -73,7 +83,7 @@ export class SchedulerRegistry {
       target?.entity?.stop();
     } else {
       const task = schedule(scheduler, () => {
-        console.log(`${name}: running a task every seconds`);
+        console.log(`[Scheduler:${scheduler}] ${name}: running a task.`);
         func();
       });
       this.cronJobList.push({

+ 28 - 0
express/src/crons/swagger.yml

@@ -0,0 +1,28 @@
+paths:
+  /crons:
+    put:
+      tags: 
+        - Crons
+      description: Toggle cronjob status
+      requestBody:
+        description: Cron job name, status(start->0, stop->1)
+        required: true
+        content:
+          application/json:
+            schema:
+              type: object
+              required:
+                - "name"
+              properties:
+                name:
+                  type: string
+                  example: COLLECTION
+                type:
+                  type: number
+                  example: 0
+      responses:
+        200:
+          schema:
+            type: object
+
+ 

+ 9 - 4
express/src/middlewares/validation.ts

@@ -4,15 +4,20 @@ import { validate, ValidationError } from "class-validator";
 import { sanitize } from "class-sanitizer";
 import HttpException from "../exception/HttpException";
 
+/**
+ * Only check for req.body
+ * When use req.query or req.params cant use dto to validate.
+ * Because all datas are string in req.query.
+ * @param type
+ * @param skipMissingProperties
+ * @returns
+ */
 export const dtoValidationMiddleware = (
   type: any,
   skipMissingProperties = false
 ): RequestHandler => {
   return (req, res, next) => {
-    const dtoObj = plainToClass(
-      type,
-      req.method === "GET" || req.method === "DELETE" ? req.query : req.body
-    );
+    const dtoObj = plainToClass(type, req.body);
     validate(dtoObj, { skipMissingProperties }).then(
       (errors: ValidationError[]) => {
         if (errors.length > 0) {

+ 3 - 6
express/src/milvus/milvus.controller.ts

@@ -1,7 +1,7 @@
 import { NextFunction, Request, Response, Router } from "express";
 import { dtoValidationMiddleware } from "../middlewares/validation";
 import { MilvusService } from "./milvus.service";
-import { CheckMilvusDto, ConnectMilvusDto, FlushDto } from "./dto";
+import { ConnectMilvusDto, FlushDto } from "./dto";
 
 export class MilvusController {
   private router: Router;
@@ -23,11 +23,7 @@ export class MilvusController {
       this.connectMilvus.bind(this)
     );
 
-    this.router.get(
-      "/check",
-      dtoValidationMiddleware(CheckMilvusDto),
-      this.checkConnect.bind(this)
-    );
+    this.router.get("/check", this.checkConnect.bind(this));
 
     this.router.put(
       "/flush",
@@ -44,6 +40,7 @@ export class MilvusController {
     const address = req.body?.address;
     try {
       const result = await this.milvusService.connectMilvus(address);
+
       res.send(result);
     } catch (error) {
       next(error);

+ 7 - 7
express/src/milvus/milvus.service.ts

@@ -1,8 +1,8 @@
-import { MilvusClient } from "@zilliz/milvus2-sdk-node";
+import { MilvusClient } from '@zilliz/milvus2-sdk-node';
 import {
   FlushReq,
   GetMetricsResponse,
-} from "@zilliz/milvus2-sdk-node/dist/milvus/types";
+} from '@zilliz/milvus2-sdk-node/dist/milvus/types';
 
 export class MilvusService {
   private milvusAddress: string;
@@ -44,17 +44,17 @@ export class MilvusService {
 
   private checkMilvus() {
     if (!this.milvusClient) {
-      throw new Error("Please connect milvus first");
+      throw new Error('Please connect milvus first');
     }
   }
 
   async connectMilvus(address: string) {
     // grpc only need address without http
-    const milvusAddress = address.replace(/(http|https):\/\//, "");
+    const milvusAddress = address.replace(/(http|https):\/\//, '');
     try {
       this.milvusClient = new MilvusClient(milvusAddress);
       await this.milvusClient.collectionManager.hasCollection({
-        collection_name: "not_exist",
+        collection_name: 'not_exist',
       });
       this.milvusAddress = address;
       this.milvusClients = {
@@ -62,7 +62,7 @@ export class MilvusService {
       };
       return { address: this.milvusAddress };
     } catch (error) {
-      throw new Error("Connect milvus failed, check your milvus address.");
+      throw new Error('Connect milvus failed, check your milvus address.');
     }
   }
 
@@ -94,7 +94,7 @@ export class MilvusService {
 
   async getMetrics(): Promise<GetMetricsResponse> {
     const res = await this.milvusClient.dataManager.getMetric({
-      request: { metric_type: "system_info" },
+      request: { metric_type: 'system_info' },
     });
     return res;
   }

+ 70 - 0
express/src/milvus/swagger.yml

@@ -0,0 +1,70 @@
+paths:
+  /milvus/connect:
+    post:
+      tags: 
+        - Milvus
+      description: Connect milvus
+      requestBody:
+        description: Milvus address 
+        required: true
+        content:
+          application/json:
+            schema:
+              type: object
+              required:
+                - "address"
+              properties:
+                address:
+                  type: string
+                  example: 127.0.0.1:19530
+      responses:
+        200:
+          schema:
+            type: object
+
+  /milvus/check:
+    get:
+      tags: 
+        - Milvus
+      description: Check milvus alive or not.
+      parameters:
+        - in: query
+          name: address
+          description: Milvus address
+      responses:
+        200:
+          schema:
+            type: object
+
+  /milvus/metrics:
+    get:
+      tags: 
+        - Milvus
+      description: Get milvus metrics     
+      responses:
+        200:
+          schema:
+            type: object
+
+  /milvus/flush:
+    post:
+      tags: 
+        - Milvus
+      description: Flush data in milvus
+      requestBody:
+        description: The collection names you want to flush
+        required: true
+        content:
+          application/json:
+            schema:
+              type: object
+              required:
+                - "collection_names"
+              properties:
+                collection_names:
+                  type: array
+                  example: [collectionName]
+      responses:
+        200:
+          schema:
+            type: object

+ 17 - 21
express/src/partitions/partitions.controller.ts

@@ -1,13 +1,13 @@
-import { NextFunction, Request, Response, Router } from "express";
-import { dtoValidationMiddleware } from "../middlewares/validation";
-import { PartitionsService } from "./partitions.service";
-import { milvusService } from "../milvus";
+import { NextFunction, Request, Response, Router } from 'express';
+import { dtoValidationMiddleware } from '../middlewares/validation';
+import { PartitionsService } from './partitions.service';
+import { milvusService } from '../milvus';
 
 import {
   GetPartitionsInfoDto,
   ManagePartitionDto,
   LoadPartitionsDto,
-} from "./dto";
+} from './dto';
 
 export class PartitionController {
   private router: Router;
@@ -19,26 +19,22 @@ export class PartitionController {
   }
 
   generateRoutes() {
-    this.router.get(
-      "/",
-      dtoValidationMiddleware(GetPartitionsInfoDto),
-      this.getPatitionsInfo.bind(this)
-    );
+    this.router.get('/', this.getPartitionsInfo.bind(this));
 
     this.router.post(
-      "/",
+      '/',
       dtoValidationMiddleware(ManagePartitionDto),
       this.managePartition.bind(this)
     );
 
-    this.router.post(
-      "/load",
+    this.router.put(
+      '/load',
       dtoValidationMiddleware(LoadPartitionsDto),
       this.loadPartition.bind(this)
     );
 
-    this.router.post(
-      "/release",
+    this.router.put(
+      '/release',
       dtoValidationMiddleware(LoadPartitionsDto),
       this.releasePartition.bind(this)
     );
@@ -46,10 +42,10 @@ export class PartitionController {
     return this.router;
   }
 
-  async getPatitionsInfo(req: Request, res: Response, next: NextFunction) {
-    const collectionName = "" + req.query?.collection_name;
+  async getPartitionsInfo(req: Request, res: Response, next: NextFunction) {
+    const collectionName = '' + req.query?.collection_name;
     try {
-      const result = await this.partitionsService.getPatitionsInfo({
+      const result = await this.partitionsService.getPartitionsInfo({
         collection_name: collectionName,
       });
       res.send(result);
@@ -62,9 +58,9 @@ export class PartitionController {
     const { type, ...params } = req.body;
     try {
       const result =
-        type.toLocaleLowerCase() === "create"
-          ? await this.partitionsService.createParition(params)
-          : await this.partitionsService.deleteParition(params);
+        type.toLocaleLowerCase() === 'create'
+          ? await this.partitionsService.createPartition(params)
+          : await this.partitionsService.deletePartition(params);
       res.send(result);
     } catch (error) {
       next(error);

+ 4 - 4
express/src/partitions/partitions.service.ts

@@ -18,7 +18,7 @@ export class PartitionsService {
     return this.milvusService.partitionManager;
   }
 
-  async getPatitionsInfo(data: ShowPartitionsReq) {
+  async getPartitionsInfo(data: ShowPartitionsReq) {
     const result = [];
     const res = await this.getPartitions(data);
     if (res.partition_names && res.partition_names.length) {
@@ -44,13 +44,13 @@ export class PartitionsService {
     return res;
   }
 
-  async createParition(data: CreatePartitionReq) {
+  async createPartition(data: CreatePartitionReq) {
     const res = await this.partitionManager.createPartition(data);
     throwErrorFromSDK(res);
     return res;
   }
 
-  async deleteParition(data: DropPartitionReq) {
+  async deletePartition(data: DropPartitionReq) {
     const res = await this.partitionManager.dropPartition(data);
     throwErrorFromSDK(res);
     return res;
@@ -73,4 +73,4 @@ export class PartitionsService {
     throwErrorFromSDK(res);
     return res;
   }
-}
+}

+ 97 - 0
express/src/partitions/swagger.yml

@@ -0,0 +1,97 @@
+paths:
+  /partitions:
+    get:
+      tags: 
+        - Partition
+      description: Get all partitions information
+      parameters:
+        - in: query
+          name: collection_name
+          type: string
+          description: Collection name
+      responses:
+        200:
+          schema:
+            type: object
+  
+    post:
+      tags: 
+        - Partition
+      description: Create or delete partition excepte _default
+      requestBody:
+        description: Manage partition req body
+        required: true
+        content:
+          application/json:
+            schema:
+              type: object
+              required:
+                - type
+                - collection_name
+                - partition_name
+              properties:
+                type:
+                  type: string
+                  example: create
+                collection_name:
+                  type: string
+                  example: ''
+                partition_name:
+                  type: string
+                  example: ''
+      responses:
+        200:
+          schema:
+            type: object
+
+  /partitions/load:
+    put:
+      tags: 
+        - Partition
+      description: Load partition to cache
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              type: object
+              required:
+                - collection_name
+                - partition_name
+              properties:
+                collection_name:
+                  type: string
+                  example: ''
+                partition_names:
+                  type: array
+                  example: []
+      responses:
+        200:
+          schema:
+            type: object
+  /partitions/release:
+    put:
+      tags: 
+        - Partition
+      description: Release partition from cache
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              type: object
+              required:
+                - collection_name
+                - partition_name
+              properties:
+                collection_name:
+                  type: string
+                  example: ''
+                partition_names:
+                  type: array
+                  example: []
+      responses:
+        200:
+          schema:
+            type: object
+ 

+ 4 - 21
express/src/schema/schema.controller.ts

@@ -3,12 +3,7 @@ import { dtoValidationMiddleware } from "../middlewares/validation";
 import { SchemaService } from "./schema.service";
 import { milvusService } from "../milvus";
 
-import {
-  ManageIndexDto,
-  DescribeIndexDto,
-  GetIndexProgressDto,
-  GetIndexStateDto,
-} from "./dto";
+import { ManageIndexDto } from "./dto";
 
 export class SchemaController {
   private router: Router;
@@ -26,23 +21,11 @@ export class SchemaController {
       this.manageIndex.bind(this)
     );
 
-    this.router.get(
-      "/index",
-      dtoValidationMiddleware(DescribeIndexDto),
-      this.describeIndex.bind(this)
-    );
+    this.router.get("/index", this.describeIndex.bind(this));
 
-    this.router.post(
-      "/index/progress",
-      dtoValidationMiddleware(GetIndexProgressDto),
-      this.getIndexBuildProgress.bind(this)
-    );
+    this.router.get("/index/progress", this.getIndexBuildProgress.bind(this));
 
-    this.router.post(
-      "/index/state",
-      dtoValidationMiddleware(GetIndexStateDto),
-      this.getIndexState.bind(this)
-    );
+    this.router.get("/index/state", this.getIndexState.bind(this));
 
     return this.router;
   }

+ 78 - 0
express/src/schema/swagger.yml

@@ -0,0 +1,78 @@
+paths:
+  /schema/index:
+    get:
+      tags: 
+        - Schema
+      description: Get index information
+      parameters:
+        - $ref: '#/definitions/CollectionName'
+      responses:
+        200:
+          schema:
+            type: object
+  
+    post:
+      tags: 
+        - Schema
+      description: Create or delete index in collection
+      requestBody:
+        description: Only type is create need extra_params
+        required: true
+        content:
+          application/json:
+            schema:
+              type: object
+              required:
+                - type
+                - collection_name
+                - field_name
+              properties:
+                type:
+                  type: string
+                  example: create
+                collection_name:
+                  type: string
+                  example: ''
+                field_name:
+                  type: string
+                  example: 'vector field'
+                extra_params:
+                  type: object
+                  example: {"index_type":"","metric_type":"","params":""}
+      responses:
+        200:
+          schema:
+            type: object
+
+  /schema/index/progress:
+    get:
+      tags: 
+        - Schema
+      description: Get index building progress percentage
+      parameters:
+        - $ref: '#/definitions/CollectionName'
+      responses:
+        200:
+          schema:
+            type: object
+
+  /schema/index/state:
+    get:
+      tags: 
+        - Schema
+      description: Get index state 
+      parameters:
+        - $ref: '#/definitions/CollectionName'
+      responses:
+        200:
+          schema:
+            type: object
+ 
+
+
+definitions:
+  CollectionName:
+    in: query
+    name: collection_name
+    type: string
+    description: Collection name

+ 1 - 1
express/src/swagger.ts

@@ -15,7 +15,7 @@ export const surveSwaggerSpecification = () => {
       },
       servers: [{ url: "/api/v1" }],
     },
-    apis: ["./src/**/*.ts"],
+    apis: ["./src/**/*.yml"],
   };
   const swaggerSpec = swaggerJsdoc(options);
 

Some files were not shown because too many files changed in this diff