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:
         with:
           node-version: 12
           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
       - name: Login to DockerHub
         uses: docker/login-action@v1
         uses: docker/login-action@v1
         with:
         with:

+ 2 - 0
README.md

@@ -1,6 +1,8 @@
 # Milvus insight
 # Milvus insight
+
 [![typescript](https://badges.aleen42.com/src/typescript.svg)](https://badges.aleen42.com/src/typescript.svg)
 [![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)
 [![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.
 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
       ├── hooks                   # React hooks
       ├── http                    # Http request api. And we have http interceptor in GlobalEffect.tsx file
       ├── http                    # Http request api. And we have http interceptor in GlobalEffect.tsx file
       ├── i18n                    # Language i18n
       ├── 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.
       ├── router                  # React router, control the page auth.
       ├── styles                  # Styles, normally we use material to control styles.
       ├── styles                  # Styles, normally we use material to control styles.
       ├── types                   # Global types
       ├── 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.
 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
 ### Icon
 
 
 We put all icons in components/icons file. Normally we use material 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.
 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",
   "bugs": "https://github.com/milvus-io/milvus-insight/issues",
   "private": true,
   "private": true,
   "dependencies": {
   "dependencies": {
+    "@date-io/dayjs": "1.x",
     "@loadable/component": "^5.15.0",
     "@loadable/component": "^5.15.0",
     "@material-ui/core": "4.11.4",
     "@material-ui/core": "4.11.4",
     "@material-ui/icons": "^4.11.2",
     "@material-ui/icons": "^4.11.2",
     "@material-ui/lab": "4.0.0-alpha.58",
     "@material-ui/lab": "4.0.0-alpha.58",
+    "@material-ui/pickers": "^3.3.10",
     "@mui/x-data-grid": "^4.0.0",
     "@mui/x-data-grid": "^4.0.0",
     "@testing-library/jest-dom": "^5.11.4",
     "@testing-library/jest-dom": "^5.11.4",
     "@testing-library/react": "^11.1.0",
     "@testing-library/react": "^11.1.0",
@@ -24,6 +26,7 @@
     "@types/react-syntax-highlighter": "^13.5.2",
     "@types/react-syntax-highlighter": "^13.5.2",
     "axios": "^0.21.3",
     "axios": "^0.21.3",
     "dayjs": "^1.10.5",
     "dayjs": "^1.10.5",
+    "file-saver": "^2.0.5",
     "i18next": "^20.3.1",
     "i18next": "^20.3.1",
     "papaparse": "^5.3.1",
     "papaparse": "^5.3.1",
     "react": "^17.0.2",
     "react": "^17.0.2",
@@ -74,6 +77,7 @@
   },
   },
   "devDependencies": {
   "devDependencies": {
     "@testing-library/react-hooks": "^7.0.1",
     "@testing-library/react-hooks": "^7.0.1",
+    "@types/file-saver": "^2.0.4",
     "@types/loadable__component": "^5.13.4",
     "@types/loadable__component": "^5.13.4",
     "@types/webpack-env": "^1.16.3",
     "@types/webpack-env": "^1.16.3",
     "prettier": "2.3.2"
     "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 Router from './router/Router';
 import { RootProvider } from './context/Root';
 import { RootProvider } from './context/Root';
 import { NavProvider } from './context/Navigation';
 import { NavProvider } from './context/Navigation';
@@ -10,7 +12,9 @@ function App() {
       <AuthProvider>
       <AuthProvider>
         <WebSocketProvider>
         <WebSocketProvider>
           <NavProvider>
           <NavProvider>
-            <Router></Router>
+            <MuiPickersUtilsProvider utils={DayjsUtils}>
+              <Router></Router>
+            </MuiPickersUtilsProvider>
           </NavProvider>
           </NavProvider>
         </WebSocketProvider>
         </WebSocketProvider>
       </AuthProvider>
       </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 {
 import {
   makeStyles,
   makeStyles,
   Theme,
   Theme,
@@ -9,39 +9,8 @@ import {
 import CloseIcon from '@material-ui/icons/Close';
 import CloseIcon from '@material-ui/icons/Close';
 import { ConditionProps, Field } from './Types';
 import { ConditionProps, Field } from './Types';
 import CustomSelector from '../customSelector/CustomSelector';
 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 Condition: FC<ConditionProps> = props => {
   const {
   const {
@@ -54,9 +23,9 @@ const Condition: FC<ConditionProps> = props => {
     ...others
     ...others
   } = props;
   } = props;
   const [operator, setOperator] = useState(
   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] || {}
     initData?.field || fields[0] || {}
   );
   );
   const [conditionValue, setConditionValue] = useState(initData?.value || '');
   const [conditionValue, setConditionValue] = useState(initData?.value || '');
@@ -80,16 +49,26 @@ const Condition: FC<ConditionProps> = props => {
     const conditionValueWithNoSpace = conditionValue.replaceAll(' ', '');
     const conditionValueWithNoSpace = conditionValue.replaceAll(' ', '');
 
 
     switch (type) {
     switch (type) {
-      case 'int':
+      case DataTypeStringEnum.Int8:
+      case DataTypeStringEnum.Int16:
+      case DataTypeStringEnum.Int32:
+      case DataTypeStringEnum.Int64:
+        // case DataTypeStringEnum:
         isLegal = isIn
         isLegal = isIn
           ? regIntInterval.test(conditionValueWithNoSpace)
           ? regIntInterval.test(conditionValueWithNoSpace)
           : regInt.test(conditionValueWithNoSpace);
           : regInt.test(conditionValueWithNoSpace);
         break;
         break;
-      case 'float':
+      case DataTypeStringEnum.Float:
+      case DataTypeStringEnum.Double:
+      case DataTypeStringEnum.FloatVector:
         isLegal = isIn
         isLegal = isIn
           ? regFloatInterval.test(conditionValueWithNoSpace)
           ? regFloatInterval.test(conditionValueWithNoSpace)
           : regFloat.test(conditionValueWithNoSpace);
           : regFloat.test(conditionValueWithNoSpace);
         break;
         break;
+      case DataTypeStringEnum.Bool:
+        const legalValues = ['false', 'true'];
+        isLegal = legalValues.includes(conditionValueWithNoSpace);
+        break;
       default:
       default:
         isLegal = false;
         isLegal = false;
         break;
         break;
@@ -108,6 +87,16 @@ const Condition: FC<ConditionProps> = props => {
 
 
   const classes = useStyles();
   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.
   // Logic operator input change.
   const handleOpChange = (event: React.ChangeEvent<{ value: unknown }>) => {
   const handleOpChange = (event: React.ChangeEvent<{ value: unknown }>) => {
     setOperator(event.target.value);
     setOperator(event.target.value);
@@ -140,7 +129,7 @@ const Condition: FC<ConditionProps> = props => {
         label="Logic"
         label="Logic"
         value={operator}
         value={operator}
         onChange={handleOpChange}
         onChange={handleOpChange}
-        options={LogicalOperators}
+        options={logicalOperators}
         variant="filled"
         variant="filled"
         wrapperClass={classes.logic}
         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 {
 export interface ConditionProps {
   others?: object;
   others?: object;
@@ -12,7 +12,7 @@ export interface ConditionProps {
 
 
 export interface Field {
 export interface Field {
   name: string;
   name: string;
-  type: 'int' | 'float';
+  type: DataTypeStringEnum;
 }
 }
 
 
 export interface TriggerChangeData {
 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 CachedIcon from '@material-ui/icons/Cached';
 import FilterListIcon from '@material-ui/icons/FilterList';
 import FilterListIcon from '@material-ui/icons/FilterList';
 import AlternateEmailIcon from '@material-ui/icons/AlternateEmail';
 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 { SvgIcon } from '@material-ui/core';
 import { ReactComponent as MilvusIcon } from '../../assets/icons/milvus.svg';
 import { ReactComponent as MilvusIcon } from '../../assets/icons/milvus.svg';
 import { ReactComponent as OverviewIcon } from '../../assets/icons/overview.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} />,
   refresh: (props = {}) => <CachedIcon {...props} />,
   filter: (props = {}) => <FilterListIcon {...props} />,
   filter: (props = {}) => <FilterListIcon {...props} />,
   alias: (props = {}) => <AlternateEmailIcon {...props} />,
   alias: (props = {}) => <AlternateEmailIcon {...props} />,
+  datePicker: (props = {}) => <DatePicker {...props} />,
+  download: (props = {}) => <GetAppIcon {...props} />,
 
 
   milvus: (props = {}) => (
   milvus: (props = {}) => (
     <SvgIcon viewBox="0 0 44 31" component={MilvusIcon} {...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'
   | 'refresh'
   | 'filter'
   | 'filter'
   | 'copyExpression'
   | '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 { useHistory, useLocation } from 'react-router-dom';
 import { authContext } from '../../context/Auth';
 import { authContext } from '../../context/Auth';
 import { IconsType } from '../icons/Types';
 import { IconsType } from '../icons/Types';
+import loadable from '@loadable/component';
 
 
 const PLUGIN_DEV = process.env?.REACT_APP_PLUGIN_DEV;
 const PLUGIN_DEV = process.env?.REACT_APP_PLUGIN_DEV;
 
 
@@ -81,16 +82,16 @@ const Layout = (props: any) => {
       label: navTrans('collection'),
       label: navTrans('collection'),
       onClick: () => history.push('/collections'),
       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) => {
     r.keys().forEach((key: any) => {
       const content = r(key);
       const content = r(key);
       const pathName = content.client?.path;
       const pathName = content.client?.path;
@@ -99,11 +100,18 @@ const Layout = (props: any) => {
         icon: icons.navOverview,
         icon: icons.navOverview,
         label: content.client?.label || 'PLGUIN',
         label: content.client?.label || 'PLGUIN',
       };
       };
-      result.onClick = () => history.push(`${pathName}`);
+      result.onClick = () => history.push(`/${pathName}`);
       const iconName: IconsType = content.client?.iconName;
       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) {
       if (iconName) {
-        // TODO: support custom icon
         result.icon = icons[iconName];
         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 &&
       content.client?.iconActiveClass &&
         (result.iconActiveClass = content.client?.iconActiveClass);
         (result.iconActiveClass = content.client?.iconActiveClass);
@@ -114,7 +122,7 @@ const Layout = (props: any) => {
   }
   }
   importAll(require.context('../../plugins', true, /config\.json$/));
   importAll(require.context('../../plugins', true, /config\.json$/));
   PLUGIN_DEV &&
   PLUGIN_DEV &&
-    importAll(require.context('all_plugins/', true, /config\.json$/));
+    importAll(require.context('all_plugins/', true, /config\.json$/), true);
 
 
   return (
   return (
     <div className={classes.root}>
     <div className={classes.root}>

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

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

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

@@ -206,3 +206,6 @@ export enum LOADING_STATE {
   LOADING,
   LOADING,
   UNLOADED,
   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,
   m: 1024 * 1024,
   g: 1024 * 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 = (
 export const useNavigationHook = (
   type: ALL_ROUTER_TYPES,
   type: ALL_ROUTER_TYPES,
   extraParam?: {
   extraParam?: {
-    collectionName: string;
+    collectionName?: string;
+    title?: string;
   }
   }
 ) => {
 ) => {
   const { t: navTrans } = useTranslation('nav');
   const { t: navTrans } = useTranslation('nav');
   const { setNavInfo } = useContext(navContext);
   const { setNavInfo } = useContext(navContext);
-  const { collectionName } = extraParam || { collectionName: '' };
+  const { collectionName = '', title = 'PLUGIN TITLE' } = extraParam || {
+    collectionName: '',
+  };
 
 
   useEffect(() => {
   useEffect(() => {
     switch (type) {
     switch (type) {
@@ -55,8 +58,16 @@ export const useNavigationHook = (
         setNavInfo(navInfo);
         setNavInfo(navInfo);
         break;
         break;
       }
       }
+      case ALL_ROUTER_TYPES.PLUGIN: {
+        const navInfo: NavInfo = {
+          navTitle: title,
+          backPath: '',
+        };
+        setNavInfo(navInfo);
+        break;
+      }
       default:
       default:
         break;
         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 { 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 { Field } from '../pages/schema/Types';
-import { VectorSearchParam } from '../pages/seach/Types';
+import { VectorSearchParam } from '../types/SearchTypes';
 import { QueryParam } from '../pages/query/Types';
 import { QueryParam } from '../pages/query/Types';
 import { IndexState, ShowCollectionsType } from '../types/Milvus';
 import { IndexState, ShowCollectionsType } from '../types/Milvus';
 import { formatNumber } from '../utils/Common';
 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) {
   static vectorSearchData(collectionName: string, params: VectorSearchParam) {
     return super.query({
     return super.query({
       path: `${this.COLLECTIONS_URL}/${collectionName}/search`,
       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 { FieldData } from '../pages/schema/Types';
 import BaseModel from './BaseModel';
 import BaseModel from './BaseModel';
 
 
 export class FieldHttp extends BaseModel implements FieldData {
 export class FieldHttp extends BaseModel implements FieldData {
-  data_type!: DataType;
+  data_type!: DataTypeStringEnum;
   fieldID!: string;
   fieldID!: string;
   type_params!: { key: string; value: string }[];
   type_params!: { key: string; value: string }[];
   is_primary_key!: true;
   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.',
   deleteTooltip: 'Please select at least one item to delete.',
   alias: 'alias',
   alias: 'alias',
   aliasTooltip: 'Please select one collection to create alias',
   aliasTooltip: 'Please select one collection to create alias',
+  download: 'Download',
+  downloadTooltip: 'Download all query results',
 
 
   collection: 'Collection',
   collection: 'Collection',
+  entites: 'entites',
 
 
   // table
   // table
   id: 'ID',
   id: 'ID',
@@ -54,6 +57,7 @@ const collectionTrans = {
   // delete dialog
   // delete dialog
   deleteWarning:
   deleteWarning:
     'You are trying to delete a collection with data. This action cannot be undone.',
     '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
   // collection tabs
   partitionTab: 'Partitions',
   partitionTab: 'Partitions',

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

@@ -38,6 +38,20 @@ const commonTrans = {
     join: 'Join our growing social community today',
     join: 'Join our growing social community today',
     get: 'Get insight, tips and share ideas',
     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;
 export default commonTrans;

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

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

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

@@ -2,8 +2,9 @@ const systemViewTrans = {
   diskTitle: 'disk',
   diskTitle: 'disk',
   memoryTitle: 'memory',
   memoryTitle: 'memory',
   qpsTitle: 'qps',
   qpsTitle: 'qps',
-  letencyTitle: 'letency',
+  latencyTitle: 'latency',
   hardwareTitle: 'hardware',
   hardwareTitle: 'hardware',
+  configTitle: 'config',
   valueTitle: 'value',
   valueTitle: 'value',
   systemTitle: 'system',
   systemTitle: 'system',
   thName: 'Node Name',
   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.',
   deleteTooltip: 'Please select at least one item to delete.',
   alias: 'alias',
   alias: 'alias',
   aliasTooltip: 'Please select one collection to create alias',
   aliasTooltip: 'Please select one collection to create alias',
+  download: 'Download',
+  downloadTooltip: 'Download all query results',
 
 
   collection: 'Collection',
   collection: 'Collection',
+  entites: 'entites',
 
 
   // table
   // table
   id: 'ID',
   id: 'ID',
@@ -52,8 +55,8 @@ const collectionTrans = {
   releaseConfirmLabel: 'Release',
   releaseConfirmLabel: 'Release',
 
 
   // delete dialog
   // 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
   // collection tabs
   partitionTab: 'Partitions',
   partitionTab: 'Partitions',

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

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

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

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

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

@@ -2,8 +2,9 @@ const systemViewTrans = {
   diskTitle: 'disk',
   diskTitle: 'disk',
   memoryTitle: 'memory',
   memoryTitle: 'memory',
   qpsTitle: 'qps',
   qpsTitle: 'qps',
-  letencyTitle: 'letency',
+  latencyTitle: 'latency',
   hardwareTitle: 'hardware',
   hardwareTitle: 'hardware',
+  configTitle: 'config',
   valueTitle: 'value',
   valueTitle: 'value',
   systemTitle: 'system',
   systemTitle: 'system',
   thName: 'Node Name',
   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 { webSokcetContext } from '../../context/WebSocket';
 import { WS_EVENTS, WS_EVENTS_TYPE } from '../../consts/Http';
 import { WS_EVENTS, WS_EVENTS_TYPE } from '../../consts/Http';
 import { checkIndexBuilding, checkLoading } from '../../utils/Validation';
 import { checkIndexBuilding, checkLoading } from '../../utils/Validation';
-import CreateAlias from './CreateAlias';
+// import CreateAlias from './CreateAlias';
 
 
 const useStyles = makeStyles((theme: Theme) => ({
 const useStyles = makeStyles((theme: Theme) => ({
   emptyWrapper: {
   emptyWrapper: {
@@ -309,29 +309,30 @@ const Collections = () => {
       disabledTooltip: collectionTrans('deleteTooltip'),
       disabledTooltip: collectionTrans('deleteTooltip'),
       disabled: data => data.length === 0,
       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',
       label: 'Search',
       icon: 'search',
       icon: 'search',

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

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

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

@@ -32,6 +32,7 @@ export interface CollectionCreateParam {
 }
 }
 
 
 export enum DataTypeEnum {
 export enum DataTypeEnum {
+  Bool = 1,
   Int8 = 2,
   Int8 = 2,
   Int16 = 3,
   Int16 = 3,
   Int32 = 4,
   Int32 = 4,
@@ -41,16 +42,17 @@ export enum DataTypeEnum {
   BinaryVector = 100,
   BinaryVector = 100,
   FloatVector = 101,
   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 {
 export interface Field {
   name: string | null;
   name: string | null;
@@ -91,3 +93,8 @@ export interface InsertDataParam {
   // e.g. [{vector: [1,2,3], age: 10}]
   // e.g. [{vector: [1,2,3], age: 10}]
   fields_data: any[];
   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 { useTranslation } from 'react-i18next';
 
 
+import { rootContext } from '../../context/Root';
+
 import EmptyCard from '../../components/cards/EmptyCard';
 import EmptyCard from '../../components/cards/EmptyCard';
 import icons from '../../components/icons/Icons';
 import icons from '../../components/icons/Icons';
 import CustomButton from '../../components/customButton/CustomButton';
 import CustomButton from '../../components/customButton/CustomButton';
 import MilvusGrid from '../../components/grid/Grid';
 import MilvusGrid from '../../components/grid/Grid';
+import { ToolBarConfig } from '../../components/grid/Types';
 import { getQueryStyles } from './Styles';
 import { getQueryStyles } from './Styles';
 import Filter from '../../components/advancedSearch';
 import Filter from '../../components/advancedSearch';
 import { CollectionHttp } from '../../http/Collection';
 import { CollectionHttp } from '../../http/Collection';
 import { FieldHttp } from '../../http/Field';
 import { FieldHttp } from '../../http/Field';
 import { usePaginationHook } from '../../hooks/Pagination';
 import { usePaginationHook } from '../../hooks/Pagination';
+// import { useTimeTravelHook } from '../../hooks/TimeTravel';
+
 import CopyButton from '../../components/advancedSearch/CopyButton';
 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<{
 const Query: FC<{
   collectionName: string;
   collectionName: string;
@@ -19,10 +30,16 @@ const Query: FC<{
   const [expression, setExpression] = useState('');
   const [expression, setExpression] = useState('');
   const [tableLoading, setTableLoading] = useState<any>();
   const [tableLoading, setTableLoading] = useState<any>();
   const [queryResult, setQueryResult] = 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 VectorSearchIcon = icons.vectorSearch;
   const ResetIcon = icons.refresh;
   const ResetIcon = icons.refresh;
 
 
+  const { t: dialogTrans } = useTranslation('dialog');
+  const { t: successTrans } = useTranslation('success');
   const { t: searchTrans } = useTranslation('search');
   const { t: searchTrans } = useTranslation('search');
   const { t: collectionTrans } = useTranslation('collection');
   const { t: collectionTrans } = useTranslation('collection');
   const { t: btnTrans } = useTranslation('btn');
   const { t: btnTrans } = useTranslation('btn');
@@ -31,6 +48,9 @@ const Query: FC<{
 
 
   const classes = getQueryStyles();
   const classes = getQueryStyles();
 
 
+  // const { timeTravel, setTimeTravel, timeTravelInfo, handleDateTimeChange } =
+  //   useTimeTravelHook();
+
   // Format result list
   // Format result list
   const queryResultMemo = useMemo(
   const queryResultMemo = useMemo(
     () =>
     () =>
@@ -39,7 +59,7 @@ const Query: FC<{
         const tmp = Object.keys(resultItem).reduce(
         const tmp = Object.keys(resultItem).reduce(
           (prev: { [key: string]: any }, item: string) => {
           (prev: { [key: string]: any }, item: string) => {
             if (Array.isArray(resultItem[item])) {
             if (Array.isArray(resultItem[item])) {
-              const list2Str = `[${resultItem[item]}]`;
+              const list2Str = JSON.stringify(resultItem[item]);
               prev[item] = (
               prev[item] = (
                 <div className={classes.vectorTableCell}>
                 <div className={classes.vectorTableCell}>
                   <div>{list2Str}</div>
                   <div>{list2Str}</div>
@@ -51,7 +71,7 @@ const Query: FC<{
                 </div>
                 </div>
               );
               );
             } else {
             } else {
-              prev[item] = resultItem[item];
+              prev[item] = `${resultItem[item]}`;
             }
             }
             return prev;
             return prev;
           },
           },
@@ -62,6 +82,14 @@ const Query: FC<{
     [queryResult, classes.vectorTableCell, classes.copyBtn, copyTrans.label]
     [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 {
   const {
     pageSize,
     pageSize,
     handlePageSize,
     handlePageSize,
@@ -80,11 +108,18 @@ const Query: FC<{
 
 
   const getFields = async (collectionName: string) => {
   const getFields = async (collectionName: string) => {
     const schemaList = await FieldHttp.getFields(collectionName);
     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.
   // Get fields at first or collection name changed.
@@ -100,17 +135,21 @@ const Query: FC<{
     setExpression('');
     setExpression('');
     setTableLoading(null);
     setTableLoading(null);
     setQueryResult(null);
     setQueryResult(null);
+    handleCurrentPage(0);
   };
   };
+
   const handleFilterSubmit = (expression: string) => {
   const handleFilterSubmit = (expression: string) => {
     setExpression(expression);
     setExpression(expression);
     setQueryResult(null);
     setQueryResult(null);
   };
   };
+
   const handleQuery = async () => {
   const handleQuery = async () => {
     setTableLoading(true);
     setTableLoading(true);
     try {
     try {
       const res = await CollectionHttp.queryData(collectionName, {
       const res = await CollectionHttp.queryData(collectionName, {
         expr: expression,
         expr: expression,
         output_fields: fields.map(i => i.name),
         output_fields: fields.map(i => i.name),
+        // travel_timestamp: timeTravelInfo.timestamp,
       });
       });
       const result = res.data;
       const result = res.data;
       setQueryResult(result);
       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 (
   return (
     <div className={classes.root}>
     <div className={classes.root}>
+      <CustomToolBar toolbarConfigs={toolbarConfigs} />
       <div className={classes.toolbar}>
       <div className={classes.toolbar}>
         <div className="left">
         <div className="left">
-          <div>{`${
-            expression || collectionTrans('exprPlaceHolder')
-          }`}</div>
+          {/* <div className="expression"> */}
+          <div>{`${expression || collectionTrans('exprPlaceHolder')}`}</div>
           <Filter
           <Filter
             ref={filterRef}
             ref={filterRef}
             title="Advanced Filter"
             title="Advanced Filter"
-            fields={fields}
+            fields={fields.filter(
+              i =>
+                i.type !== DataTypeStringEnum.FloatVector &&
+                i.type !== DataTypeStringEnum.BinaryVector
+            )}
             filterDisabled={false}
             filterDisabled={false}
             onSubmit={handleFilterSubmit}
             onSubmit={handleFilterSubmit}
             showTitle={false}
             showTitle={false}
             showTooltip={false}
             showTooltip={false}
           />
           />
+          {/* </div> */}
+
+          {/* <CustomDatePicker
+            label={timeTravelInfo.label}
+            onChange={handleDateTimeChange}
+            date={timeTravel}
+            setDate={setTimeTravel}
+          /> */}
         </div>
         </div>
         <div className="right">
         <div className="right">
           <CustomButton className="btn" onClick={handleFilterReset}>
           <CustomButton className="btn" onClick={handleFilterReset}>
@@ -162,10 +269,12 @@ const Query: FC<{
             label: i.name,
             label: i.name,
           }))}
           }))}
           primaryKey={fields.find(i => i.is_primary_key)?.name}
           primaryKey={fields.find(i => i.is_primary_key)?.name}
-          openCheckBox={false}
+          openCheckBox={true}
           isLoading={!!tableLoading}
           isLoading={!!tableLoading}
           rows={result}
           rows={result}
           rowCount={total}
           rowCount={total}
+          selected={selectedDatas}
+          setSelected={handleSelectChange}
           page={currentPage}
           page={currentPage}
           onChangePage={handlePageChange}
           onChangePage={handlePageChange}
           rowsPerPage={pageSize}
           rowsPerPage={pageSize}

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

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

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

@@ -2,4 +2,5 @@ export interface QueryParam {
   expr: string;
   expr: string;
   partitions_names?: string[];
   partitions_names?: string[];
   output_fields?: 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 DialogTemplate from '../../components/customDialog/DialogTemplate';
 import CustomSwitch from '../../components/customSwitch/CustomSwitch';
 import CustomSwitch from '../../components/customSwitch/CustomSwitch';
 import {
 import {
+  DEFAULT_SEFMENT_FILE_SIZE,
+  DEFAULT_VECTORS,
   INDEX_CONFIG,
   INDEX_CONFIG,
   INDEX_OPTIONS_MAP,
   INDEX_OPTIONS_MAP,
   MetricType,
   MetricType,
@@ -14,21 +16,31 @@ import { getCreateIndexJSCode } from '../../utils/code/Js';
 import { getCreateIndexPYCode } from '../../utils/code/Py';
 import { getCreateIndexPYCode } from '../../utils/code/Py';
 import { formatForm, getMetricOptions } from '../../utils/Form';
 import { formatForm, getMetricOptions } from '../../utils/Form';
 import { getEmbeddingType } from '../../utils/search';
 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 CreateForm from './CreateForm';
+import SizingInfo from './SizingInfo';
 import { IndexType, IndexExtraParam, INDEX_TYPES_ENUM } from './Types';
 import { IndexType, IndexExtraParam, INDEX_TYPES_ENUM } from './Types';
 
 
 const CreateIndex = (props: {
 const CreateIndex = (props: {
   collectionName: string;
   collectionName: string;
-  fieldType: DataType;
+  fieldType: DataTypeStringEnum;
   handleCreate: (params: IndexExtraParam) => void;
   handleCreate: (params: IndexExtraParam) => void;
   handleCancel: () => void;
   handleCancel: () => void;
 
 
   // used for code mode
   // used for code mode
   fieldName: string;
   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: indexTrans } = useTranslation('index');
   const { t: dialogTrans } = useTranslation('dialog');
   const { t: dialogTrans } = useTranslation('dialog');
@@ -84,7 +96,7 @@ const CreateIndex = (props: {
     });
     });
 
 
     const { index_type, metric_type } = indexSetting;
     const { index_type, metric_type } = indexSetting;
-  
+
     const extraParams: IndexExtraParam = {
     const extraParams: IndexExtraParam = {
       index_type,
       index_type,
       metric_type,
       metric_type,
@@ -108,6 +120,58 @@ const CreateIndex = (props: {
     return form;
     return form;
   }, [indexSetting, indexCreateParams]);
   }, [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
    * create index code mode
    */
    */
@@ -200,6 +264,8 @@ const CreateIndex = (props: {
         indexParams={indexCreateParams}
         indexParams={indexCreateParams}
         indexTypeChange={onIndexTypeChange}
         indexTypeChange={onIndexTypeChange}
       />
       />
+
+      <SizingInfo info={sizingInfo} />
     </DialogTemplate>
     </DialogTemplate>
   );
   );
 };
 };

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

@@ -175,6 +175,7 @@ const IndexTypeElement: FC<{
             collectionName={collectionName}
             collectionName={collectionName}
             fieldName={data._fieldName}
             fieldName={data._fieldName}
             fieldType={data._fieldType}
             fieldType={data._fieldType}
+            dimension={Number(data._dimension)}
             handleCancel={handleCloseDialog}
             handleCancel={handleCloseDialog}
             handleCreate={requestCreateIndex}
             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 { FieldHttp } from '../../http/Field';
 import { FieldView } from './Types';
 import { FieldView } from './Types';
 import IndexTypeElement from './IndexTypeElement';
 import IndexTypeElement from './IndexTypeElement';
-import { DataType } from '../collections/Types';
+import { DataTypeStringEnum } from '../collections/Types';
 import { IndexHttp } from '../../http/Index';
 import { IndexHttp } from '../../http/Index';
 
 
 const useStyles = makeStyles((theme: Theme) => ({
 const useStyles = makeStyles((theme: Theme) => ({
@@ -77,7 +77,10 @@ const Schema: FC<{
   const fetchSchemaListWithIndex = async (
   const fetchSchemaListWithIndex = async (
     collectionName: string
     collectionName: string
   ): Promise<FieldView[]> => {
   ): Promise<FieldView[]> => {
-    const vectorTypes: DataType[] = ['BinaryVector', 'FloatVector'];
+    const vectorTypes: DataTypeStringEnum[] = [
+      DataTypeStringEnum.BinaryVector,
+      DataTypeStringEnum.FloatVector,
+    ];
     const indexList = await IndexHttp.getIndexInfo(collectionName);
     const indexList = await IndexHttp.getIndexInfo(collectionName);
     const schemaList = await FieldHttp.getFields(collectionName);
     const schemaList = await FieldHttp.getFields(collectionName);
     let fields: FieldView[] = [];
     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 { ReactElement } from 'react';
 import { MetricType } from '../../consts/Milvus';
 import { MetricType } from '../../consts/Milvus';
-import { DataType } from '../collections/Types';
+import { DataTypeStringEnum } from '../collections/Types';
 
 
 export enum INDEX_TYPES_ENUM {
 export enum INDEX_TYPES_ENUM {
   IVF_FLAT = 'IVF_FLAT',
   IVF_FLAT = 'IVF_FLAT',
@@ -16,7 +16,7 @@ export enum INDEX_TYPES_ENUM {
 }
 }
 
 
 export interface Field {
 export interface Field {
-  data_type: DataType;
+  data_type: DataTypeStringEnum;
   fieldID: string;
   fieldID: string;
   type_params: { key: string; value: string }[];
   type_params: { key: string; value: string }[];
   is_primary_key: true;
   is_primary_key: true;
@@ -30,7 +30,7 @@ export interface FieldData {
   _isAutoId: boolean;
   _isAutoId: boolean;
   _fieldName: string;
   _fieldName: string;
   _fieldNameElement?: ReactElement;
   _fieldNameElement?: ReactElement;
-  _fieldType: DataType;
+  _fieldType: DataTypeStringEnum;
   _dimension: string;
   _dimension: string;
   _desc: string;
   _desc: string;
 }
 }
@@ -78,3 +78,10 @@ export interface IndexExtraParam {
   metric_type: string;
   metric_type: string;
   params: 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 { Option } from '../../components/customSelector/Types';
 import { searchKeywordsType } from '../../consts/Milvus';
 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 {
 export interface SearchParamsProps {
   // if user created index, pass metric type choosed when creating
   // if user created index, pass metric type choosed when creating
@@ -30,7 +33,7 @@ export interface SearchResultView {
 }
 }
 
 
 export interface FieldOption extends Option {
 export interface FieldOption extends Option {
-  fieldType: DataType;
+  fieldType: DataTypeStringEnum;
   // used to get metric type, index type and index params for search params
   // used to get metric type, index type and index params for search params
   // if user doesn't create index, default value is null
   // if user doesn't create index, default value is null
   indexInfo: IndexView | null;
   indexInfo: IndexView | null;
@@ -60,7 +63,8 @@ export interface VectorSearchParam {
   };
   };
   vectors: any;
   vectors: any;
   output_fields: string[];
   output_fields: string[];
-  vector_type: number | DataTypeEnum;
+  vector_type: DataTypeEnum;
+  travel_timestamp?: string;
 }
 }
 
 
 export interface SearchResult {
 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 { TOP_K_OPTIONS } from './Constants';
 import { Option } from '../../components/customSelector/Types';
 import { Option } from '../../components/customSelector/Types';
 import { CollectionHttp } from '../../http/Collection';
 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 { IndexHttp } from '../../http/Index';
 import { getVectorSearchStyles } from './Styles';
 import { getVectorSearchStyles } from './Styles';
 import { parseValue } from '../../utils/Insert';
 import { parseValue } from '../../utils/Insert';
@@ -33,6 +36,8 @@ import Filter from '../../components/advancedSearch';
 import { Field } from '../../components/advancedSearch/Types';
 import { Field } from '../../components/advancedSearch/Types';
 import { useLocation } from 'react-router-dom';
 import { useLocation } from 'react-router-dom';
 import { parseLocationSearch } from '../../utils/Format';
 import { parseLocationSearch } from '../../utils/Format';
+import { CustomDatePicker } from 'insight_src/components/customDatePicker/CustomDatePicker';
+import { useTimeTravelHook } from 'insight_src/hooks/TimeTravel';
 
 
 const VectorSearch = () => {
 const VectorSearch = () => {
   useNavigationHook(ALL_ROUTER_TYPES.SEARCH);
   useNavigationHook(ALL_ROUTER_TYPES.SEARCH);
@@ -51,6 +56,7 @@ const VectorSearch = () => {
   // fields for advanced filter
   // fields for advanced filter
   const [filterFields, setFilterFields] = useState<Field[]>([]);
   const [filterFields, setFilterFields] = useState<Field[]>([]);
   const [selectedField, setSelectedField] = useState<string>('');
   const [selectedField, setSelectedField] = useState<string>('');
+
   // search params form
   // search params form
   const [searchParam, setSearchParam] = useState<{ [key in string]: number }>(
   const [searchParam, setSearchParam] = useState<{ [key in string]: number }>(
     {}
     {}
@@ -78,6 +84,9 @@ const VectorSearch = () => {
     handleGridSort,
     handleGridSort,
   } = usePaginationHook(searchResult || []);
   } = usePaginationHook(searchResult || []);
 
 
+  const { timeTravel, setTimeTravel, timeTravelInfo, handleDateTimeChange } =
+    useTimeTravelHook();
+
   const collectionOptions: Option[] = useMemo(
   const collectionOptions: Option[] = useMemo(
     () =>
     () =>
       collections.map(c => ({
       collections.map(c => ({
@@ -282,7 +291,9 @@ const VectorSearch = () => {
     setSearchResult(null);
     setSearchResult(null);
     setFilterFields([]);
     setFilterFields([]);
     setExpression('');
     setExpression('');
+    setTimeTravel(null);
   };
   };
+
   const handleSearch = async (topK: number, expr = expression) => {
   const handleSearch = async (topK: number, expr = expression) => {
     const searhParamPairs = {
     const searhParamPairs = {
       params: JSON.stringify(searchParam),
       params: JSON.stringify(searchParam),
@@ -298,6 +309,7 @@ const VectorSearch = () => {
       search_params: searhParamPairs,
       search_params: searhParamPairs,
       vectors: [parseValue(vectors)],
       vectors: [parseValue(vectors)],
       vector_type: fieldType,
       vector_type: fieldType,
+      travel_timestamp: timeTravelInfo.timestamp,
     };
     };
 
 
     setTableLoading(true);
     setTableLoading(true);
@@ -457,6 +469,12 @@ const VectorSearch = () => {
             filterDisabled={selectedField === '' || selectedCollection === ''}
             filterDisabled={selectedField === '' || selectedCollection === ''}
             onSubmit={handleAdvancedFilterChange}
             onSubmit={handleAdvancedFilterChange}
           />
           />
+          <CustomDatePicker
+            label={timeTravelInfo.label}
+            onChange={handleDateTimeChange}
+            date={timeTravel}
+            setDate={setTimeTravel}
+          />
         </div>
         </div>
         <div className="right">
         <div className="right">
           <CustomButton className="btn" onClick={handleReset}>
           <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 { t: commonTrans } = useTranslation();
   const capacityTrans: { [key in string]: string } = commonTrans('capacity');
   const capacityTrans: { [key in string]: string } = commonTrans('capacity');
   const { node, extend } = props;
   const { node, extend } = props;
+
   const hardwareTitle = [t('hardwareTitle'), t('valueTitle')];
   const hardwareTitle = [t('hardwareTitle'), t('valueTitle')];
   const hardwareContent = [];
   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 {
   const {
     cpu_core_count: cpu = 0,
     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 {
   const {
     system_version: version,
     system_version: version,
     deploy_mode: mode = '',
     deploy_mode: mode = '',
-    created_time: create = '',
-    updated_time: update = '',
-  } = sysInfos;
+  } = system_info;
   systemContent.push({ label: t('thVersion'), value: version });
   systemContent.push({ label: t('thVersion'), value: version });
   systemContent.push({ label: t('thDeployMode'), value: mode });
   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 (
   return (
     <div className={classes.root}>
     <div className={classes.root}>
@@ -188,6 +203,11 @@ const DataCard: FC<DataCardProps & React.HTMLAttributes<HTMLDivElement>> = (prop
       </div>
       </div>
       {extend && <DataSection titles={hardwareTitle} contents={hardwareContent} />}
       {extend && <DataSection titles={hardwareTitle} contents={hardwareContent} />}
       <DataSection titles={systemTitle} contents={systemContent} />
       <DataSection titles={systemTitle} contents={systemContent} />
+      {systemConfig.length ?
+        <DataSection titles={configTitle} contents={systemConfig} />
+        :
+        null
+      }
     </div>
     </div>
   );
   );
 };
 };

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

@@ -21,7 +21,6 @@ const getStyles = makeStyles((theme: Theme) => ({
       `"a a"
       `"a a"
        "b ."
        "b ."
        "b d"`,
        "b d"`,
-    height: 'calc(100% - 28px)',
   },
   },
   cardContainer: {
   cardContainer: {
     display: 'grid',
     display: 'grid',
@@ -69,7 +68,7 @@ const NodeListView: FC<NodeListViewProps> = (props) => {
   const [rows, setRows] = useState<any[]>([]);
   const [rows, setRows] = useState<any[]>([]);
   const { selectedCord, childNodes, setCord } = props;
   const { selectedCord, childNodes, setCord } = props;
 
 
-  let columns: any[] = [
+  const columns: any[] = [
     {
     {
       field: 'name',
       field: 'name',
       headerName: t('thName'),
       headerName: t('thName'),
@@ -124,6 +123,17 @@ const NodeListView: FC<NodeListViewProps> = (props) => {
     }
     }
   }, [selectedCord, childNodes, capacityTrans]);
   }, [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 (
   return (
     <div className={classes.root}>
     <div className={classes.root}>
       <button className={classes.childCloseBtn} onClick={() => setCord(null)}>
       <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',
     fontFamily: 'Roboto',
     margin: '14px 40px',
     margin: '14px 40px',
     position: 'relative',
     position: 'relative',
-    height: 'calc(100vh - 80px)',
+    height: 'fit-content',
     display: 'flex',
     display: 'flex',
     flexDirection: 'column',
     flexDirection: 'column',
   },
   },
@@ -48,10 +48,11 @@ const getStyles = makeStyles((theme: Theme) => ({
   },
   },
   showChildView: {
   showChildView: {
     top: 0,
     top: 0,
-    maxHeight: 'auto',
+    minHeight: '100%',
+    height: 'fit-content',
   },
   },
   hideChildView: {
   hideChildView: {
-    top: '1000px',
+    top: '1500px',
     maxHeight: 0,
     maxHeight: 0,
   },
   },
   childCloseBtn: {
   childCloseBtn: {
@@ -68,7 +69,7 @@ const parseJson = (jsonData: any) => {
 
 
   const system = {
   const system = {
     // qps: Math.random() * 1000,
     // qps: Math.random() * 1000,
-    letency: Math.random() * 1000,
+    latency: Math.random() * 1000,
     disk: 0,
     disk: 0,
     diskUsage: 0,
     diskUsage: 0,
     memory: 0,
     memory: 0,
@@ -94,7 +95,6 @@ const parseJson = (jsonData: any) => {
   return { nodes, childNodes, system };
   return { nodes, childNodes, system };
 }
 }
 
 
-
 const SystemView: any = () => {
 const SystemView: any = () => {
   useNavigationHook(ALL_ROUTER_TYPES.SYSTEM);
   useNavigationHook(ALL_ROUTER_TYPES.SYSTEM);
   const { t } = useTranslation('systemView');
   const { t } = useTranslation('systemView');
@@ -123,16 +123,18 @@ const SystemView: any = () => {
   }, []);
   }, []);
 
 
   let qps = system?.qps || 0;
   let qps = system?.qps || 0;
-  const letency = system?.letency || 0;
+  const latency = system?.latency || 0;
   const childView = useRef<HTMLInputElement>(null);
   const childView = useRef<HTMLInputElement>(null);
 
 
+
+
   return (
   return (
     <div className={classes.root}>
     <div className={classes.root}>
       <div className={clsx(classes.cardContainer, selectedCord && classes.transparent)}>
       <div className={clsx(classes.cardContainer, selectedCord && classes.transparent)}>
         <ProgressCard title={t('diskTitle')} usage={system.diskUsage} total={system.disk} />
         <ProgressCard title={t('diskTitle')} usage={system.diskUsage} total={system.disk} />
         <ProgressCard title={t('memoryTitle')} usage={system.memoryUsage} total={system.memory} />
         <ProgressCard title={t('memoryTitle')} usage={system.memoryUsage} total={system.memory} />
         <LineChartCard title={t('qpsTitle')} value={qps} />
         <LineChartCard title={t('qpsTitle')} value={qps} />
-        <LineChartCard title={t('letencyTitle')} value={letency} />
+        <LineChartCard title={t('latencyTitle')} value={latency} />
       </div>
       </div>
       <div className={classes.contentContainer}>
       <div className={classes.contentContainer}>
         <Topo nodes={nodes} setNode={setNode} setCord={setCord} />
         <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',
       transition: 'all .25s',
     },
     },
 
 
-    '&:hover, &:focus': {
+    '&:hover, &.selectedNode': {
       transform: 'scale(1.1)',
       transform: 'scale(1.1)',
       filter: 'drop-shadow(3px 3px 5px rgba(0, 0, 0, .2))',
       filter: 'drop-shadow(3px 3px 5px rgba(0, 0, 0, .2))',
-    },
-
-    '&:focus': {
       outline: 'none',
       outline: 'none',
+    },
 
 
+    '&.selectedNode': {
       '& circle': {
       '& circle': {
         fill: '#06AFF2',
         fill: '#06AFF2',
         stroke: '#06AFF2',
         stroke: '#06AFF2',
@@ -57,14 +56,13 @@ const getStyles = makeStyles((theme: Theme) => ({
       transition: 'all .25s',
       transition: 'all .25s',
     },
     },
 
 
-    '&:hover, &:focus': {
+    '&:hover, &.selectedNode': {
       transform: 'scale(1.1)',
       transform: 'scale(1.1)',
       filter: 'drop-shadow(3px 3px 5px rgba(0, 0, 0, .2))',
       filter: 'drop-shadow(3px 3px 5px rgba(0, 0, 0, .2))',
-    },
-
-    '&:focus': {
       outline: 'none',
       outline: 'none',
+    },
 
 
+    '&.selectedNode': {
       '& svg path': {
       '& svg path': {
         fill: 'white',
         fill: 'white',
       },
       },
@@ -113,6 +111,20 @@ const capitalize = (s: string) => {
   return s.charAt(0).toUpperCase() + s.slice(1);
   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 Topo = (props: any) => {
   const classes = getStyles();
   const classes = getStyles();
   const { nodes, setNode, setCord } = props;
   const { nodes, setNode, setCord } = props;
@@ -221,8 +233,9 @@ const Topo = (props: any) => {
               <g key={`${node?.infos?.name}`}>
               <g key={`${node?.infos?.name}`}>
                 <line x1={`${WIDTH / 2}`} y1={`${HEIGHT / 2}`} x2={nodeCenterX} y2={nodeCenterY} stroke="#06AFF2" />
                 <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" />)}
                 {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);
                   setNode(node);
+                  setSelected(e.target);
                 }}
                 }}
                 >
                 >
                   <circle cx={nodeCenterX} cy={nodeCenterY} r={R2} fill="white" stroke="#06AFF2" />
                   <circle cx={nodeCenterX} cy={nodeCenterY} r={R2} fill="white" stroke="#06AFF2" />
@@ -255,9 +268,10 @@ const Topo = (props: any) => {
           }
           }
           return null;
           return null;
         })}
         })}
-        <g id="center" className={classes.rootNode} tabIndex={0} onClick={() => {
+        <g id="center" className={classes.rootNode} tabIndex={0} onClick={e => {
           setNode(centerNode);
           setNode(centerNode);
-        }} >
+          setSelected(e.target);
+        }}>
           <circle cx={`${WIDTH / 2}`} cy={`${HEIGHT / 2}`} r={R1} fill="white" stroke="#06AFF2" />
           <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>
           <text fontFamily="Roboto" textAnchor="middle" alignmentBaseline="middle" fill="#06AFF2" fontWeight="700" fontSize="24" x={`${WIDTH / 2}`} y={`${HEIGHT / 2}`}>Milvus</text>
         </g>
         </g>

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

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

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

@@ -3,6 +3,7 @@
   "version": "0.1.0",
   "version": "0.1.0",
   "client": {
   "client": {
     "path": "system",
     "path": "system",
+    "auth": true,
     "entry": "SystemView.tsx",
     "entry": "SystemView.tsx",
     "label": "System View",
     "label": "System View",
     "iconName": "navSystem"
     "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 Collections from '../pages/collections/Collections';
 import Connect from '../pages/connect/Connect';
 import Connect from '../pages/connect/Connect';
 import Overview from '../pages/overview/Overview';
 import Overview from '../pages/overview/Overview';
-import VectorSearch from '../pages/seach/VectorSearch';
+// import VectorSearch from '../pages/seach/VectorSearch';
 import { RouterConfigType } from './Types';
 import { RouterConfigType } from './Types';
 import loadable from '@loadable/component';
 import loadable from '@loadable/component';
 
 
@@ -29,11 +29,11 @@ const RouterConfig: RouterConfigType[] = [
     component: Collection,
     component: Collection,
     auth: true,
     auth: true,
   },
   },
-  {
-    path: '/search',
-    component: VectorSearch,
-    auth: true,
-  },
+  // {
+  //   path: '/search',
+  //   component: VectorSearch,
+  //   auth: true,
+  // },
 ];
 ];
 
 
 function importAll(r: any, outOfRoot = false) {
 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 = 'search',
   SEARCH = 'search',
   // 'system'
   // 'system'
-  SYSTEM = 'system'
+  SYSTEM = 'system',
+  // plugins
+  PLUGIN = 'plugin',
 }
 }
 
 
 export type NavInfo = {
 export type NavInfo = {

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

@@ -2,7 +2,15 @@ import {
   // for strict mode
   // for strict mode
   unstable_createMuiStrictModeTheme as createMuiTheme,
   unstable_createMuiStrictModeTheme as createMuiTheme,
 } from '@material-ui/core/styles';
 } 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' {
 declare module '@material-ui/core/styles/createPalette' {
   interface Palette {
   interface Palette {
     milvusBlue: Palette['primary'];
     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 { Option } from '../components/customSelector/Types';
 import { METRIC_TYPES_VALUES } from '../consts/Milvus';
 import { METRIC_TYPES_VALUES } from '../consts/Milvus';
 import { IForm } from '../hooks/Form';
 import { IForm } from '../hooks/Form';
-import { DataType } from '../pages/collections/Types';
+import { DataTypeStringEnum } from '../pages/collections/Types';
 import { IndexType } from '../pages/schema/Types';
 import { IndexType } from '../pages/schema/Types';
 
 
 interface IInfo {
 interface IInfo {
@@ -22,7 +22,7 @@ export const formatForm = (info: IInfo): IForm[] => {
 
 
 export const getMetricOptions = (
 export const getMetricOptions = (
   indexType: IndexType,
   indexType: IndexType,
-  fieldType: DataType
+  fieldType: DataTypeStringEnum
 ): Option[] => {
 ): Option[] => {
   const baseFloatOptions = [
   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();
 export const formatAddress = (address: string): string => address.trim();
 
 
 // generate a sting like 20.22/98.33MB with proper unit
 // 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}`;
   if (!value1 || !value2) return `0${capacityTrans.b}`;
   const power = Math.round(Math.log(value1) / Math.log(1024));
   const power = Math.round(Math.log(value1) / Math.log(1024));
   let unit = '';
   let unit = '';
@@ -168,8 +172,47 @@ export const getByteString = (value1: number, value2: number, capacityTrans: { [
       unit = capacityTrans.b;
       unit = capacityTrans.b;
       break;
       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 { Field } from '../components/advancedSearch/Types';
-import { DataType, DataTypeEnum } from '../pages/collections/Types';
+import { DataTypeEnum, DataTypeStringEnum } from '../pages/collections/Types';
 import {
 import {
   FieldData,
   FieldData,
   IndexType,
   IndexType,
@@ -10,7 +10,7 @@ import {
   FieldOption,
   FieldOption,
   SearchResult,
   SearchResult,
   SearchResultView,
   SearchResultView,
-} from '../pages/seach/Types';
+} from '../types/SearchTypes';
 
 
 /**
 /**
  * Do not change  vector search result default sort  by ourself.
  * Do not change  vector search result default sort  by ourself.
@@ -21,11 +21,19 @@ import {
 export const transferSearchResult = (
 export const transferSearchResult = (
   result: SearchResult[]
   result: SearchResult[]
 ): SearchResultView[] => {
 ): 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;
   return resultView;
 };
 };
@@ -35,7 +43,7 @@ export const transferSearchResult = (
  * @param fieldType only vector type fields: 'BinaryVector' or 'FloatVector'
  * @param fieldType only vector type fields: 'BinaryVector' or 'FloatVector'
  */
  */
 export const getEmbeddingType = (
 export const getEmbeddingType = (
-  fieldType: DataType
+  fieldType: DataTypeStringEnum
 ): DataTypeEnum.BinaryVector | DataTypeEnum.FloatVector => {
 ): DataTypeEnum.BinaryVector | DataTypeEnum.FloatVector => {
   const type =
   const type =
     fieldType === 'BinaryVector'
     fieldType === 'BinaryVector'
@@ -64,7 +72,10 @@ export const getDefaultIndexType = (embeddingType: DataTypeEnum): IndexType => {
 export const classifyFields = (
 export const classifyFields = (
   fields: FieldData[]
   fields: FieldData[]
 ): { vectorFields: FieldData[]; nonVectorFields: FieldData[] } => {
 ): { vectorFields: FieldData[]; nonVectorFields: FieldData[] } => {
-  const vectorTypes: DataType[] = ['BinaryVector', 'FloatVector'];
+  const vectorTypes: DataTypeStringEnum[] = [
+    DataTypeStringEnum.BinaryVector,
+    DataTypeStringEnum.FloatVector,
+  ];
   return fields.reduce(
   return fields.reduce(
     (result, cur) => {
     (result, cur) => {
       const changedFieldType = vectorTypes.includes(cur._fieldType)
       const changedFieldType = vectorTypes.includes(cur._fieldType)
@@ -101,9 +112,8 @@ export const getVectorFieldOptions = (
 };
 };
 
 
 export const getNonVectorFieldsForFilter = (fields: FieldData[]): Field[] => {
 export const getNonVectorFieldsForFilter = (fields: FieldData[]): Field[] => {
-  const intTypes: DataType[] = ['Int8', 'Int16', 'Int32', 'Int64'];
   return fields.map(f => ({
   return fields.map(f => ({
     name: f._fieldName,
     name: f._fieldName,
-    type: intTypes.includes(f._fieldType) ? 'int' : 'float',
+    type: f._fieldType,
   }));
   }));
 };
 };

+ 2 - 1
client/tsconfig.paths.json

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

+ 57 - 2
client/yarn.lock

@@ -1172,6 +1172,13 @@
   dependencies:
   dependencies:
     regenerator-runtime "^0.13.4"
     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":
 "@babel/template@^7.10.4", "@babel/template@^7.16.0", "@babel/template@^7.3.3":
   version "7.16.0"
   version "7.16.0"
   resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.0.tgz#d16a35ebf4cd74e202083356fab21dd89363ddd6"
   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"
   resolved "https://registry.yarnpkg.com/@csstools/normalize.css/-/normalize.css-10.1.0.tgz#f0950bba18819512d42f7197e56c518aa491cf18"
   integrity sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg==
   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":
 "@emotion/hash@^0.8.0":
   version "0.8.0"
   version "0.8.0"
   resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413"
   resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413"
@@ -1541,6 +1560,18 @@
     prop-types "^15.7.2"
     prop-types "^15.7.2"
     react-is "^16.8.0 || ^17.0.0"
     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":
 "@material-ui/styles@^4.11.4":
   version "4.11.4"
   version "4.11.4"
   resolved "https://registry.yarnpkg.com/@material-ui/styles/-/styles-4.11.4.tgz#eb9dfccfcc2d208243d986457dff025497afa00d"
   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"
   resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
   integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==
   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":
 "@types/glob@^7.1.1":
   version "7.2.0"
   version "7.2.0"
   resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb"
   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"
   resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"
   integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==
   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":
 "@types/tapable@^1", "@types/tapable@^1.0.5":
   version "1.0.8"
   version "1.0.8"
   resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.8.tgz#b94a4391c85666c7b73299fd3ad79d4faa435310"
   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"
     strip-ansi "^6.0.0"
     wrap-ansi "^6.2.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"
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188"
   resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188"
   integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==
   integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==
@@ -5358,6 +5401,11 @@ file-loader@6.1.1:
     loader-utils "^2.0.0"
     loader-utils "^2.0.0"
     schema-utils "^3.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:
 file-uri-to-path@1.0.0:
   version "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"
   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"
     prismjs "^1.22.0"
     refractor "^3.2.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"
   version "4.4.2"
   resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.2.tgz#8b59a56f09ced7b55cbd53c36768b922890d5470"
   resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.2.tgz#8b59a56f09ced7b55cbd53c36768b922890d5470"
   integrity sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg==
   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"
   resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3"
   integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=
   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:
 rimraf@^2.5.4, rimraf@^2.6.3:
   version "2.7.1"
   version "2.7.1"
   resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
   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-jsdoc": "^6.1.0",
     "swagger-ui-express": "^4.1.6"
     "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": {
   "devDependencies": {
-    "@types/swagger-jsdoc": "^6.0.1",
     "@types/chalk": "^2.2.0",
     "@types/chalk": "^2.2.0",
     "@types/cors": "^2.8.12",
     "@types/cors": "^2.8.12",
     "@types/express": "^4.17.13",
     "@types/express": "^4.17.13",
     "@types/glob": "^7.2.0",
     "@types/glob": "^7.2.0",
+    "@types/jest": "^27.0.2",
     "@types/morgan": "^1.9.3",
     "@types/morgan": "^1.9.3",
     "@types/node": "^16.11.6",
     "@types/node": "^16.11.6",
     "@types/node-cron": "^3.0.0",
     "@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/swagger-ui-express": "^4.1.3",
     "@types/ws": "^8.2.0",
     "@types/ws": "^8.2.0",
+    "jest": "^27.3.1",
     "nodemon": "^2.0.14",
     "nodemon": "^2.0.14",
+    "prettier": "^2.4.1",
+    "supertest": "^6.1.6",
+    "ts-jest": "^27.0.7",
     "ts-node": "^10.4.0",
     "ts-node": "^10.4.0",
     "tslint": "^6.1.3",
     "tslint": "^6.1.3",
     "typescript": "^4.4.4"
     "typescript": "^4.4.4"
@@ -44,8 +66,12 @@
     "start": "nodemon ./src/app",
     "start": "nodemon ./src/app",
     "start:plugin": "yarn build && cross-env PLUGIN_DEV=1 node dist/milvus-insight/express/src/app.js",
     "start:plugin": "yarn build && cross-env PLUGIN_DEV=1 node dist/milvus-insight/express/src/app.js",
     "start:prod": "node dist/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": {
   "nodemonConfig": {
     "ignore": [
     "ignore": [
@@ -58,6 +84,6 @@
     "watch": [
     "watch": [
       "src"
       "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 SRC_PLUGIN_DIR = "src/plugins";
 const DEV_PLUGIN_DIR = "../../src/*/server";
 const DEV_PLUGIN_DIR = "../../src/*/server";
 
 
-const app = express();
+export const app = express();
 const PORT = 3000;
 const PORT = 3000;
 // initialize a simple http server
 // initialize a simple http server
 const server = http.createServer(app);
 const server = http.createServer(app);

+ 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 {
 import {
   CreateAliasDto,
   CreateAliasDto,
   CreateCollectionDto,
   CreateCollectionDto,
   InsertDataDto,
   InsertDataDto,
-  ShowCollectionsDto,
   VectorSearchDto,
   VectorSearchDto,
   QueryDto,
   QueryDto,
-} from "./dto";
+} from './dto';
 
 
 export class CollectionController {
 export class CollectionController {
   private collectionsService: CollectionsService;
   private collectionsService: CollectionsService;
@@ -26,67 +25,57 @@ export class CollectionController {
   }
   }
 
 
   generateRoutes() {
   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(
     this.router.post(
-      "/",
+      '/',
       dtoValidationMiddleware(CreateCollectionDto),
       dtoValidationMiddleware(CreateCollectionDto),
       this.createCollection.bind(this)
       this.createCollection.bind(this)
     );
     );
 
 
-    this.router.get("/statistics", this.getStatistics.bind(this));
+    this.router.get('/statistics', this.getStatistics.bind(this));
 
 
     this.router.get(
     this.router.get(
-      "/:name/statistics",
+      '/:name/statistics',
       this.getCollectionStatistics.bind(this)
       this.getCollectionStatistics.bind(this)
     );
     );
 
 
     this.router.get(
     this.router.get(
-      "/indexes/status",
+      '/indexes/status',
       this.getCollectionsIndexStatus.bind(this)
       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(
     this.router.post(
-      "/:name/insert",
+      '/:name/insert',
       dtoValidationMiddleware(InsertDataDto),
       dtoValidationMiddleware(InsertDataDto),
       this.insert.bind(this)
       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(
     this.router.post(
-      "/:name/search",
+      '/:name/search',
       dtoValidationMiddleware(VectorSearchDto),
       dtoValidationMiddleware(VectorSearchDto),
       this.vectorSearch.bind(this)
       this.vectorSearch.bind(this)
     );
     );
 
 
     this.router.post(
     this.router.post(
-      "/:name/query",
+      '/:name/query',
       dtoValidationMiddleware(QueryDto),
       dtoValidationMiddleware(QueryDto),
       this.query.bind(this)
       this.query.bind(this)
     );
     );
 
 
     this.router.post(
     this.router.post(
-      "/:name/alias",
+      '/:name/alias',
       dtoValidationMiddleware(CreateAliasDto),
       dtoValidationMiddleware(CreateAliasDto),
       this.createAlias.bind(this)
       this.createAlias.bind(this)
     );
     );
@@ -95,7 +84,7 @@ export class CollectionController {
   }
   }
 
 
   async showCollections(req: Request, res: Response, next: NextFunction) {
   async showCollections(req: Request, res: Response, next: NextFunction) {
-    const type = parseInt("" + req.query?.type, 10);
+    const type = parseInt('' + req.query?.type, 10);
     try {
     try {
       const result =
       const result =
         type === 1
         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) {
   async vectorSearch(req: Request, res: Response, next: NextFunction) {
     const name = req.params?.name;
     const name = req.params?.name;
     const data = req.body;
     const data = req.body;
@@ -248,12 +251,12 @@ export class CollectionController {
         collection_name: name,
         collection_name: name,
         ...data,
         ...data,
       });
       });
-      const queryResultList = result.data;
+      // const queryResultList = result.data;
       const queryResultLength = result.data.length;
       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 });
       res.send({ ...result, limit, page, total: queryResultLength });
     } catch (error) {
     } catch (error) {
       next(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 {
 import {
   CreateCollectionReq,
   CreateCollectionReq,
   DescribeCollectionReq,
   DescribeCollectionReq,
@@ -9,18 +9,19 @@ import {
   LoadCollectionReq,
   LoadCollectionReq,
   ReleaseLoadCollectionReq,
   ReleaseLoadCollectionReq,
   SearchReq,
   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 {
 import {
   AlterAliasReq,
   AlterAliasReq,
   CreateAliasReq,
   CreateAliasReq,
   DropAliasReq,
   DropAliasReq,
   ShowCollectionsReq,
   ShowCollectionsReq,
   ShowCollectionsType,
   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 {
 export class CollectionsService {
   constructor(private milvusService: MilvusService) {}
   constructor(private milvusService: MilvusService) {}
@@ -85,6 +86,12 @@ export class CollectionsService {
     return res;
     return res;
   }
   }
 
 
+  async deleteEntities(data: DeleteEntitiesReq) {
+    const res = await this.dataManager.deleteEntities(data);
+    throwErrorFromSDK(res.status);
+    return res;
+  }
+
   async vectorSearch(data: SearchReq) {
   async vectorSearch(data: SearchReq) {
     const res = await this.dataManager.search(data);
     const res = await this.dataManager.search(data);
     throwErrorFromSDK(res.status);
     throwErrorFromSDK(res.status);
@@ -144,7 +151,6 @@ export class CollectionsService {
     if (res.data.length > 0) {
     if (res.data.length > 0) {
       for (const item of res.data) {
       for (const item of res.data) {
         const { name } = item;
         const { name } = item;
-
         const collectionInfo = await this.describeCollection({
         const collectionInfo = await this.describeCollection({
           collection_name: name,
           collection_name: name,
         });
         });
@@ -166,7 +172,7 @@ export class CollectionsService {
         );
         );
 
 
         const loadedPercentage = !loadCollection
         const loadedPercentage = !loadCollection
-          ? "-1"
+          ? '-1'
           : loadCollection.loadedPercentage;
           : loadCollection.loadedPercentage;
 
 
         data.push({
         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 {
 export class CronsService {
   constructor(
   constructor(
@@ -11,13 +11,22 @@ export class CronsService {
 
 
   async toggleCronJobByName(data: { name: string; type: WS_EVENTS_TYPE }) {
   async toggleCronJobByName(data: { name: string; type: WS_EVENTS_TYPE }) {
     const { name, type } = data;
     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) {
   async getCollections(name: string) {
@@ -26,17 +35,18 @@ export class CronsService {
         const res = await this.collectionService.getAllCollections();
         const res = await this.collectionService.getAllCollections();
         // TODO
         // TODO
         // this.eventService.server.emit("COLLECTION", res);
         // this.eventService.server.emit("COLLECTION", res);
-        pubSub.emit("ws_pubsub", {
-          event: WS_EVENTS.COLLECTION + "",
+        pubSub.emit('ws_pubsub', {
+          event: WS_EVENTS.COLLECTION + '',
           data: res,
           data: res,
         });
         });
         return res;
         return res;
       } catch (error) {
       } catch (error) {
         // When user not connect milvus, stop cron
         // 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);
         throw new Error(error);
       }
       }
     };
     };
@@ -54,7 +64,7 @@ export class SchedulerRegistry {
 
 
   setCronJobEverySecond(name: string, func: () => {}) {
   setCronJobEverySecond(name: string, func: () => {}) {
     // The cron job will run every second
     // The cron job will run every second
-    this.setCronJob(name, "* * * * * *", func);
+    this.setCronJob(name, '* * * * * *', func);
   }
   }
 
 
   // ┌────────────── second (optional)
   // ┌────────────── second (optional)
@@ -73,7 +83,7 @@ export class SchedulerRegistry {
       target?.entity?.stop();
       target?.entity?.stop();
     } else {
     } else {
       const task = schedule(scheduler, () => {
       const task = schedule(scheduler, () => {
-        console.log(`${name}: running a task every seconds`);
+        console.log(`[Scheduler:${scheduler}] ${name}: running a task.`);
         func();
         func();
       });
       });
       this.cronJobList.push({
       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 { sanitize } from "class-sanitizer";
 import HttpException from "../exception/HttpException";
 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 = (
 export const dtoValidationMiddleware = (
   type: any,
   type: any,
   skipMissingProperties = false
   skipMissingProperties = false
 ): RequestHandler => {
 ): RequestHandler => {
   return (req, res, next) => {
   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(
     validate(dtoObj, { skipMissingProperties }).then(
       (errors: ValidationError[]) => {
       (errors: ValidationError[]) => {
         if (errors.length > 0) {
         if (errors.length > 0) {

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

@@ -1,7 +1,7 @@
 import { NextFunction, Request, Response, Router } from "express";
 import { NextFunction, Request, Response, Router } from "express";
 import { dtoValidationMiddleware } from "../middlewares/validation";
 import { dtoValidationMiddleware } from "../middlewares/validation";
 import { MilvusService } from "./milvus.service";
 import { MilvusService } from "./milvus.service";
-import { CheckMilvusDto, ConnectMilvusDto, FlushDto } from "./dto";
+import { ConnectMilvusDto, FlushDto } from "./dto";
 
 
 export class MilvusController {
 export class MilvusController {
   private router: Router;
   private router: Router;
@@ -23,11 +23,7 @@ export class MilvusController {
       this.connectMilvus.bind(this)
       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(
     this.router.put(
       "/flush",
       "/flush",
@@ -44,6 +40,7 @@ export class MilvusController {
     const address = req.body?.address;
     const address = req.body?.address;
     try {
     try {
       const result = await this.milvusService.connectMilvus(address);
       const result = await this.milvusService.connectMilvus(address);
+
       res.send(result);
       res.send(result);
     } catch (error) {
     } catch (error) {
       next(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 {
 import {
   FlushReq,
   FlushReq,
   GetMetricsResponse,
   GetMetricsResponse,
-} from "@zilliz/milvus2-sdk-node/dist/milvus/types";
+} from '@zilliz/milvus2-sdk-node/dist/milvus/types';
 
 
 export class MilvusService {
 export class MilvusService {
   private milvusAddress: string;
   private milvusAddress: string;
@@ -44,17 +44,17 @@ export class MilvusService {
 
 
   private checkMilvus() {
   private checkMilvus() {
     if (!this.milvusClient) {
     if (!this.milvusClient) {
-      throw new Error("Please connect milvus first");
+      throw new Error('Please connect milvus first');
     }
     }
   }
   }
 
 
   async connectMilvus(address: string) {
   async connectMilvus(address: string) {
     // grpc only need address without http
     // grpc only need address without http
-    const milvusAddress = address.replace(/(http|https):\/\//, "");
+    const milvusAddress = address.replace(/(http|https):\/\//, '');
     try {
     try {
       this.milvusClient = new MilvusClient(milvusAddress);
       this.milvusClient = new MilvusClient(milvusAddress);
       await this.milvusClient.collectionManager.hasCollection({
       await this.milvusClient.collectionManager.hasCollection({
-        collection_name: "not_exist",
+        collection_name: 'not_exist',
       });
       });
       this.milvusAddress = address;
       this.milvusAddress = address;
       this.milvusClients = {
       this.milvusClients = {
@@ -62,7 +62,7 @@ export class MilvusService {
       };
       };
       return { address: this.milvusAddress };
       return { address: this.milvusAddress };
     } catch (error) {
     } 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> {
   async getMetrics(): Promise<GetMetricsResponse> {
     const res = await this.milvusClient.dataManager.getMetric({
     const res = await this.milvusClient.dataManager.getMetric({
-      request: { metric_type: "system_info" },
+      request: { metric_type: 'system_info' },
     });
     });
     return res;
     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 {
 import {
   GetPartitionsInfoDto,
   GetPartitionsInfoDto,
   ManagePartitionDto,
   ManagePartitionDto,
   LoadPartitionsDto,
   LoadPartitionsDto,
-} from "./dto";
+} from './dto';
 
 
 export class PartitionController {
 export class PartitionController {
   private router: Router;
   private router: Router;
@@ -19,26 +19,22 @@ export class PartitionController {
   }
   }
 
 
   generateRoutes() {
   generateRoutes() {
-    this.router.get(
-      "/",
-      dtoValidationMiddleware(GetPartitionsInfoDto),
-      this.getPatitionsInfo.bind(this)
-    );
+    this.router.get('/', this.getPartitionsInfo.bind(this));
 
 
     this.router.post(
     this.router.post(
-      "/",
+      '/',
       dtoValidationMiddleware(ManagePartitionDto),
       dtoValidationMiddleware(ManagePartitionDto),
       this.managePartition.bind(this)
       this.managePartition.bind(this)
     );
     );
 
 
-    this.router.post(
-      "/load",
+    this.router.put(
+      '/load',
       dtoValidationMiddleware(LoadPartitionsDto),
       dtoValidationMiddleware(LoadPartitionsDto),
       this.loadPartition.bind(this)
       this.loadPartition.bind(this)
     );
     );
 
 
-    this.router.post(
-      "/release",
+    this.router.put(
+      '/release',
       dtoValidationMiddleware(LoadPartitionsDto),
       dtoValidationMiddleware(LoadPartitionsDto),
       this.releasePartition.bind(this)
       this.releasePartition.bind(this)
     );
     );
@@ -46,10 +42,10 @@ export class PartitionController {
     return this.router;
     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 {
     try {
-      const result = await this.partitionsService.getPatitionsInfo({
+      const result = await this.partitionsService.getPartitionsInfo({
         collection_name: collectionName,
         collection_name: collectionName,
       });
       });
       res.send(result);
       res.send(result);
@@ -62,9 +58,9 @@ export class PartitionController {
     const { type, ...params } = req.body;
     const { type, ...params } = req.body;
     try {
     try {
       const result =
       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);
       res.send(result);
     } catch (error) {
     } catch (error) {
       next(error);
       next(error);

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

@@ -18,7 +18,7 @@ export class PartitionsService {
     return this.milvusService.partitionManager;
     return this.milvusService.partitionManager;
   }
   }
 
 
-  async getPatitionsInfo(data: ShowPartitionsReq) {
+  async getPartitionsInfo(data: ShowPartitionsReq) {
     const result = [];
     const result = [];
     const res = await this.getPartitions(data);
     const res = await this.getPartitions(data);
     if (res.partition_names && res.partition_names.length) {
     if (res.partition_names && res.partition_names.length) {
@@ -44,13 +44,13 @@ export class PartitionsService {
     return res;
     return res;
   }
   }
 
 
-  async createParition(data: CreatePartitionReq) {
+  async createPartition(data: CreatePartitionReq) {
     const res = await this.partitionManager.createPartition(data);
     const res = await this.partitionManager.createPartition(data);
     throwErrorFromSDK(res);
     throwErrorFromSDK(res);
     return res;
     return res;
   }
   }
 
 
-  async deleteParition(data: DropPartitionReq) {
+  async deletePartition(data: DropPartitionReq) {
     const res = await this.partitionManager.dropPartition(data);
     const res = await this.partitionManager.dropPartition(data);
     throwErrorFromSDK(res);
     throwErrorFromSDK(res);
     return res;
     return res;
@@ -73,4 +73,4 @@ export class PartitionsService {
     throwErrorFromSDK(res);
     throwErrorFromSDK(res);
     return 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 { SchemaService } from "./schema.service";
 import { milvusService } from "../milvus";
 import { milvusService } from "../milvus";
 
 
-import {
-  ManageIndexDto,
-  DescribeIndexDto,
-  GetIndexProgressDto,
-  GetIndexStateDto,
-} from "./dto";
+import { ManageIndexDto } from "./dto";
 
 
 export class SchemaController {
 export class SchemaController {
   private router: Router;
   private router: Router;
@@ -26,23 +21,11 @@ export class SchemaController {
       this.manageIndex.bind(this)
       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;
     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" }],
       servers: [{ url: "/api/v1" }],
     },
     },
-    apis: ["./src/**/*.ts"],
+    apis: ["./src/**/*.yml"],
   };
   };
   const swaggerSpec = swaggerJsdoc(options);
   const swaggerSpec = swaggerJsdoc(options);
 
 

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