2
0
Эх сурвалжийг харах

Merge remote-tracking branch 'upstream/main' into test/client-component

tumao 4 жил өмнө
parent
commit
ee6dc232c4
100 өөрчлөгдсөн 4385 нэмэгдсэн , 252 устгасан
  1. 3 14
      .github/ISSUE_TEMPLATE/Bug_report.md
  2. 3 1
      .github/ISSUE_TEMPLATE/Feature_request.md
  3. 1 0
      .gitignore
  4. 27 6
      README.md
  5. 3 0
      client/package.json
  6. 4 0
      client/src/assets/icons/copy.svg
  7. 3 0
      client/src/assets/icons/nav-search.svg
  8. 5 0
      client/src/assets/icons/search.svg
  9. 5 0
      client/src/assets/icons/upload.svg
  10. BIN
      client/src/assets/imgs/insert/fail.png
  11. BIN
      client/src/assets/imgs/insert/success.png
  12. 2 2
      client/src/components/__test__/customDialog/DialogTemplate.spec.tsx
  13. 191 0
      client/src/components/advancedSearch/Condition.tsx
  14. 234 0
      client/src/components/advancedSearch/ConditionGroup.tsx
  15. 56 0
      client/src/components/advancedSearch/CopyButton.tsx
  16. 220 0
      client/src/components/advancedSearch/Dialog.tsx
  17. 320 0
      client/src/components/advancedSearch/Filter.tsx
  18. 83 0
      client/src/components/advancedSearch/Types.ts
  19. 3 0
      client/src/components/advancedSearch/index.tsx
  20. 1 1
      client/src/components/customButton/CustomButton.tsx
  21. 15 3
      client/src/components/customDialog/CustomDialogTitle.tsx
  22. 31 15
      client/src/components/customDialog/DialogTemplate.tsx
  23. 9 3
      client/src/components/customDialog/Types.ts
  24. 19 24
      client/src/components/customSelector/CustomSelector.tsx
  25. 3 1
      client/src/components/customSelector/Types.ts
  26. 6 0
      client/src/components/grid/Grid.tsx
  27. 28 13
      client/src/components/grid/Table.tsx
  28. 36 0
      client/src/components/grid/TableEditableHead.tsx
  29. 9 5
      client/src/components/grid/TableHead.tsx
  30. 6 3
      client/src/components/grid/ToolBar.tsx
  31. 22 0
      client/src/components/grid/Types.ts
  32. 22 0
      client/src/components/icons/Icons.tsx
  33. 8 1
      client/src/components/icons/Types.ts
  34. 356 0
      client/src/components/insert/Container.tsx
  35. 195 0
      client/src/components/insert/Import.tsx
  36. 210 0
      client/src/components/insert/Preview.tsx
  37. 93 0
      client/src/components/insert/Status.tsx
  38. 81 0
      client/src/components/insert/Types.ts
  39. 30 7
      client/src/components/layout/Layout.tsx
  40. 26 6
      client/src/components/menu/SimpleMenu.tsx
  41. 7 1
      client/src/components/menu/Types.ts
  42. 15 0
      client/src/components/uploader/Types.ts
  43. 86 0
      client/src/components/uploader/Uploader.tsx
  44. 4 0
      client/src/consts/Insert.ts
  45. 108 31
      client/src/consts/Milvus.tsx
  46. 8 0
      client/src/context/Auth.tsx
  47. 28 4
      client/src/hooks/Dialog.tsx
  48. 4 1
      client/src/hooks/Form.ts
  49. 6 7
      client/src/hooks/Navigation.ts
  50. 1 1
      client/src/http/Axios.ts
  51. 6 0
      client/src/http/BaseModel.ts
  52. 34 2
      client/src/http/Collection.ts
  53. 13 1
      client/src/http/Index.ts
  54. 10 0
      client/src/http/Milvus.ts
  55. 4 0
      client/src/i18n/cn/button.ts
  56. 1 0
      client/src/i18n/cn/collection.ts
  57. 28 0
      client/src/i18n/cn/insert.ts
  58. 1 0
      client/src/i18n/cn/nav.ts
  59. 15 0
      client/src/i18n/cn/search.ts
  60. 2 0
      client/src/i18n/cn/warning.ts
  61. 4 0
      client/src/i18n/en/button.ts
  62. 1 0
      client/src/i18n/en/collection.ts
  63. 28 0
      client/src/i18n/en/insert.ts
  64. 1 0
      client/src/i18n/en/nav.ts
  65. 15 0
      client/src/i18n/en/search.ts
  66. 2 0
      client/src/i18n/en/warning.ts
  67. 8 0
      client/src/i18n/index.ts
  68. 69 5
      client/src/pages/collections/Collections.tsx
  69. 5 2
      client/src/pages/collections/Create.tsx
  70. 3 3
      client/src/pages/collections/CreateFields.tsx
  71. 8 0
      client/src/pages/collections/Types.ts
  72. 33 3
      client/src/pages/connect/Connect.tsx
  73. 39 15
      client/src/pages/overview/Overview.tsx
  74. 6 3
      client/src/pages/overview/collectionCard/CollectionCard.tsx
  75. 5 4
      client/src/pages/overview/collectionCard/Types.ts
  76. 1 1
      client/src/pages/partitions/Create.tsx
  77. 70 2
      client/src/pages/partitions/Partitions.tsx
  78. 31 15
      client/src/pages/schema/Create.tsx
  79. 3 3
      client/src/pages/schema/CreateForm.tsx
  80. 4 1
      client/src/pages/schema/Schema.tsx
  81. 21 6
      client/src/pages/schema/Types.ts
  82. 1 0
      client/src/pages/seach/Constants.ts
  83. 259 0
      client/src/pages/seach/SearchParams.tsx
  84. 119 0
      client/src/pages/seach/Styles.ts
  85. 62 0
      client/src/pages/seach/Types.ts
  86. 422 0
      client/src/pages/seach/VectorSearch.tsx
  87. 6 0
      client/src/router/Config.ts
  88. 2 2
      client/src/router/Types.ts
  89. 5 0
      client/src/styles/common.css
  90. 9 0
      client/src/styles/theme.ts
  91. 28 0
      client/src/utils/Common.ts
  92. 7 7
      client/src/utils/Form.ts
  93. 24 10
      client/src/utils/Format.ts
  94. 51 0
      client/src/utils/Insert.ts
  95. 29 5
      client/src/utils/Validation.ts
  96. 104 0
      client/src/utils/search.ts
  97. 100 2
      client/yarn.lock
  98. 33 0
      server/generate-csv.ts
  99. 8 1
      server/package.json
  100. 9 9
      server/src/app.module.ts

+ 3 - 14
.github/ISSUE_TEMPLATE/Bug_report.md

@@ -5,27 +5,16 @@ labels: defect
 
 
 ---
 ---
 
 
-**Milvus-insight version:**
-
-**Milvus version:**
-
-**Browser version:**
+**Describe the bug:**
 
 
-**Browser OS version:**
 
 
-**Describe the bug:**
 
 
 **Steps to reproduce:**
 **Steps to reproduce:**
 1.
 1.
 2.
 2.
 3.
 3.
 
 
-**Expected behavior:**
-
-**Screenshots (if relevant):**
-
-**Errors in browser console (if relevant):**
+**Milvus-insight version:**
 
 
-**Provide logs and/or server output (if relevant):**
 
 
-**Any additional context:**
+**Milvus version:**

+ 3 - 1
.github/ISSUE_TEMPLATE/Feature_request.md

@@ -1,9 +1,11 @@
 ---
 ---
 name: Feature request
 name: Feature request
 about: Milvus-insight can't do all the things, but maybe it can do your things.
 about: Milvus-insight can't do all the things, but maybe it can do your things.
+labels: feat
 
 
 ---
 ---
 
 
 **Describe the feature:**
 **Describe the feature:**
 
 
-**Describe a specific use case for the feature:**
+
+**Describe a specific use case for the feature:**

+ 1 - 0
.gitignore

@@ -32,6 +32,7 @@ server/dist
 server/build
 server/build
 server/coverage
 server/coverage
 server/documentation
 server/documentation
+server/vectors.csv
 
 
 
 
 # package.lock.json
 # package.lock.json

+ 27 - 6
README.md

@@ -4,7 +4,26 @@ Milvus insight provides an intuitive and efficient GUI for Milvus, allowing you
 
 
 <img src="./.github/images/screenshot.png" alt="Miluvs insight" />
 <img src="./.github/images/screenshot.png" alt="Miluvs insight" />
 
 
-## ⭐️ Install
+## Features and Roadmap
+Milvus insight is under rapid development nad we are adding new features weekly, here are the current plan, we will release a version once a feature is available.
+- Manage collections/partitions
+- Manage index
+- Basic statistics overview
+- Load/release collections for search
+- Insert entities
+- Vector search with advanced filter
+- Milvus system view(TBD)
+- View milvus node configuration(TBD)
+- Data view (TBD)
+- more...
+
+## Quick start
+
+### Before you start
+
+Ensure you have Milvus installed on [your server](https://milvus.io/docs/install_standalone-docker.md) or [cluster](https://milvus.io/docs/install_cluster-docker.md), and Milvus insight only supports Milvus 2.x.
+
+### ⭐️ Install Milvus insight
 
 
 Start Docker container and map the url to the container:
 Start Docker container and map the url to the container:
 
 
@@ -19,6 +38,8 @@ docker run -p 8000:3000 -e HOST_URL=http://127.0.0.1:8000 -e MILVUS_URL=127.0.0.
 
 
 Once you start the docker, open the browser, type `http://127.0.0.1:8000`, you can view the milvus insight.
 Once you start the docker, open the browser, type `http://127.0.0.1:8000`, you can view the milvus insight.
 
 
+***note*** We plan to release milvus insight once a feature is done. Also, if you want to try the nightly build, please pull the docker image with the `dev` tag.
+
 ## ✨ Building and Running Milvus insight, and/or Contributing Code
 ## ✨ Building and Running Milvus insight, and/or Contributing Code
 
 
 You might want to build Milvus-insight locally to contribute some code, test out the latest features, or try
 You might want to build Milvus-insight locally to contribute some code, test out the latest features, or try
@@ -39,15 +60,15 @@ out an open PR:
 4. Create a branch for your PR
 4. Create a branch for your PR
 
 
 ### Milvus
 ### Milvus
+
 New to milvus? Milvus is an open-source vector database built to power AI applications and embedding similarity search.
 New to milvus? Milvus is an open-source vector database built to power AI applications and embedding similarity search.
+
+### Userful links
+
 - [Milvus installation guide](https://milvus.io/docs/v2.0.0/install_standalone-docker.md)
 - [Milvus installation guide](https://milvus.io/docs/v2.0.0/install_standalone-docker.md)
 - [Milvus python sdk](https://milvus.io/docs/v2.0.0/explore_pymilvus.md)
 - [Milvus python sdk](https://milvus.io/docs/v2.0.0/explore_pymilvus.md)
 - [Milvus bootcamp](https://milvus.io/bootcamp)
 - [Milvus bootcamp](https://milvus.io/bootcamp)
 
 
-## 📖 Documentation
-
-TBD
-
 ## Community
 ## Community
 
 
 👉 Join the Milvus community on [Slack](https://join.slack.com/t/milvusio/shared_invite/zt-e0u4qu3k-bI2GDNys3ZqX1YCJ9OM~GQ) to share your suggestions, advice, and questions with our engineering team.
 👉 Join the Milvus community on [Slack](https://join.slack.com/t/milvusio/shared_invite/zt-e0u4qu3k-bI2GDNys3ZqX1YCJ9OM~GQ) to share your suggestions, advice, and questions with our engineering team.
@@ -56,7 +77,7 @@ TBD
     <img src="https://zillizstorage.blob.core.windows.net/zilliz-assets/zilliz-assets/assets/readme_slack_4a07c4c92f.png" alt="Miluvs Slack Channel"  height="150" width="500">
     <img src="https://zillizstorage.blob.core.windows.net/zilliz-assets/zilliz-assets/assets/readme_slack_4a07c4c92f.png" alt="Miluvs Slack Channel"  height="150" width="500">
 </a>
 </a>
 
 
-### ❓ Questions? Problems?
+#### ❓ Questions? Problems?
 
 
 - If you've found a bug or want to request a feature, please create a [GitHub Issue](https://github.com/milvus-io/milvus-insight/issues/new/choose).
 - If you've found a bug or want to request a feature, please create a [GitHub Issue](https://github.com/milvus-io/milvus-insight/issues/new/choose).
   Please check to make sure someone else hasn't already created an issue for the same topic.
   Please check to make sure someone else hasn't already created an issue for the same topic.

+ 3 - 0
client/package.json

@@ -14,6 +14,7 @@
     "@testing-library/user-event": "^12.1.10",
     "@testing-library/user-event": "^12.1.10",
     "@types/jest": "^26.0.15",
     "@types/jest": "^26.0.15",
     "@types/node": "^12.0.0",
     "@types/node": "^12.0.0",
+    "@types/papaparse": "^5.2.6",
     "@types/react": "^17.0.0",
     "@types/react": "^17.0.0",
     "@types/react-dom": "^17.0.0",
     "@types/react-dom": "^17.0.0",
     "@types/react-highlight-words": "^0.16.2",
     "@types/react-highlight-words": "^0.16.2",
@@ -21,6 +22,7 @@
     "axios": "^0.21.1",
     "axios": "^0.21.1",
     "dayjs": "^1.10.5",
     "dayjs": "^1.10.5",
     "i18next": "^20.3.1",
     "i18next": "^20.3.1",
+    "papaparse": "^5.3.1",
     "react": "^17.0.2",
     "react": "^17.0.2",
     "react-app-rewired": "^2.1.8",
     "react-app-rewired": "^2.1.8",
     "react-dom": "^17.0.2",
     "react-dom": "^17.0.2",
@@ -28,6 +30,7 @@
     "react-i18next": "^11.10.0",
     "react-i18next": "^11.10.0",
     "react-router-dom": "^5.2.0",
     "react-router-dom": "^5.2.0",
     "react-scripts": "4.0.3",
     "react-scripts": "4.0.3",
+    "socket.io-client": "^4.1.3",
     "typescript": "^4.1.2",
     "typescript": "^4.1.2",
     "web-vitals": "^1.0.1"
     "web-vitals": "^1.0.1"
   },
   },

+ 4 - 0
client/src/assets/icons/copy.svg

@@ -0,0 +1,4 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M13.3333 6H7.33333C6.59695 6 6 6.59695 6 7.33333V13.3333C6 14.0697 6.59695 14.6667 7.33333 14.6667H13.3333C14.0697 14.6667 14.6667 14.0697 14.6667 13.3333V7.33333C14.6667 6.59695 14.0697 6 13.3333 6Z" stroke="#06AFF2" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3.33301 9.99992H2.66634C2.31272 9.99992 1.97358 9.85944 1.72353 9.60939C1.47348 9.35935 1.33301 9.02021 1.33301 8.66659V2.66659C1.33301 2.31296 1.47348 1.97382 1.72353 1.72378C1.97358 1.47373 2.31272 1.33325 2.66634 1.33325H8.66634C9.01996 1.33325 9.3591 1.47373 9.60915 1.72378C9.8592 1.97382 9.99967 2.31296 9.99967 2.66659V3.33325" stroke="#06AFF2" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 3 - 0
client/src/assets/icons/nav-search.svg

@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M11.165 0.902214C11.5044 1.04952 11.706 1.40291 11.6601 1.76999L10.9438 7.49997H17.4998C17.8232 7.49997 18.1174 7.68702 18.2545 7.97984C18.3917 8.27266 18.347 8.61838 18.14 8.86679L9.8067 18.8668C9.56987 19.151 9.17403 19.245 8.83469 19.0977C8.49534 18.9504 8.29373 18.597 8.33962 18.2299L9.05586 12.5H2.49985C2.1765 12.5 1.88234 12.3129 1.74519 12.0201C1.60804 11.7273 1.65266 11.3815 1.85966 11.1331L10.193 1.13314C10.4298 0.84895 10.8257 0.754907 11.165 0.902214ZM4.27905 10.8333H9.99985C10.2389 10.8333 10.4664 10.9359 10.6246 11.1151C10.7828 11.2943 10.8564 11.5328 10.8267 11.77L10.346 15.6163L15.7206 9.16663H9.99985C9.76082 9.16663 9.5333 9.06399 9.37512 8.8848C9.21693 8.70561 9.1433 8.46712 9.17295 8.22994L9.65373 4.38368L4.27905 10.8333Z" fill="#06AFF2"/>
+</svg>

+ 5 - 0
client/src/assets/icons/search.svg

@@ -0,0 +1,5 @@
+<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M29.29 17.7886C30.0117 16.9137 31.306 16.7894 32.181 17.511C33.0559 18.2326 33.1802 19.5269 32.4586 20.4019L22.9354 31.9485C22.2138 32.8234 20.9195 32.9477 20.0445 32.2261C19.1696 31.5044 19.0453 30.2101 19.7669 29.3352L29.29 17.7886Z" fill="#AEAEBB"/>
+<path d="M22 19.1014L15.3798 17.0207L22 14.8986V11L12 14.7785V19.2215L22 23V19.1014Z" fill="#AEAEBB"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M21 1C9.9543 1 1 9.9543 1 21C1 32.0457 9.9543 41 21 41C25.5938 41 29.8258 39.4512 33.2025 36.8474C33.3392 37.1868 33.545 37.5048 33.8201 37.7799L42.2201 46.1799C43.3136 47.2734 45.0864 47.2734 46.1799 46.1799C47.2734 45.0864 47.2734 43.3136 46.1799 42.2201L37.7799 33.8201C37.5048 33.545 37.1868 33.3392 36.8474 33.2025C39.4512 29.8258 41 25.5938 41 21C41 9.9543 32.0457 1 21 1ZM6.71429 21C6.71429 13.1102 13.1102 6.71429 21 6.71429C28.8898 6.71429 35.2857 13.1102 35.2857 21C35.2857 28.8898 28.8898 35.2857 21 35.2857C13.1102 35.2857 6.71429 28.8898 6.71429 21Z" fill="#AEAEBB"/>
+</svg>

+ 5 - 0
client/src/assets/icons/upload.svg

@@ -0,0 +1,5 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M14 6.66669C13.6318 6.66669 13.3333 6.36821 13.3333 6.00002L13.3333 3.33335C13.3333 3.15654 13.2631 2.98697 13.1381 2.86195C13.0131 2.73692 12.8435 2.66669 12.6667 2.66669L3.33334 2.66669C3.15653 2.66669 2.98696 2.73693 2.86193 2.86195C2.73691 2.98697 2.66667 3.15654 2.66667 3.33335L2.66667 6.00002C2.66667 6.36821 2.36819 6.66669 2 6.66669C1.63181 6.66669 1.33334 6.36821 1.33334 6.00002L1.33334 3.33335C1.33334 2.80292 1.54405 2.29421 1.91912 1.91914C2.2942 1.54407 2.8029 1.33335 3.33334 1.33335L12.6667 1.33335C13.1971 1.33335 13.7058 1.54407 14.0809 1.91914C14.456 2.29421 14.6667 2.80292 14.6667 3.33335L14.6667 6.00002C14.6667 6.36821 14.3682 6.66669 14 6.66669Z" fill="white"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M11.8047 9.80474C11.5444 10.0651 11.1223 10.0651 10.8619 9.80474L8 6.94281L5.13807 9.80474C4.87772 10.0651 4.45561 10.0651 4.19526 9.80474C3.93491 9.54439 3.93491 9.12228 4.19526 8.86193L7.5286 5.5286C7.78894 5.26825 8.21105 5.26825 8.4714 5.5286L11.8047 8.86193C12.0651 9.12228 12.0651 9.54439 11.8047 9.80474Z" fill="white"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M8.00001 14.6667C7.63182 14.6667 7.33334 14.3682 7.33334 14L7.33334 6.00002C7.33334 5.63183 7.63181 5.33335 8 5.33335C8.36819 5.33335 8.66667 5.63183 8.66667 6.00002L8.66667 14C8.66667 14.3682 8.36819 14.6667 8.00001 14.6667Z" fill="white"/>
+</svg>

BIN
client/src/assets/imgs/insert/fail.png


BIN
client/src/assets/imgs/insert/success.png


+ 2 - 2
client/src/components/__test__/customDialog/DialogTemplate.spec.tsx

@@ -14,7 +14,7 @@ describe('test dialog template component', () => {
         <I18nextProvider i18n={i18n}>
         <I18nextProvider i18n={i18n}>
           <DialogTemplate
           <DialogTemplate
             title="dialog template"
             title="dialog template"
-            handleCancel={mockCancelFn}
+            handleClose={mockCancelFn}
             handleConfirm={mockConfirmFn}
             handleConfirm={mockConfirmFn}
           >
           >
             dialog content
             dialog content
@@ -38,7 +38,7 @@ describe('test dialog template component', () => {
         <I18nextProvider i18n={i18n}>
         <I18nextProvider i18n={i18n}>
           <DialogTemplate
           <DialogTemplate
             title="dialog template"
             title="dialog template"
-            handleCancel={mockCancelFn}
+            handleClose={mockCancelFn}
             handleConfirm={mockConfirmFn}
             handleConfirm={mockConfirmFn}
             confirmDisabled={true}
             confirmDisabled={true}
           >
           >

+ 191 - 0
client/src/components/advancedSearch/Condition.tsx

@@ -0,0 +1,191 @@
+import React, { useState, useEffect, FC } from 'react';
+import {
+  makeStyles,
+  Theme,
+  createStyles,
+  IconButton,
+  TextField,
+} from '@material-ui/core';
+import CloseIcon from '@material-ui/icons/Close';
+import { ConditionProps, Field } from './Types';
+import CustomSelector from '../customSelector/CustomSelector';
+
+// Todo: Move to corrsponding Constant file.
+// Static logical operators.
+const LogicalOperators = [
+  {
+    value: '<',
+    label: '<',
+  },
+  {
+    value: '<=',
+    label: '<=',
+  },
+  {
+    value: '>',
+    label: '>',
+  },
+  {
+    value: '>=',
+    label: '>=',
+  },
+  {
+    value: '==',
+    label: '==',
+  },
+  {
+    value: '!=',
+    label: '!=',
+  },
+  {
+    value: 'in',
+    label: 'in',
+  },
+];
+
+const Condition: FC<ConditionProps> = props => {
+  const {
+    onDelete,
+    triggerChange,
+    fields = [],
+    id = '',
+    initData,
+    className = '',
+    ...others
+  } = props;
+  const [operator, setOperator] = useState(
+    initData?.op || LogicalOperators[0].value
+  );
+  const [conditionField, setConditionField] = useState<Field | any>(
+    initData?.field || fields[0] || {}
+  );
+  const [conditionValue, setConditionValue] = useState(initData?.value || '');
+  const [isValuelegal, setIsValueLegal] = useState(
+    initData?.isCorrect || false
+  );
+
+  /**
+   * Check condition's value by field's and operator's type.
+   * Trigger condition change event.
+   */
+  useEffect(() => {
+    const regInt = /^\d+$/;
+    const regFloat = /^\d+\.\d+$/;
+    const regIntInterval = /^\[\d+,\d+\]$/;
+    const regFloatInterval = /^\[\d+\.\d+,\d+\.\d+]$/;
+
+    const type = conditionField?.type;
+    const isIn = operator === 'in';
+    let isLegal = false;
+
+    switch (type) {
+      case 'int':
+        isLegal = isIn
+          ? regIntInterval.test(conditionValue)
+          : regInt.test(conditionValue);
+        break;
+      case 'float':
+        isLegal = isIn
+          ? regFloatInterval.test(conditionValue)
+          : regFloat.test(conditionValue);
+        break;
+      default:
+        isLegal = false;
+        break;
+    }
+    setIsValueLegal(isLegal);
+    triggerChange(id, {
+      field: conditionField,
+      op: operator,
+      value: conditionValue,
+      isCorrect: isLegal,
+      id,
+    });
+    // No need for 'id' and 'triggerChange'.
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [conditionField, operator, conditionValue]);
+
+  const classes = useStyles();
+
+  // Logic operator input change.
+  const handleOpChange = (event: React.ChangeEvent<{ value: unknown }>) => {
+    setOperator(event.target.value);
+  };
+  // Field Name input change.
+  const handleFieldNameChange = (
+    event: React.ChangeEvent<{ value: unknown }>
+  ) => {
+    const value = event.target.value;
+    const target = fields.find(field => field.name === value);
+    target && setConditionField(target);
+  };
+  // Value input change.
+  const handleValueChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+    const value = event.target.value;
+    setConditionValue(value);
+  };
+
+  return (
+    <div className={`${classes.wrapper} ${className}`} {...others}>
+      <CustomSelector
+        label="Field Name"
+        value={conditionField?.name}
+        onChange={handleFieldNameChange}
+        options={fields.map(i => ({ value: i.name, label: i.name }))}
+        variant="filled"
+        wrapperClass={classes.fieldName}
+      />
+      <CustomSelector
+        label="Logic"
+        value={operator}
+        onChange={handleOpChange}
+        options={LogicalOperators}
+        variant="filled"
+        wrapperClass={classes.logic}
+      />
+      <TextField
+        className={classes.value}
+        label="Value"
+        variant="filled"
+        // size="small"
+        onChange={handleValueChange}
+        value={conditionValue}
+        error={!isValuelegal}
+      />
+      <IconButton
+        aria-label="close"
+        className={classes.closeButton}
+        onClick={onDelete}
+        size="small"
+      >
+        <CloseIcon />
+      </IconButton>
+    </div>
+  );
+};
+
+Condition.displayName = 'Condition';
+
+const useStyles = makeStyles((theme: Theme) =>
+  createStyles({
+    root: {},
+    wrapper: {
+      minWidth: '466px',
+      minHeight: '62px',
+      background: '#FFFFFF',
+      padding: '12px 16px',
+      display: 'flex',
+      flexDirection: 'row',
+      alignItems: 'center',
+    },
+    closeButton: {},
+    fieldName: {
+      minHeight: '38px',
+      minWidth: '130px',
+    },
+    logic: { minHeight: '38px', minWidth: '70px', margin: '0 24px' },
+    value: { minHeight: '38px', minWidth: '130px' },
+  })
+);
+
+export default Condition;

+ 234 - 0
client/src/components/advancedSearch/ConditionGroup.tsx

@@ -0,0 +1,234 @@
+import React, { useState, FC } from 'react';
+import { makeStyles, Theme, createStyles, Button } from '@material-ui/core';
+import { ToggleButton, ToggleButtonGroup } from '@material-ui/lab';
+import ConditionItem from './Condition';
+import AddIcon from '@material-ui/icons/Add';
+import {
+  ConditionGroupProps,
+  BinaryLogicalOpProps,
+  AddConditionProps,
+} from './Types';
+
+// "And & or" operator component.
+const BinaryLogicalOp: FC<BinaryLogicalOpProps> = props => {
+  const { onChange, className, initValue = 'and' } = props;
+  const [operator, setOperator] = useState(initValue);
+  const handleChange = (
+    event: React.MouseEvent<HTMLElement>,
+    newOp: string
+  ) => {
+    if (newOp !== null) {
+      setOperator(newOp);
+      onChange(newOp);
+    }
+  };
+  return (
+    <>
+      <div className={`${className} op-${operator}`}>
+        <ToggleButtonGroup
+          value={operator}
+          exclusive
+          onChange={handleChange}
+          aria-label="Binary Logical Operator"
+        >
+          <ToggleButton value="and" aria-label="And">
+            AND
+          </ToggleButton>
+          <ToggleButton value="or" aria-label="Or">
+            OR
+          </ToggleButton>
+        </ToggleButtonGroup>
+        <div className="op-split" />
+      </div>
+    </>
+  );
+};
+
+// "+ Add condition" component.
+const AddCondition: FC<AddConditionProps> = props => {
+  const { className, onClick } = props;
+  return (
+    <Button onClick={onClick} color="primary" className={className}>
+      <AddIcon />
+      Add Condition
+    </Button>
+  );
+};
+
+// Condition group component which contains of BinaryLogicalOp, AddCondition and ConditionItem.
+const ConditionGroup = (props: ConditionGroupProps) => {
+  const {
+    fields = [],
+    handleConditions = {},
+    conditions: flatConditions = [],
+  } = props;
+  const {
+    addCondition,
+    removeCondition,
+    changeBinaryLogicalOp,
+    updateConditionData,
+  } = handleConditions;
+
+  const classes = useStyles();
+
+  const generateClassName = (conditions: any, currentIndex: number) => {
+    let className = '';
+    if (currentIndex === 0 || conditions[currentIndex - 1].type === 'break') {
+      className += 'radius-top';
+    }
+    if (
+      currentIndex === conditions.length - 1 ||
+      conditions[currentIndex + 1].type === 'break'
+    ) {
+      className ? (className = 'radius-all') : (className = 'radius-bottom');
+    }
+    return className;
+  };
+
+  // Generate condition items with operators and add condition btn.
+  const generateConditionItems = (conditions: any[]) => {
+    const conditionsLength = conditions.length;
+    const results = conditions.reduce((prev: any, condition, currentIndex) => {
+      if (currentIndex === conditionsLength - 1) {
+        prev.push(
+          <ConditionItem
+            key={condition.id}
+            id={condition.id}
+            onDelete={() => {
+              removeCondition(condition.id);
+            }}
+            fields={fields}
+            triggerChange={updateConditionData}
+            initData={condition?.data}
+            className={generateClassName(conditions, currentIndex)}
+          />
+        );
+        prev.push(
+          <AddCondition
+            key={`${condition.id}-add`}
+            className={classes.addBtn}
+            onClick={() => {
+              addCondition(condition.id);
+            }}
+          />
+        );
+      } else if (condition.type === 'break') {
+        prev.pop();
+        prev.push(
+          <AddCondition
+            key={`${condition.id}-add`}
+            className={classes.addBtn}
+            onClick={() => {
+              addCondition(condition.id, true);
+            }}
+          />
+        );
+        prev.push(
+          <BinaryLogicalOp
+            key={`${condition.id}-op`}
+            onChange={newOp => {
+              changeBinaryLogicalOp(newOp, condition.id);
+            }}
+            className={classes.binaryLogicOp}
+            initValue="or"
+          />
+        );
+      } else if (condition.type === 'condition') {
+        prev.push(
+          <ConditionItem
+            key={condition.id}
+            id={condition.id}
+            onDelete={() => {
+              removeCondition(condition.id);
+            }}
+            fields={fields}
+            triggerChange={updateConditionData}
+            initData={condition?.data}
+            className={generateClassName(conditions, currentIndex)}
+          />
+        );
+        prev.push(
+          <BinaryLogicalOp
+            key={`${condition.id}-op`}
+            onChange={newOp => {
+              changeBinaryLogicalOp(newOp, condition.id);
+            }}
+            className={classes.binaryLogicOp}
+          />
+        );
+      }
+      return prev;
+    }, []);
+    return results;
+  };
+
+  return (
+    <div className={classes.wrapper}>
+      {generateConditionItems(flatConditions)}
+      {flatConditions?.length === 0 && (
+        <AddCondition
+          className={classes.addBtn}
+          onClick={() => {
+            addCondition();
+          }}
+        />
+      )}
+    </div>
+  );
+};
+
+ConditionGroup.displayName = 'ConditionGroup';
+
+const useStyles = makeStyles((theme: Theme) =>
+  createStyles({
+    root: {},
+    wrapper: {
+      display: 'flex',
+      flexDirection: 'column',
+      alignItems: 'center',
+
+      '& .op-or': {
+        backgroundColor: 'unset',
+        margin: '16px 0',
+      },
+
+      '& .radius-top': {
+        borderRadius: '8px 8px 0 0',
+      },
+      '& .radius-bottom': {
+        borderRadius: '0 0 8px 8px',
+      },
+      '& .radius-all': {
+        borderRadius: '8px',
+      },
+    },
+    addBtn: {},
+    binaryLogicOp: {
+      width: '100%',
+      backgroundColor: '#FFFFFF',
+      display: 'flex',
+      flexDirection: 'row',
+      alignItems: 'center',
+      '& .op-split': {
+        height: '1px',
+        backgroundColor: '#E9E9ED',
+        width: '100%',
+      },
+      '& button': {
+        width: '42px',
+        height: '32px',
+        color: '#010E29',
+      },
+      '& button.Mui-selected': {
+        backgroundColor: '#06AFF2',
+        color: '#FFFFFF',
+      },
+      '& button.Mui-selected:hover': {
+        backgroundColor: '#06AFF2',
+        color: '#FFFFFF',
+      },
+    },
+  })
+);
+
+export default ConditionGroup;

+ 56 - 0
client/src/components/advancedSearch/CopyButton.tsx

@@ -0,0 +1,56 @@
+import React, { useState, FC } from 'react';
+import { makeStyles, Theme, createStyles } from '@material-ui/core';
+import { CopyButtonProps } from './Types';
+import icons from '../icons/Icons';
+import CustomIconButton from '../customButton/CustomIconButton';
+
+const CopyIcon = icons.copyExpression;
+
+const CopyButton: FC<CopyButtonProps> = props => {
+  const {
+    label = 'copy button',
+    icon,
+    className,
+    value = '',
+    ...others
+  } = props;
+  const classes = useStyles();
+  const [tooltipTitle, setTooltipTitle] = useState('Copy');
+
+  const handleClick = (v: string) => {
+    setTooltipTitle('Copied!');
+    navigator.clipboard.writeText(v);
+    setTimeout(() => {
+      setTooltipTitle('Copy');
+    }, 1000);
+  };
+
+  return (
+    <CustomIconButton
+      tooltip={tooltipTitle}
+      aria-label={label}
+      className={`${classes.button} ${className}`}
+      onClick={() => handleClick(value || '')}
+      {...others}
+    >
+      {icon || <CopyIcon style={{ color: 'transparent' }} />}
+    </CustomIconButton>
+  );
+};
+
+CopyButton.displayName = 'CopyButton';
+
+const useStyles = makeStyles((theme: Theme) =>
+  createStyles({
+    root: {},
+    button: {
+      '& svg': {
+        width: '16px',
+        height: '16px',
+      },
+    },
+    tooltip: {},
+  })
+);
+
+export default CopyButton;

+ 220 - 0
client/src/components/advancedSearch/Dialog.tsx

@@ -0,0 +1,220 @@
+import React, { useEffect } from 'react';
+import {
+  makeStyles,
+  Theme,
+  createStyles,
+  Typography,
+  Button,
+  IconButton,
+  Dialog,
+  DialogActions,
+  DialogContent,
+  DialogTitle,
+} from '@material-ui/core';
+import CloseIcon from '@material-ui/icons/Close';
+import CachedIcon from '@material-ui/icons/Cached';
+import ConditionGroup from './ConditionGroup';
+import CopyBtn from './CopyButton';
+// import DialogTemplate from '../customDialog/DialogTemplate';
+import { DialogProps } from './Types';
+
+const AdvancedDialog = (props: DialogProps) => {
+  const {
+    open = false,
+    onClose,
+    onSubmit,
+    onReset,
+    onCancel,
+    handleConditions = {},
+    conditions: flatConditions = [],
+    isLegal = false,
+    expression: filterExpression = '',
+    title = 'Advanced Filter',
+    fields = [],
+    ...others
+  } = props;
+  const { addCondition } = handleConditions;
+  const classes = useStyles();
+
+  useEffect(() => {
+    flatConditions.length === 0 && addCondition();
+    // Only need add one condition after dialog's first mount.
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, []);
+
+  const shouldSowPlaceholder: boolean = !isLegal || !filterExpression;
+
+  return (
+    <>
+      <Dialog
+        onClose={onClose}
+        aria-labelledby="customized-dialog-title"
+        open={open}
+        maxWidth="xl"
+        className={classes.wrapper}
+        {...others}
+      >
+        <DialogTitle className={classes.dialogTitle} disableTypography>
+          <Typography variant="h5" component="h2">
+            {title}
+          </Typography>
+          <IconButton
+            aria-label="close"
+            className={classes.closeButton}
+            onClick={onCancel}
+            size="small"
+          >
+            <CloseIcon />
+          </IconButton>
+        </DialogTitle>
+        <div
+          className={`${classes.expResult} ${
+            shouldSowPlaceholder && 'disable-exp'
+          }`}
+        >
+          {`${shouldSowPlaceholder ? 'Filter Expression' : filterExpression}`}
+          {!shouldSowPlaceholder && (
+            <CopyBtn label="copy expression" value={filterExpression} />
+          )}
+        </div>
+        <DialogContent>
+          <div className={classes.expWrapper}>
+            <ConditionGroup
+              fields={fields}
+              handleConditions={handleConditions}
+              conditions={flatConditions}
+            />
+          </div>
+        </DialogContent>
+        <DialogActions className={classes.dialogActions}>
+          <Button
+            onClick={onReset}
+            color="primary"
+            className={classes.resetBtn}
+            size="small"
+          >
+            <CachedIcon />
+            Reset
+          </Button>
+          <div>
+            <Button
+              autoFocus
+              onClick={onCancel}
+              color="primary"
+              className={classes.cancelBtn}
+            >
+              Cancel
+            </Button>
+            <Button
+              onClick={onSubmit}
+              variant="contained"
+              color="primary"
+              className={classes.applyBtn}
+              disabled={!isLegal}
+            >
+              Apply Filters
+            </Button>
+          </div>
+        </DialogActions>
+      </Dialog>
+      {/* <DialogTemplate
+        title={title}
+        handleClose={onClose}
+        showCloseIcon
+        handleConfirm={onSubmit}
+        confirmLabel="Apply Filters"
+        confirmDisabled={!isLegal}
+        handleCancel={onCancel}
+        cancelLabel="Cancel"
+        leftActions={
+          <Button
+            onClick={onReset}
+            color="primary"
+            className={classes.resetBtn}
+            size="small"
+          >
+            <CachedIcon />
+            Reset
+          </Button>
+        }
+      >
+        <div
+          className={`${classes.expResult} ${
+            !isLegal && 'disable-exp'
+          } testcopy`}
+        >
+          {`${isLegal ? filterExpression : 'Filter Expression'}`}
+          {isLegal && (
+            <CopyBtn label="copy expression" value={filterExpression} />
+          )}
+        </div>
+        <div className={classes.expWrapper}>
+          <ConditionGroup
+            fields={fields}
+            handleConditions={handleConditions}
+            conditions={flatConditions}
+          />
+        </div>
+      </DialogTemplate> */}
+    </>
+  );
+};
+
+AdvancedDialog.displayName = 'AdvancedDialog';
+
+const useStyles = makeStyles((theme: Theme) =>
+  createStyles({
+    root: {},
+    wrapper: {
+      '& .disable-exp': {
+        userSelect: 'none',
+        color: '#AEAEBB',
+      },
+    },
+    closeButton: {
+      color: 'black',
+    },
+    dialogTitle: {
+      display: 'flex',
+      alignItems: 'center',
+      justifyContent: 'space-between',
+    },
+    dialogActions: {
+      justifyContent: 'space-between',
+    },
+    resetBtn: {},
+    cancelBtn: {
+      marginRight: '32px',
+    },
+    applyBtn: {
+      backgroundColor: '#06AFF2',
+      color: 'white',
+    },
+    copyButton: {
+      borderRadius: '0',
+    },
+    expResult: {
+      background: '#F9F9F9',
+      borderRadius: '8px',
+      display: 'flex',
+      justifyContent: 'space-between',
+      alignItems: 'center',
+      minHeight: '40px',
+      margin: '8px 32px',
+      padding: '0 16px',
+      fontStyle: 'normal',
+      fontWeight: 'normal',
+      fontSize: '16px',
+      lineHeight: '24px',
+    },
+    expWrapper: {
+      background: '#F9F9F9',
+      borderRadius: '8px',
+      minWidth: '480px',
+      minHeight: '104px',
+      padding: '12px',
+    },
+  })
+);
+
+export default AdvancedDialog;

+ 320 - 0
client/src/components/advancedSearch/Filter.tsx

@@ -0,0 +1,320 @@
+import { useState, useEffect } from 'react';
+import {
+  makeStyles,
+  Theme,
+  createStyles,
+  Chip,
+  Tooltip,
+} from '@material-ui/core';
+import FilterListIcon from '@material-ui/icons/FilterList';
+import AdvancedDialog from './Dialog';
+import { FilterProps, ConditionData } from './Types';
+import { generateIdByHash } from '../../utils/Common';
+import CustomButton from '../customButton/CustomButton';
+
+const Filter = function Filter(props: FilterProps) {
+  const {
+    title = 'title',
+    showTitle = true,
+    className = '',
+    filterDisabled = false,
+    tooltipPlacement = 'top',
+    onSubmit,
+    fields = [],
+    ...others
+  } = props;
+  const classes = useStyles();
+
+  const [open, setOpen] = useState(false);
+  const [flatConditions, setFlatConditions] = useState<any[]>([]);
+  const [initConditions, setInitConditions] = useState<any[]>([]);
+  const [isConditionsLegal, setIsConditionsLegal] = useState(false);
+  const [filterExpression, setFilterExpression] = useState('');
+
+  // if fields if empty array, reset all conditions
+  useEffect(() => {
+    if (fields.length === 0) {
+      setFlatConditions([]);
+      setInitConditions([]);
+    }
+  }, [fields]);
+
+  // Check all conditions are all correct.
+  useEffect(() => {
+    // Calc the sum of conditions.
+    for (let i = 0; i < flatConditions.length; i++) {
+      const { data, type } = flatConditions[i];
+      if (type !== 'condition') continue;
+      if (!data) {
+        setIsConditionsLegal(false);
+        return;
+      }
+      if (!data.isCorrect) {
+        setIsConditionsLegal(data.isCorrect);
+        return;
+      }
+    }
+    setIsConditionsLegal(true);
+    generateExpression(flatConditions, setFilterExpression);
+  }, [flatConditions]);
+
+  const setFilteredFlatConditions = (conditions: any[]) => {
+    const newConditions = conditions.reduce((prev, item, currentIndex) => {
+      if (prev.length === 0 && item.type !== 'condition') return prev;
+      if (
+        prev.length &&
+        item.type !== 'condition' &&
+        prev[prev.length - 1].type !== 'condition'
+      )
+        return prev;
+      return [...prev, item];
+    }, []);
+    setFlatConditions(newConditions);
+  };
+
+  const generateExpression = (conditions: any[], func: any) => {
+    const expression = conditions.reduce((prev, item) => {
+      const { type, data } = item;
+      if (type === 'break') return `${prev} || `;
+      const {
+        field: { name },
+        op,
+        value,
+      } = data;
+      return `${prev}${
+        prev && !prev.endsWith('|| ') ? ' && ' : ''
+      }${name} ${op} ${value}`;
+    }, '');
+    func(expression);
+  };
+
+  /**
+   * Insert "OR" operator into specified position.
+   * @param targetId The break operator will be inserted after the target one.
+   */
+  const addOrOp = (targetId?: string) => {
+    if (!targetId) {
+      setFilteredFlatConditions([
+        ...flatConditions,
+        { id: generateIdByHash('break'), type: 'break' },
+        {
+          id: generateIdByHash('condition'),
+          type: 'condition',
+          field: '',
+          operator: '<',
+          value: '',
+        },
+      ]);
+      return;
+    }
+    const formerConditons = [...flatConditions];
+    const newConditions = formerConditons.reduce((prev, item) => {
+      if (item.id === targetId) {
+        return [
+          ...prev,
+          item,
+          { id: generateIdByHash('break'), type: 'break' },
+        ];
+      }
+      return [...prev, item];
+    }, []);
+    setFilteredFlatConditions(newConditions);
+  };
+  /**
+   * Remove "OR" operator in specified position.
+   * @param targetId Remove the break operator after the target one.
+   */
+  const removeOrOp = (targetId: string) => {
+    setFilteredFlatConditions(flatConditions.filter(i => i.id !== targetId));
+  };
+  /**
+   * Add new condition to specified position.
+   * @param targetId Position where new condition item will be inserted.
+   * Will be pushed to tail if empty.
+   * @param beforeTarget Will be inserted before the target item.
+   */
+  const addCondition = (targetId?: string, beforeTarget?: boolean) => {
+    const formerConditons = [...flatConditions];
+    const newItem = {
+      id: generateIdByHash('condition'),
+      type: 'condition',
+      field: '',
+      operator: '<',
+      value: '',
+    };
+    if (!targetId) {
+      formerConditons.push(newItem);
+      setFilteredFlatConditions(formerConditons);
+      return;
+    }
+    const newConditions = formerConditons.reduce((prev, item) => {
+      if (item.id === targetId) {
+        const newItems = [
+          item,
+          {
+            id: generateIdByHash('condition'),
+            type: 'condition',
+            field: '',
+            operator: '<',
+            value: '',
+          },
+        ];
+        beforeTarget && newItems.reverse();
+        return [...prev, ...newItems];
+      }
+      return [...prev, item];
+    }, []);
+    setIsConditionsLegal(false);
+    setFilteredFlatConditions(newConditions);
+  };
+  /**
+   * Remove condition from specified position.
+   * @param targetId Position where new condition item will be removed.
+   */
+  const removeCondition = (targetId: string) => {
+    setFilteredFlatConditions(flatConditions.filter(i => i.id !== targetId));
+  };
+  const changeBinaryLogicalOp = (value: string, targetId: string) => {
+    if (value === 'or') {
+      addOrOp(targetId);
+    } else if (value === 'and') {
+      removeOrOp(targetId);
+    }
+  };
+  /**
+   * Update specified condition's data.
+   * @param id Specify one item that will be updated.
+   * @param data Data that will be updated to condition.
+   */
+  const updateConditionData = (id: string, data: ConditionData) => {
+    const formerFlatConditions = flatConditions.map(i => {
+      if (i.id === id) return { ...i, data };
+      return i;
+    });
+    setFilteredFlatConditions(formerFlatConditions);
+  };
+  // Reset conditions.
+  const resetConditions = () => {
+    setIsConditionsLegal(false);
+    setFilteredFlatConditions([
+      {
+        id: generateIdByHash(),
+        type: 'condition',
+        field: '',
+        operator: '<',
+        value: '',
+      },
+    ]);
+  };
+  const getConditionsSum = () => {
+    const conds = flatConditions.filter(i => i.type === 'condition');
+    return conds.length;
+  };
+
+  const handleConditions = {
+    addOrOp,
+    removeOrOp,
+    addCondition,
+    removeCondition,
+    changeBinaryLogicalOp,
+    updateConditionData,
+    resetConditions,
+    getConditionsSum,
+    setFilteredFlatConditions,
+    setIsConditionsLegal,
+    setFilterExpression,
+  };
+
+  const handleClickOpen = () => {
+    setOpen(true);
+  };
+  const handleClose = () => {
+    setOpen(false);
+  };
+  const handleDeleteAll = () => {
+    setFlatConditions([]);
+    setInitConditions([]);
+    onSubmit('');
+  };
+  const handleCancel = () => {
+    setOpen(false);
+    handleFallback();
+  };
+  const handleSubmit = () => {
+    onSubmit(filterExpression);
+    setInitConditions(flatConditions);
+    setOpen(false);
+  };
+  const handleReset = () => {
+    setFilteredFlatConditions([
+      {
+        id: generateIdByHash('condition'),
+        type: 'condition',
+        field: '',
+        operator: '<',
+        value: '',
+      },
+    ]);
+  };
+  const handleFallback = () => {
+    setFilteredFlatConditions(initConditions);
+  };
+
+  return (
+    <>
+      <div className={`${classes.wrapper} ${className}`} {...others}>
+        <CustomButton
+          disabled={filterDisabled}
+          className={`${classes.afBtn} af-btn`}
+          onClick={handleClickOpen}
+        >
+          <FilterListIcon />
+          {showTitle ? title : ''}
+        </CustomButton>
+        {initConditions.length > 0 && (
+          <Tooltip
+            arrow
+            interactive
+            title={filterExpression}
+            placement={tooltipPlacement}
+          >
+            <Chip
+              label={initConditions.filter(i => i.type === 'condition').length}
+              onDelete={handleDeleteAll}
+              variant="outlined"
+              size="small"
+            />
+          </Tooltip>
+        )}
+        {open && (
+          <AdvancedDialog
+            open={open}
+            onClose={handleClose}
+            onCancel={handleCancel}
+            onSubmit={handleSubmit}
+            onReset={handleReset}
+            title="Advanced Filter"
+            fields={fields}
+            handleConditions={handleConditions}
+            conditions={flatConditions}
+            isLegal={isConditionsLegal}
+            expression={filterExpression}
+          />
+        )}
+      </div>
+    </>
+  );
+};
+
+Filter.displayName = 'AdvancedFilter';
+
+const useStyles = makeStyles((theme: Theme) =>
+  createStyles({
+    wrapper: {},
+    afBtn: {
+      color: '#06AFF2',
+    },
+  })
+);
+
+export default Filter;

+ 83 - 0
client/src/components/advancedSearch/Types.ts

@@ -0,0 +1,83 @@
+// import { ReactElement } from 'react';
+
+export interface ConditionProps {
+  others?: object;
+  onDelete: () => void;
+  triggerChange: (id: string, data: TriggerChangeData) => void;
+  fields: Field[];
+  id: string;
+  initData: any;
+  className?: string;
+}
+
+export interface Field {
+  name: string;
+  type: 'int' | 'float';
+}
+
+export interface TriggerChangeData {
+  field: Field;
+  op: string;
+  value: string;
+  isCorrect: boolean;
+  id: string;
+}
+
+export interface ConditionGroupProps {
+  others?: object;
+  fields: Field[];
+  handleConditions: any;
+  conditions: any[];
+}
+
+export interface BinaryLogicalOpProps {
+  onChange: (newOp: string) => void;
+  className?: string;
+  initValue?: string;
+}
+
+export interface AddConditionProps {
+  className?: string;
+  onClick?: () => void;
+}
+
+export interface CopyButtonProps {
+  className?: string;
+  icon?: any;
+  label: string;
+  value: string;
+  others?: any;
+}
+
+export interface DialogProps {
+  others?: object;
+  open: boolean;
+  onClose: () => void;
+  onSubmit: (data: any) => void;
+  onReset: () => void;
+  onCancel: () => void;
+  title: string;
+  fields: Field[];
+  handleConditions: any;
+  conditions: any[];
+  isLegal: boolean;
+  expression: string;
+}
+
+export interface FilterProps {
+  className?: string;
+  title: string;
+  showTitle?: boolean;
+  filterDisabled?: boolean;
+  others?: object;
+  onSubmit: (data: any) => void;
+  tooltipPlacement?: 'left' | 'right' | 'bottom' | 'top';
+  fields: Field[];
+}
+
+export interface ConditionData {
+  field: Field;
+  op: string;
+  value: string;
+  isCorrect: boolean;
+}

+ 3 - 0
client/src/components/advancedSearch/index.tsx

@@ -0,0 +1,3 @@
+import Filter from './Filter';
+
+export default Filter;

+ 1 - 1
client/src/components/customButton/CustomButton.tsx

@@ -19,7 +19,7 @@ const buttonStyle = makeStyles(theme => ({
     backgroundColor: theme.palette.primary.main,
     backgroundColor: theme.palette.primary.main,
     boxShadow: 'initial',
     boxShadow: 'initial',
     fontWeight: 'bold',
     fontWeight: 'bold',
-    lineHeight: '16px',
+    lineHeight: '24px',
     '&:hover': {
     '&:hover': {
       backgroundColor: theme.palette.primary.dark,
       backgroundColor: theme.palette.primary.dark,
       boxShadow: 'initial',
       boxShadow: 'initial',

+ 15 - 3
client/src/components/customDialog/CustomDialogTitle.tsx

@@ -17,6 +17,9 @@ const getStyles = makeStyles((theme: Theme) => ({
   // closeButton: {
   // closeButton: {
   //   padding: theme.spacing(1),
   //   padding: theme.spacing(1),
   // },
   // },
+  title: {
+    fontWeight: 500,
+  },
   icon: {
   icon: {
     fontSize: '24px',
     fontSize: '24px',
     color: '#010e29',
     color: '#010e29',
@@ -27,10 +30,17 @@ const getStyles = makeStyles((theme: Theme) => ({
 
 
 interface IProps extends DialogTitleProps {
 interface IProps extends DialogTitleProps {
   onClose?: () => void;
   onClose?: () => void;
+  showCloseIcon?: boolean;
 }
 }
 
 
 const CustomDialogTitle = (props: IProps) => {
 const CustomDialogTitle = (props: IProps) => {
-  const { children, classes = { root: '' }, onClose, ...other } = props;
+  const {
+    children,
+    classes = { root: '' },
+    onClose,
+    showCloseIcon = true,
+    ...other
+  } = props;
   const innerClass = getStyles();
   const innerClass = getStyles();
 
 
   const ClearIcon = icons.clear;
   const ClearIcon = icons.clear;
@@ -41,8 +51,10 @@ const CustomDialogTitle = (props: IProps) => {
       className={`${innerClass.root} ${classes.root}`}
       className={`${innerClass.root} ${classes.root}`}
       {...other}
       {...other}
     >
     >
-      <Typography variant="h5">{children}</Typography>
-      {onClose ? (
+      <Typography variant="h4" className={innerClass.title}>
+        {children}
+      </Typography>
+      {showCloseIcon && onClose ? (
         <ClearIcon
         <ClearIcon
           data-testid="clear-icon"
           data-testid="clear-icon"
           classes={{ root: innerClass.icon }}
           classes={{ root: innerClass.icon }}

+ 31 - 15
client/src/components/customDialog/DialogTemplate.tsx

@@ -13,41 +13,57 @@ import CustomButton from '../customButton/CustomButton';
 const useStyles = makeStyles((theme: Theme) => ({
 const useStyles = makeStyles((theme: Theme) => ({
   actions: {
   actions: {
     paddingTop: theme.spacing(2),
     paddingTop: theme.spacing(2),
+    justifyContent: 'space-between',
   },
   },
 }));
 }));
 
 
 const DialogTemplate: FC<DialogContainerProps> = ({
 const DialogTemplate: FC<DialogContainerProps> = ({
   title,
   title,
   cancelLabel,
   cancelLabel,
+  handleClose,
   handleCancel,
   handleCancel,
   confirmLabel,
   confirmLabel,
   handleConfirm,
   handleConfirm,
   confirmDisabled,
   confirmDisabled,
   children,
   children,
+  showActions = true,
+  showCancel = true,
+  showCloseIcon = true,
+  leftActions,
 }) => {
 }) => {
   const { t } = useTranslation('btn');
   const { t } = useTranslation('btn');
   const cancel = cancelLabel || t('cancel');
   const cancel = cancelLabel || t('cancel');
   const confirm = confirmLabel || t('confirm');
   const confirm = confirmLabel || t('confirm');
   const classes = useStyles();
   const classes = useStyles();
+  const onCancel = handleCancel || handleClose;
 
 
   return (
   return (
     <>
     <>
-      <CustomDialogTitle onClose={handleCancel}>{title}</CustomDialogTitle>
+      <CustomDialogTitle onClose={handleClose} showCloseIcon={showCloseIcon}>
+        {title}
+      </CustomDialogTitle>
       <DialogContent>{children}</DialogContent>
       <DialogContent>{children}</DialogContent>
-      <DialogActions className={classes.actions}>
-        <CustomButton onClick={handleCancel} color="default" name="cancel">
-          {cancel}
-        </CustomButton>
-        <CustomButton
-          variant="contained"
-          onClick={handleConfirm}
-          color="primary"
-          disabled={confirmDisabled}
-          name="confirm"
-        >
-          {confirm}
-        </CustomButton>
-      </DialogActions>
+      {showActions && (
+        <DialogActions className={classes.actions}>
+          <div>{leftActions}</div>
+          <div>
+            {showCancel && (
+              <CustomButton onClick={onCancel} color="default" name="cancel">
+                {cancel}
+              </CustomButton>
+            )}
+            <CustomButton
+              variant="contained"
+              onClick={handleConfirm}
+              color="primary"
+              disabled={confirmDisabled}
+              name="confirm"
+            >
+              {confirm}
+            </CustomButton>
+          </div>
+        </DialogActions>
+      )}
     </>
     </>
   );
   );
 };
 };

+ 9 - 3
client/src/components/customDialog/Types.ts

@@ -1,3 +1,4 @@
+import { ReactElement } from 'react';
 import { DialogType } from '../../context/Types';
 import { DialogType } from '../../context/Types';
 export type CustomDialogType = DialogType & {
 export type CustomDialogType = DialogType & {
   onClose: () => void;
   onClose: () => void;
@@ -21,9 +22,14 @@ export type DeleteDialogContentType = {
 
 
 export type DialogContainerProps = {
 export type DialogContainerProps = {
   title: string;
   title: string;
-  cancelLabel?: string;
-  confirmLabel?: string;
-  handleCancel: () => void;
+  cancelLabel?: string | ReactElement;
+  confirmLabel?: string | ReactElement;
+  showCloseIcon?: boolean;
+  handleClose: () => void;
+  handleCancel?: () => void;
   handleConfirm: (param: any) => void;
   handleConfirm: (param: any) => void;
   confirmDisabled?: boolean;
   confirmDisabled?: boolean;
+  showActions?: boolean;
+  showCancel?: boolean;
+  leftActions?: ReactElement;
 };
 };

+ 19 - 24
client/src/components/customSelector/CustomSelector.tsx

@@ -1,39 +1,34 @@
 import { FC } from 'react';
 import { FC } from 'react';
-import {
-  createStyles,
-  FormControl,
-  InputLabel,
-  makeStyles,
-  MenuItem,
-  Select,
-  Theme,
-} from '@material-ui/core';
+import { FormControl, InputLabel, MenuItem, Select } from '@material-ui/core';
 import { CustomSelectorType } from './Types';
 import { CustomSelectorType } from './Types';
 import { generateId } from '../../utils/Common';
 import { generateId } from '../../utils/Common';
 
 
-const useStyles = makeStyles((theme: Theme) =>
-  createStyles({
-    label: {
-      // textTransform: 'capitalize',
-    },
-  })
-);
-
 /**
 /**
  *  label: We may need label lowecase or capitalize, so we cant control css inside.
  *  label: We may need label lowecase or capitalize, so we cant control css inside.
  * */
  * */
 const CustomSelector: FC<CustomSelectorType> = props => {
 const CustomSelector: FC<CustomSelectorType> = props => {
-  const { label, value, onChange, options, classes, variant, ...others } =
-    props;
+  const {
+    label,
+    value,
+    onChange,
+    options,
+    classes,
+    variant,
+    wrapperClass = '',
+    labelClass = '',
+    ...others
+  } = props;
   const id = generateId('selector');
   const id = generateId('selector');
-  const selectorClasses = useStyles();
 
 
   return (
   return (
-    <FormControl variant={variant} classes={classes}>
-      <InputLabel htmlFor={id} className={selectorClasses.label}>
-        {label}
-      </InputLabel>
+    <FormControl variant={variant} className={wrapperClass}>
+      {label && (
+        <InputLabel classes={{ root: labelClass }} htmlFor={id}>
+          {label}
+        </InputLabel>
+      )}
       <Select
       <Select
+        classes={classes}
         {...others}
         {...others}
         value={value}
         value={value}
         onChange={onChange}
         onChange={onChange}

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

@@ -12,12 +12,14 @@ export interface GroupOption {
 }
 }
 
 
 export type CustomSelectorType = SelectProps & {
 export type CustomSelectorType = SelectProps & {
-  label: string;
+  label?: string;
   value: string | number;
   value: string | number;
   options: Option[];
   options: Option[];
   onChange: (e: React.ChangeEvent<{ value: unknown }>) => void;
   onChange: (e: React.ChangeEvent<{ value: unknown }>) => void;
   classes?: Partial<ClassNameMap<FormControlClassKey>>;
   classes?: Partial<ClassNameMap<FormControlClassKey>>;
   variant?: 'filled' | 'outlined' | 'standard';
   variant?: 'filled' | 'outlined' | 'standard';
+  labelClass?: string;
+  wrapperClass?: string;
 };
 };
 
 
 export interface ICustomGroupSelect {
 export interface ICustomGroupSelect {

+ 6 - 0
client/src/components/grid/Grid.tsx

@@ -100,9 +100,12 @@ const MilvusGrid: FC<MilvusGridType> = props => {
     disableSelect = false,
     disableSelect = false,
     noData = gridTrans.noData,
     noData = gridTrans.noData,
     showHoverStyle = true,
     showHoverStyle = true,
+    headEditable = false,
+    editHeads = [],
     selected = [],
     selected = [],
     setSelected = () => {},
     setSelected = () => {},
     setRowsPerPage = () => {},
     setRowsPerPage = () => {},
+    tableCellMaxWidth,
   } = props;
   } = props;
 
 
   const _isSelected = (row: { [x: string]: any }) => {
   const _isSelected = (row: { [x: string]: any }) => {
@@ -208,6 +211,9 @@ const MilvusGrid: FC<MilvusGridType> = props => {
           showHoverStyle={showHoverStyle}
           showHoverStyle={showHoverStyle}
           isLoading={isLoading}
           isLoading={isLoading}
           setPageSize={setRowsPerPage}
           setPageSize={setRowsPerPage}
+          headEditable={headEditable}
+          editHeads={editHeads}
+          tableCellMaxWidth={tableCellMaxWidth}
         ></Table>
         ></Table>
         {rowCount ? (
         {rowCount ? (
           <TablePagination
           <TablePagination

+ 28 - 13
client/src/components/grid/Table.tsx

@@ -10,6 +10,7 @@ import Checkbox from '@material-ui/core/Checkbox';
 import { TableType } from './Types';
 import { TableType } from './Types';
 import { Box, Button, Typography } from '@material-ui/core';
 import { Box, Button, Typography } from '@material-ui/core';
 import EnhancedTableHead from './TableHead';
 import EnhancedTableHead from './TableHead';
+import EditableTableHead from './TableEditableHead';
 import { stableSort, getComparator } from './Utils';
 import { stableSort, getComparator } from './Utils';
 import Copy from '../../components/copy/Copy';
 import Copy from '../../components/copy/Copy';
 import ActionBar from './ActionBar';
 import ActionBar from './ActionBar';
@@ -86,7 +87,8 @@ const useStyles = makeStyles(theme => ({
       overflow: 'hidden',
       overflow: 'hidden',
       textOverflow: 'ellipsis',
       textOverflow: 'ellipsis',
       whiteSpace: 'nowrap',
       whiteSpace: 'nowrap',
-      maxWidth: '300px',
+      maxWidth: (props: { tableCellMaxWidth: string }) =>
+        props.tableCellMaxWidth,
       fontSize: '14px',
       fontSize: '14px',
       lineHeight: '20px',
       lineHeight: '20px',
     },
     },
@@ -108,14 +110,23 @@ const EnhancedTable: FC<TableType> = props => {
     rows = [],
     rows = [],
     colDefinitions,
     colDefinitions,
     primaryKey,
     primaryKey,
+    // whether show checkbox in the first column
+    // set true as default
     openCheckBox = true,
     openCheckBox = true,
     disableSelect,
     disableSelect,
     noData,
     noData,
-    showHoverStyle,
+    // whether change table row background color when mouse hover
+    // set true as default
+    showHoverStyle = true,
     isLoading,
     isLoading,
     setPageSize,
     setPageSize,
+    headEditable = false,
+    // editable heads required param, contains heads components and its value
+    editHeads = [],
+    // if table cell max width not be passed, table row will use 300px as default
+    tableCellMaxWidth = '300px',
   } = props;
   } = props;
-  const classes = useStyles();
+  const classes = useStyles({ tableCellMaxWidth });
   const [order, setOrder] = React.useState('asc');
   const [order, setOrder] = React.useState('asc');
   const [orderBy, setOrderBy] = React.useState<string>('');
   const [orderBy, setOrderBy] = React.useState<string>('');
   const [tableMouseStatus, setTableMouseStatus] = React.useState<boolean[]>([]);
   const [tableMouseStatus, setTableMouseStatus] = React.useState<boolean[]>([]);
@@ -165,16 +176,20 @@ const EnhancedTable: FC<TableType> = props => {
           size="medium"
           size="medium"
           aria-label="enhanced table"
           aria-label="enhanced table"
         >
         >
-          <EnhancedTableHead
-            colDefinitions={colDefinitions}
-            numSelected={selected.length}
-            order={order}
-            orderBy={orderBy}
-            onSelectAllClick={onSelectedAll}
-            onRequestSort={handleRequestSort}
-            rowCount={rows.length}
-            openCheckBox={openCheckBox}
-          />
+          {!headEditable ? (
+            <EnhancedTableHead
+              colDefinitions={colDefinitions}
+              numSelected={selected.length}
+              order={order}
+              orderBy={orderBy}
+              onSelectAllClick={onSelectedAll}
+              onRequestSort={handleRequestSort}
+              rowCount={rows.length}
+              openCheckBox={openCheckBox}
+            />
+          ) : (
+            <EditableTableHead editHeads={editHeads} />
+          )}
           {!isLoading && (
           {!isLoading && (
             <TableBody>
             <TableBody>
               {rows && rows.length ? (
               {rows && rows.length ? (

+ 36 - 0
client/src/components/grid/TableEditableHead.tsx

@@ -0,0 +1,36 @@
+import { FC } from 'react';
+import { TableEditableHeadType } from './Types';
+import { TableHead, TableRow, TableCell, makeStyles } from '@material-ui/core';
+
+const useStyles = makeStyles(theme => ({
+  tableCell: {
+    paddingLeft: theme.spacing(2),
+  },
+  tableHeader: {
+    textTransform: 'capitalize',
+    color: 'rgba(0, 0, 0, 0.6)',
+    fontSize: '12.8px',
+  },
+  tableRow: {
+    // borderBottom: '1px solid rgba(0, 0, 0, 0.6);',
+  },
+}));
+
+const EditableTableHead: FC<TableEditableHeadType> = props => {
+  const { editHeads } = props;
+  const classes = useStyles();
+
+  return (
+    <TableHead>
+      <TableRow className={classes.tableRow}>
+        {editHeads.map((headCell, index) => (
+          <TableCell key={index} className={classes.tableCell}>
+            {headCell.component}
+          </TableCell>
+        ))}
+      </TableRow>
+    </TableHead>
+  );
+};
+
+export default EditableTableHead;

+ 9 - 5
client/src/components/grid/TableHead.tsx

@@ -74,20 +74,24 @@ const EnhancedTableHead: FC<TableHeadType> = props => {
             key={headCell.id}
             key={headCell.id}
             align={headCell.align || 'left'}
             align={headCell.align || 'left'}
             padding={headCell.disablePadding ? 'none' : 'default'}
             padding={headCell.disablePadding ? 'none' : 'default'}
-            sortDirection={orderBy === headCell.id ? order : false}
+            sortDirection={
+              orderBy === (headCell.sortBy || headCell.id) ? order : false
+            }
             className={classes.tableCell}
             className={classes.tableCell}
           >
           >
             {headCell.label && !headCell.notSort ? (
             {headCell.label && !headCell.notSort ? (
               <TableSortLabel
               <TableSortLabel
-                active={orderBy === headCell.id}
-                direction={orderBy === headCell.id ? order : 'asc'}
-                onClick={createSortHandler(headCell.id)}
+                active={orderBy === (headCell.sortBy || headCell.id)}
+                direction={
+                  orderBy === (headCell.sortBy || headCell.id) ? order : 'asc'
+                }
+                onClick={createSortHandler(headCell.sortBy || headCell.id)}
               >
               >
                 <Typography variant="body1" className={classes.tableHeader}>
                 <Typography variant="body1" className={classes.tableHeader}>
                   {headCell.label}
                   {headCell.label}
                 </Typography>
                 </Typography>
 
 
-                {orderBy === headCell.id ? (
+                {orderBy === (headCell.sortBy || headCell.id) ? (
                   <Typography className={classes.visuallyHidden}>
                   <Typography className={classes.visuallyHidden}>
                     {order === 'desc'
                     {order === 'desc'
                       ? 'sorted descending'
                       ? 'sorted descending'

+ 6 - 3
client/src/components/grid/ToolBar.tsx

@@ -69,6 +69,8 @@ const CustomToolBar: FC<ToolBarType> = props => {
 
 
             const Icon = c.icon ? Icons[c.icon!]() : '';
             const Icon = c.icon ? Icons[c.icon!]() : '';
             const disabled = c.disabled ? c.disabled(selected) : false;
             const disabled = c.disabled ? c.disabled(selected) : false;
+            // when disabled "disabledTooltip" will replace "tooltip"
+            const tooltip = disabled && c.disabledTooltip ? c.disabledTooltip : c.tooltip;
             const isIcon = c.type === 'iconBtn';
             const isIcon = c.type === 'iconBtn';
 
 
             const btn = (
             const btn = (
@@ -79,8 +81,9 @@ const CustomToolBar: FC<ToolBarType> = props => {
                 startIcon={Icon}
                 startIcon={Icon}
                 color="primary"
                 color="primary"
                 disabled={disabled}
                 disabled={disabled}
-                variant="contained"
-                tooltip={c.tooltip}
+                // use contained variant as default
+                variant={c.btnVariant || 'contained'}
+                tooltip={tooltip}
                 className={classes.btn}
                 className={classes.btn}
               >
               >
                 <Typography variant="button">{c.label}</Typography>
                 <Typography variant="button">{c.label}</Typography>
@@ -91,7 +94,7 @@ const CustomToolBar: FC<ToolBarType> = props => {
               <CustomIconButton
               <CustomIconButton
                 key={i}
                 key={i}
                 onClick={c.onClick}
                 onClick={c.onClick}
-                tooltip={c.tooltip}
+                tooltip={tooltip}
                 disabled={disabled}
                 disabled={disabled}
               >
               >
                 {Icon}
                 {Icon}

+ 22 - 0
client/src/components/grid/Types.ts

@@ -36,10 +36,13 @@ export type ToolBarConfig = Partial<TableSwitchType> &
     onClick?: (arg0: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
     onClick?: (arg0: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
     disabled?: (data: any[]) => boolean;
     disabled?: (data: any[]) => boolean;
     tooltip?: string;
     tooltip?: string;
+    // when disabled "disabledTooltip" will replace "tooltip"
+    disabledTooltip?: string;
     hidden?: boolean;
     hidden?: boolean;
     type?: 'iconBtn' | 'buttton' | 'switch' | 'select' | 'groupSelect';
     type?: 'iconBtn' | 'buttton' | 'switch' | 'select' | 'groupSelect';
     position?: 'right' | 'left';
     position?: 'right' | 'left';
     component?: ReactElement;
     component?: ReactElement;
+    btnVariant?: 'contained' | 'outlined' | 'text';
   };
   };
 
 
 export type TableHeadType = {
 export type TableHeadType = {
@@ -53,6 +56,15 @@ export type TableHeadType = {
   openCheckBox?: boolean;
   openCheckBox?: boolean;
 };
 };
 
 
+export type TableEditableHeadType = {
+  editHeads: EditableHeads[];
+};
+
+export type EditableHeads = {
+  component: ReactElement;
+  value: string;
+};
+
 export type TableType = {
 export type TableType = {
   selected: any[];
   selected: any[];
   onSelected: (e: React.MouseEvent, row: any) => void;
   onSelected: (e: React.MouseEvent, row: any) => void;
@@ -67,6 +79,10 @@ export type TableType = {
   showHoverStyle?: boolean;
   showHoverStyle?: boolean;
   isLoading?: boolean;
   isLoading?: boolean;
   setPageSize?: (size: number) => void;
   setPageSize?: (size: number) => void;
+  headEditable?: boolean;
+  editHeads: EditableHeads[];
+  // with unit like '20px'
+  tableCellMaxWidth?: string;
 };
 };
 
 
 export type ColDefinitionsType = {
 export type ColDefinitionsType = {
@@ -78,6 +94,8 @@ export type ColDefinitionsType = {
   showActionCell?: boolean;
   showActionCell?: boolean;
   isHoverAction?: boolean;
   isHoverAction?: boolean;
   notSort?: boolean;
   notSort?: boolean;
+  // custom sort rule property, default is row id
+  sortBy?: string;
   onClick?: (
   onClick?: (
     e: React.MouseEvent<HTMLButtonElement, MouseEvent>,
     e: React.MouseEvent<HTMLButtonElement, MouseEvent>,
     data?: any
     data?: any
@@ -112,6 +130,10 @@ export type MilvusGridType = ToolBarType & {
   disableSelect?: boolean;
   disableSelect?: boolean;
   noData?: string;
   noData?: string;
   showHoverStyle?: boolean;
   showHoverStyle?: boolean;
+  headEditable?: boolean;
+  editHeads?: EditableHeads[];
+  // with unit like '20px'
+  tableCellMaxWidth?: string;
 };
 };
 
 
 export type ActionBarType = {
 export type ActionBarType = {

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

@@ -18,6 +18,9 @@ import ArrowBackIosIcon from '@material-ui/icons/ArrowBackIos';
 import ExitToAppIcon from '@material-ui/icons/ExitToApp';
 import ExitToAppIcon from '@material-ui/icons/ExitToApp';
 import ArrowForwardIosIcon from '@material-ui/icons/ArrowForwardIos';
 import ArrowForwardIosIcon from '@material-ui/icons/ArrowForwardIos';
 import RemoveCircleOutlineIcon from '@material-ui/icons/RemoveCircleOutline';
 import RemoveCircleOutlineIcon from '@material-ui/icons/RemoveCircleOutline';
+import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown';
+import CachedIcon from '@material-ui/icons/Cached';
+import FilterListIcon from '@material-ui/icons/FilterList';
 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';
@@ -27,6 +30,10 @@ import { ReactComponent as InfoIcon } from '../../assets/icons/info.svg';
 import { ReactComponent as ReleaseIcon } from '../../assets/icons/release.svg';
 import { ReactComponent as ReleaseIcon } from '../../assets/icons/release.svg';
 import { ReactComponent as LoadIcon } from '../../assets/icons/load.svg';
 import { ReactComponent as LoadIcon } from '../../assets/icons/load.svg';
 import { ReactComponent as KeyIcon } from '../../assets/icons/key.svg';
 import { ReactComponent as KeyIcon } from '../../assets/icons/key.svg';
+import { ReactComponent as UploadIcon } from '../../assets/icons/upload.svg';
+import { ReactComponent as VectorSearchIcon } from '../../assets/icons/nav-search.svg';
+import { ReactComponent as SearchEmptyIcon } from '../../assets/icons/search.svg';
+import { ReactComponent as CopyIcon } from '../../assets/icons/copy.svg';
 
 
 const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
 const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
   search: (props = {}) => <SearchIcon {...props} />,
   search: (props = {}) => <SearchIcon {...props} />,
@@ -47,6 +54,9 @@ const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
   logout: (props = {}) => <ExitToAppIcon {...props} />,
   logout: (props = {}) => <ExitToAppIcon {...props} />,
   rightArrow: (props = {}) => <ArrowForwardIosIcon {...props} />,
   rightArrow: (props = {}) => <ArrowForwardIosIcon {...props} />,
   remove: (props = {}) => <RemoveCircleOutlineIcon {...props} />,
   remove: (props = {}) => <RemoveCircleOutlineIcon {...props} />,
+  dropdown: (props = {}) => <ArrowDropDownIcon {...props} />,
+  refresh: (props = {}) => <CachedIcon {...props} />,
+  filter: (props = {}) => <FilterListIcon {...props} />,
 
 
   milvus: (props = {}) => (
   milvus: (props = {}) => (
     <SvgIcon viewBox="0 0 44 31" component={MilvusIcon} {...props} />
     <SvgIcon viewBox="0 0 44 31" component={MilvusIcon} {...props} />
@@ -60,6 +70,9 @@ const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
   navConsole: (props = {}) => (
   navConsole: (props = {}) => (
     <SvgIcon viewBox="0 0 20 20" component={ConsoleIcon} {...props} />
     <SvgIcon viewBox="0 0 20 20" component={ConsoleIcon} {...props} />
   ),
   ),
+  navSearch: (props = {}) => (
+    <SvgIcon viewBox="0 0 20 20" component={VectorSearchIcon} {...props} />
+  ),
   info: (props = {}) => (
   info: (props = {}) => (
     <SvgIcon viewBox="0 0 16 16" component={InfoIcon} {...props} />
     <SvgIcon viewBox="0 0 16 16" component={InfoIcon} {...props} />
   ),
   ),
@@ -72,6 +85,15 @@ const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
   key: (props = {}) => (
   key: (props = {}) => (
     <SvgIcon viewBox="0 0 16 16" component={KeyIcon} {...props} />
     <SvgIcon viewBox="0 0 16 16" component={KeyIcon} {...props} />
   ),
   ),
+  upload: (props = {}) => (
+    <SvgIcon viewBox="0 0 16 16" component={UploadIcon} {...props} />
+  ),
+  vectorSearch: (props = {}) => (
+    <SvgIcon viewBox="0 0 48 48" component={SearchEmptyIcon} {...props} />
+  ),
+  copyExpression: (props = {}) => (
+    <SvgIcon viewBox="0 0 16 16" component={CopyIcon} {...props} />
+  ),
 };
 };
 
 
 export default icons;
 export default icons;

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

@@ -15,6 +15,7 @@ export type IconsType =
   | 'navOverview'
   | 'navOverview'
   | 'navCollection'
   | 'navCollection'
   | 'navConsole'
   | 'navConsole'
+  | 'navSearch'
   | 'expandLess'
   | 'expandLess'
   | 'expandMore'
   | 'expandMore'
   | 'back'
   | 'back'
@@ -24,4 +25,10 @@ export type IconsType =
   | 'release'
   | 'release'
   | 'load'
   | 'load'
   | 'remove'
   | 'remove'
-  | 'key';
+  | 'key'
+  | 'upload'
+  | 'dropdown'
+  | 'vectorSearch'
+  | 'refresh'
+  | 'filter'
+  | 'copyExpression';

+ 356 - 0
client/src/components/insert/Container.tsx

@@ -0,0 +1,356 @@
+import { makeStyles, Theme } from '@material-ui/core';
+import {
+  FC,
+  ReactElement,
+  useCallback,
+  useContext,
+  useEffect,
+  useMemo,
+  useState,
+} from 'react';
+import { useTranslation } from 'react-i18next';
+import DialogTemplate from '../customDialog/DialogTemplate';
+import icons from '../icons/Icons';
+import { rootContext } from '../../context/Root';
+import InsertImport from './Import';
+import InsertPreview from './Preview';
+import InsertStatus from './Status';
+import {
+  InsertContentProps,
+  InsertStatusEnum,
+  InsertStepperEnum,
+  SchemaOption,
+} from './Types';
+import { Option } from '../customSelector/Types';
+import { parse } from 'papaparse';
+import { PartitionHttp } from '../../http/Partition';
+import { combineHeadsAndData } from '../../utils/Insert';
+
+const getStyles = makeStyles((theme: Theme) => ({
+  icon: {
+    fontSize: '16px',
+  },
+}));
+
+/**
+ * this component contains processes during insert
+ * including import, preview and status
+ */
+
+const InsertContainer: FC<InsertContentProps> = ({
+  collections = [],
+  defaultSelectedCollection,
+  defaultSelectedPartition,
+
+  partitions,
+  schema,
+  handleInsert,
+}) => {
+  const classes = getStyles();
+
+  const { t: insertTrans } = useTranslation('insert');
+  const { t: btnTrans } = useTranslation('btn');
+  const { handleCloseDialog, openSnackBar } = useContext(rootContext);
+  const [activeStep, setActiveStep] = useState<InsertStepperEnum>(
+    InsertStepperEnum.import
+  );
+  const [insertStatus, setInsertStatus] = useState<InsertStatusEnum>(
+    InsertStatusEnum.init
+  );
+  const [insertFailMsg, setInsertFailMsg] = useState<string>('');
+
+  const [nextDisabled, setNextDisabled] = useState<boolean>(false);
+
+  // selected collection name
+  const [collectionValue, setCollectionValue] = useState<string>(
+    defaultSelectedCollection
+  );
+  // selected partition name
+  const [partitionValue, setPartitionValue] = useState<string>(
+    defaultSelectedPartition
+  );
+  // use contain field names yes as default
+  const [isContainFieldNames, setIsContainFieldNames] = useState<number>(1);
+  // uploaded file name
+  const [fileName, setFileName] = useState<string>('');
+  const [file, setFile] = useState<File | null>(null);
+
+  // uploaded csv data (type: string)
+  const [csvData, setCsvData] = useState<any[]>([]);
+
+  // handle changed table heads
+  const [tableHeads, setTableHeads] = useState<string[]>([]);
+
+  const [partitionOptions, setPartitionOptions] = useState<Option[]>([]);
+
+  const previewData = useMemo(() => {
+    // we only show top 4 results of uploaded csv data
+    const end = isContainFieldNames ? 5 : 4;
+    return csvData.slice(0, end);
+  }, [csvData, isContainFieldNames]);
+
+  useEffect(() => {
+    if (activeStep === InsertStepperEnum.import) {
+      /**
+       * 1. must choose collection and partition
+       * 2. must upload a csv file
+       */
+      const selectValid = collectionValue !== '' && partitionValue !== '';
+      const uploadValid = csvData.length > 0;
+      const condition = selectValid && uploadValid;
+      setNextDisabled(!condition);
+    }
+    if (activeStep === InsertStepperEnum.preview) {
+      /**
+       * table heads shouldn't be empty
+       */
+      const headsValid = tableHeads.every(h => h !== '');
+      setNextDisabled(!headsValid);
+    }
+  }, [activeStep, collectionValue, partitionValue, csvData, tableHeads]);
+
+  useEffect(() => {
+    const heads = isContainFieldNames
+      ? previewData[0]
+      : new Array(previewData[0].length).fill('');
+
+    setTableHeads(heads);
+  }, [previewData, isContainFieldNames]);
+
+  const fetchPartition = useCallback(async () => {
+    if (collectionValue) {
+      const partitions = await PartitionHttp.getPartitions(collectionValue);
+      const partitionOptions: Option[] = partitions.map(p => ({
+        label: p._formatName,
+        value: p._name,
+      }));
+      setPartitionOptions(partitionOptions);
+    }
+  }, [collectionValue]);
+
+  useEffect(() => {
+    // if not on partitions page, we need to fetch partitions according to selected collection
+    if (!partitions || partitions.length === 0) {
+      fetchPartition();
+    } else {
+      const options = partitions
+        .map(p => ({
+          label: p._formatName,
+          value: p._name,
+        }))
+        // when there's single selected partition
+        // insert dialog partitions shouldn't selectable
+        .filter(
+          partition =>
+            partition.label === defaultSelectedPartition ||
+            defaultSelectedPartition === ''
+        );
+      setPartitionOptions(options);
+    }
+  }, [partitions, fetchPartition, defaultSelectedPartition]);
+
+  const BackIcon = icons.back;
+
+  // modal actions part, buttons label text or component
+  const { confirm, cancel } = useMemo(() => {
+    const labelMap: {
+      [key in InsertStepperEnum]: {
+        confirm: string;
+        cancel: string | ReactElement;
+      };
+    } = {
+      [InsertStepperEnum.import]: {
+        confirm: btnTrans('next'),
+        cancel: btnTrans('cancel'),
+      },
+      [InsertStepperEnum.preview]: {
+        confirm: btnTrans('insert'),
+        cancel: (
+          <>
+            <BackIcon classes={{ root: classes.icon }} />
+            {btnTrans('previous')}
+          </>
+        ),
+      },
+      [InsertStepperEnum.status]: {
+        confirm: btnTrans('done'),
+        cancel: '',
+      },
+    };
+    return labelMap[activeStep];
+  }, [activeStep, btnTrans, BackIcon, classes.icon]);
+
+  const { showActions, showCancel } = useMemo(() => {
+    return {
+      showActions: insertStatus !== InsertStatusEnum.loading,
+      showCancel: insertStatus === InsertStatusEnum.init,
+    };
+  }, [insertStatus]);
+
+  // props children component needed:
+  const collectionOptions: Option[] = useMemo(
+    () =>
+      defaultSelectedCollection === ''
+        ? collections.map(c => ({
+            label: c._name,
+            value: c._name,
+          }))
+        : [
+            {
+              label: defaultSelectedCollection,
+              value: defaultSelectedCollection,
+            },
+          ],
+    [collections, defaultSelectedCollection]
+  );
+
+  const schemaOptions: SchemaOption[] = useMemo(() => {
+    const list =
+      schema && schema.length > 0
+        ? schema
+        : collections.find(c => c._name === collectionValue)?._fields;
+
+    return (list || []).map(s => ({
+      label: s._fieldName,
+      value: s._fieldId,
+      isPrimaryKey: s._isPrimaryKey,
+    }));
+  }, [schema, collectionValue, collections]);
+
+  const checkUploadFileValidation = (fieldNamesLength: number): boolean => {
+    return (
+      schemaOptions.filter(s => !s.isPrimaryKey).length === fieldNamesLength
+    );
+  };
+
+  const handleUploadedData = (csv: string, uploader: HTMLFormElement) => {
+    const { data } = parse(csv);
+    const uploadFieldNamesLength = (data as string[])[0].length;
+    const validation = checkUploadFileValidation(uploadFieldNamesLength);
+    if (!validation) {
+      // open snackbar
+      openSnackBar(insertTrans('uploadFieldNamesLenWarning'), 'error');
+      // reset uploader value and filename
+      setFileName('');
+      setFile(null);
+      uploader.value = null;
+      return;
+    }
+    setCsvData(data);
+  };
+
+  const handleInsertData = async () => {
+    // start loading
+    setInsertStatus(InsertStatusEnum.loading);
+    // combine table heads and data
+    const tableData = isContainFieldNames ? csvData.slice(1) : csvData;
+    const data = combineHeadsAndData(tableHeads, tableData);
+    const { result, msg } = await handleInsert(
+      collectionValue,
+      partitionValue,
+      data
+    );
+
+    if (!result) {
+      setInsertFailMsg(msg);
+    }
+    const status = result ? InsertStatusEnum.success : InsertStatusEnum.error;
+    setInsertStatus(status);
+  };
+
+  const handleCollectionChange = (name: string) => {
+    setCollectionValue(name);
+    // reset partition
+    setPartitionValue('');
+  };
+
+  const handleNext = () => {
+    switch (activeStep) {
+      case InsertStepperEnum.import:
+        setActiveStep(activeStep => activeStep + 1);
+        break;
+      case InsertStepperEnum.preview:
+        setActiveStep(activeStep => activeStep + 1);
+        handleInsertData();
+        break;
+      // default represent InsertStepperEnum.status
+      default:
+        handleCloseDialog();
+        break;
+    }
+  };
+
+  const handleUploadFileChange = (file: File, upload: HTMLFormElement) => {
+    setFile(file);
+  };
+
+  const handleBack = () => {
+    switch (activeStep) {
+      case InsertStepperEnum.import:
+        handleCloseDialog();
+        break;
+      case InsertStepperEnum.preview:
+        setActiveStep(activeStep => activeStep - 1);
+        break;
+      // default represent InsertStepperEnum.status
+      // status don't have cancel button
+      default:
+        break;
+    }
+  };
+
+  const generateContent = (activeStep: InsertStepperEnum) => {
+    switch (activeStep) {
+      case InsertStepperEnum.import:
+        return (
+          <InsertImport
+            collectionOptions={collectionOptions}
+            partitionOptions={partitionOptions}
+            selectedCollection={collectionValue}
+            selectedPartition={partitionValue}
+            handleCollectionChange={handleCollectionChange}
+            handlePartitionChange={setPartitionValue}
+            handleUploadedData={handleUploadedData}
+            handleUploadFileChange={handleUploadFileChange}
+            fileName={fileName}
+            setFileName={setFileName}
+          />
+        );
+      case InsertStepperEnum.preview:
+        return (
+          <InsertPreview
+            schemaOptions={schemaOptions}
+            data={previewData}
+            tableHeads={tableHeads}
+            setTableHeads={setTableHeads}
+            isContainFieldNames={isContainFieldNames}
+            file={file}
+            handleIsContainedChange={setIsContainFieldNames}
+          />
+        );
+      // default represents InsertStepperEnum.status
+      default:
+        return <InsertStatus status={insertStatus} failMsg={insertFailMsg} />;
+    }
+  };
+
+  return (
+    <DialogTemplate
+      title={insertTrans('import')}
+      handleClose={handleCloseDialog}
+      confirmLabel={confirm}
+      cancelLabel={cancel}
+      handleCancel={handleBack}
+      handleConfirm={handleNext}
+      confirmDisabled={nextDisabled}
+      showActions={showActions}
+      showCancel={showCancel}
+      // don't show close icon when insert not finish
+      showCloseIcon={insertStatus !== InsertStatusEnum.loading}
+    >
+      {generateContent(activeStep)}
+    </DialogTemplate>
+  );
+};
+
+export default InsertContainer;

+ 195 - 0
client/src/components/insert/Import.tsx

@@ -0,0 +1,195 @@
+import { FC } from 'react';
+import { useTranslation } from 'react-i18next';
+import { makeStyles, Theme, Divider, Typography } from '@material-ui/core';
+import CustomSelector from '../customSelector/CustomSelector';
+import { InsertImportProps } from './Types';
+import Uploader from '../uploader/Uploader';
+import { INSERT_CSV_SAMPLE } from '../../consts/Insert';
+import { parseByte } from '../../utils/Format';
+
+const getStyles = makeStyles((theme: Theme) => ({
+  tip: {
+    color: theme.palette.milvusGrey.dark,
+    fontWeight: 500,
+    marginBottom: theme.spacing(1),
+  },
+  selectors: {
+    '& .selectorWrapper': {
+      display: 'flex',
+      justifyContent: 'space-between',
+      alignItems: 'center',
+
+      marginBottom: theme.spacing(3),
+
+      '& .selectLabel': {
+        fontSize: '14px',
+        lineHeight: '20px',
+        color: '#010e29',
+      },
+
+      '& .divider': {
+        width: '20px',
+        margin: theme.spacing(0, 4),
+        backgroundColor: theme.palette.milvusGrey.dark,
+      },
+    },
+
+    '& .selector': {
+      flexBasis: '40%',
+      minWidth: '256px',
+    },
+  },
+
+  uploadWrapper: {
+    marginTop: theme.spacing(3),
+    padding: theme.spacing(1),
+    backgroundColor: '#f9f9f9',
+
+    '& .text': {
+      color: theme.palette.milvusGrey.dark,
+    },
+
+    '& .file': {
+      marginBottom: theme.spacing(1),
+    },
+
+    '& .uploaderWrapper': {
+      display: 'flex',
+      alignItems: 'center',
+
+      border: '1px solid #e9e9ed',
+      borderRadius: '4px',
+      padding: theme.spacing(1),
+
+      backgroundColor: '#fff',
+
+      '& .uploader': {
+        marginRight: theme.spacing(1),
+      },
+    },
+
+    '& .sampleWrapper': {
+      '& .sample': {
+        backgroundColor: '#fff',
+        padding: theme.spacing(2),
+        margin: theme.spacing(1, 0),
+      },
+    },
+
+    '& .title': {
+      marginTop: theme.spacing(1),
+    },
+
+    '& .noteList': {
+      marginTop: theme.spacing(1),
+      paddingLeft: theme.spacing(3),
+    },
+
+    '& .noteItem': {
+      maxWidth: '560px',
+    },
+  },
+}));
+
+const InsertImport: FC<InsertImportProps> = ({
+  collectionOptions,
+  partitionOptions,
+
+  selectedCollection,
+  selectedPartition,
+
+  handleCollectionChange,
+  handlePartitionChange,
+
+  handleUploadedData,
+  handleUploadFileChange,
+  fileName,
+  setFileName,
+}) => {
+  const { t: insertTrans } = useTranslation('insert');
+  const { t: collectionTrans } = useTranslation('collection');
+  const { t: partitionTrans } = useTranslation('partition');
+  const classes = getStyles();
+
+  return (
+    <section>
+      <Typography className={classes.tip}>
+        {insertTrans('targetTip')}
+      </Typography>
+
+      <form className={classes.selectors}>
+        <div className="selectorWrapper">
+          <CustomSelector
+            options={collectionOptions}
+            disabled={collectionOptions.length === 0}
+            wrapperClass="selector"
+            labelClass="selectLabel"
+            value={selectedCollection}
+            variant="filled"
+            label={collectionTrans('collection')}
+            onChange={(e: { target: { value: unknown } }) => {
+              const collection = e.target.value;
+              handleCollectionChange &&
+                handleCollectionChange(collection as string);
+            }}
+          />
+          <Divider classes={{ root: 'divider' }} />
+          <CustomSelector
+            options={partitionOptions}
+            disabled={partitionOptions.length === 0}
+            wrapperClass="selector"
+            labelClass="selectLabel"
+            value={selectedPartition}
+            variant="filled"
+            label={partitionTrans('partition')}
+            onChange={(e: { target: { value: unknown } }) => {
+              const partition = e.target.value;
+              handlePartitionChange(partition as string);
+            }}
+          />
+        </div>
+      </form>
+
+      <div className={classes.uploadWrapper}>
+        <Typography className="text file" variant="body1">
+          {insertTrans('file')}
+        </Typography>
+        <div className="uploaderWrapper">
+          <Uploader
+            btnClass="uploader"
+            label={insertTrans('uploaderLabel')}
+            accept=".csv"
+            setFileName={setFileName}
+            handleUploadedData={handleUploadedData}
+            maxSize={parseByte('150m')}
+            overSizeWarning={insertTrans('overSizeWarning')}
+            handleUploadFileChange={handleUploadFileChange}
+          />
+          <Typography className="text">
+            {fileName || insertTrans('fileNamePlaceHolder')}
+          </Typography>
+        </div>
+
+        <div className="sampleWrapper">
+          <Typography variant="body2" className="text title">
+            {insertTrans('sample')}
+          </Typography>
+          <pre className="sample">{INSERT_CSV_SAMPLE}</pre>
+        </div>
+
+        <Typography variant="body2" className="text title">
+          {insertTrans('noteTitle')}
+        </Typography>
+        <ul className="noteList">
+          {insertTrans('notes', { returnObjects: true }).map(note => (
+            <li key={note} className="text noteItem">
+              <Typography>{note}</Typography>
+            </li>
+          ))}
+        </ul>
+      </div>
+    </section>
+  );
+};
+
+export default InsertImport;

+ 210 - 0
client/src/components/insert/Preview.tsx

@@ -0,0 +1,210 @@
+import { FC, useCallback, useMemo } from 'react';
+import { makeStyles, Theme, Typography } from '@material-ui/core';
+import { useTranslation } from 'react-i18next';
+import { InsertPreviewProps } from './Types';
+import { Option } from '../customSelector/Types';
+import CustomSelector from '../customSelector/CustomSelector';
+import MilvusGrid from '../grid/Grid';
+import { transferCsvArrayToTableData } from '../../utils/Insert';
+import { ColDefinitionsType } from '../grid/Types';
+import SimpleMenu from '../menu/SimpleMenu';
+import icons from '../icons/Icons';
+
+const getStyles = makeStyles((theme: Theme) => ({
+  wrapper: {
+    width: '75vw',
+  },
+  selectorTip: {
+    color: theme.palette.milvusGrey.dark,
+    fontWeight: 500,
+    marginBottom: theme.spacing(1),
+  },
+  selectorWrapper: {
+    '& .selector': {
+      flexBasis: '40%',
+      minWidth: '256px',
+    },
+
+    '& .isContainSelect': {
+      paddingTop: theme.spacing(2),
+      paddingBottom: theme.spacing(2),
+    },
+  },
+  gridWrapper: {
+    height: '320px',
+  },
+  tableTip: {
+    display: 'flex',
+    justifyContent: 'space-between',
+    alignItems: 'center',
+
+    marginTop: theme.spacing(3),
+    marginBottom: theme.spacing(1),
+
+    '& .text': {
+      color: theme.palette.milvusGrey.dark,
+      fontWeight: 500,
+    },
+  },
+  menuLabel: {
+    display: 'flex',
+    justifyContent: 'space-between',
+    minWidth: '160px',
+
+    color: theme.palette.milvusGrey.dark,
+    backgroundColor: '#fff',
+
+    '&:hover': {
+      backgroundColor: '#fff',
+    },
+  },
+
+  menuIcon: {
+    color: theme.palette.milvusGrey.dark,
+  },
+  menuItem: {
+    fontWeight: 500,
+    fontSize: '12px',
+    lineHeight: '16px',
+    color: theme.palette.milvusGrey.dark,
+  },
+  menuActive: {
+    color: theme.palette.primary.main,
+  },
+}));
+
+const getTableData = (
+  data: any[],
+  isContainFieldNames: number
+): { [key in string]: any }[] => {
+  const csvData = isContainFieldNames ? data.slice(1) : data;
+  return transferCsvArrayToTableData(csvData);
+};
+
+const InsertPreview: FC<InsertPreviewProps> = ({
+  schemaOptions,
+  data,
+  isContainFieldNames,
+  handleIsContainedChange,
+  tableHeads,
+  setTableHeads,
+  file,
+}) => {
+  const classes = getStyles();
+  const { t: insertTrans } = useTranslation('insert');
+
+  const ArrowIcon = icons.dropdown;
+  // table needed table structure, metadata from csv
+  const tableData = getTableData(data, isContainFieldNames);
+
+  const handleTableHeadChange = useCallback(
+    (index: number, label: string) => {
+      const newHeads = [...tableHeads];
+      newHeads[index] = label;
+      setTableHeads(newHeads);
+    },
+    [tableHeads, setTableHeads]
+  );
+
+  const editHeads = useMemo(
+    () =>
+      tableHeads.map((head: string, index: number) => ({
+        value: head,
+        component: (
+          <SimpleMenu
+            label={head || insertTrans('requiredFieldName')}
+            menuItems={schemaOptions.map(schema => ({
+              label: schema.label,
+              callback: () => handleTableHeadChange(index, schema.label),
+              wrapperClass: `${classes.menuItem} ${
+                head === schema.label ? classes.menuActive : ''
+              }`,
+            }))}
+            buttonProps={{
+              className: classes.menuLabel,
+              endIcon: <ArrowIcon classes={{ root: classes.menuIcon }} />,
+            }}
+          ></SimpleMenu>
+        ),
+      })),
+    [
+      tableHeads,
+      classes.menuLabel,
+      classes.menuIcon,
+      classes.menuItem,
+      classes.menuActive,
+      ArrowIcon,
+      schemaOptions,
+      insertTrans,
+      handleTableHeadChange,
+    ]
+  );
+
+  const isContainedOptions: Option[] = [
+    {
+      label: 'Yes',
+      value: 1,
+    },
+    { label: 'No', value: 0 },
+  ];
+
+  // use table row first item to get value
+  const colDefinitions: ColDefinitionsType[] = Object.keys(tableData[0])
+    // filter id since we don't want to show it in the table
+    .filter(item => item !== 'id')
+    .map(key => ({
+      id: key,
+      align: 'left',
+      disablePadding: true,
+      label: '',
+    }));
+
+  return (
+    <section className={classes.wrapper}>
+      <form className={classes.selectorWrapper}>
+        <label>
+          <Typography className={classes.selectorTip}>
+            {insertTrans('isContainFieldNames')}
+          </Typography>
+        </label>
+        <CustomSelector
+          options={isContainedOptions}
+          wrapperClass="selector"
+          classes={{ filled: 'isContainSelect' }}
+          value={isContainFieldNames}
+          variant="filled"
+          onChange={(e: { target: { value: unknown } }) => {
+            const isContainedValue = e.target.value;
+            handleIsContainedChange(isContainedValue as number);
+          }}
+        />
+      </form>
+      <div className={classes.tableTip}>
+        <Typography className="text">
+          {insertTrans('previewTipData')}
+        </Typography>
+        <Typography className="text">
+          {insertTrans('previewTipAction')}
+        </Typography>
+      </div>
+      {tableData.length > 0 && (
+        <div className={classes.gridWrapper}>
+          <MilvusGrid
+            toolbarConfigs={[]}
+            colDefinitions={colDefinitions}
+            rows={tableData}
+            rowCount={0}
+            primaryKey="id"
+            openCheckBox={false}
+            showHoverStyle={false}
+            headEditable={true}
+            editHeads={editHeads}
+            tableCellMaxWidth="120px"
+          />
+        </div>
+      )}
+    </section>
+  );
+};
+
+export default InsertPreview;

+ 93 - 0
client/src/components/insert/Status.tsx

@@ -0,0 +1,93 @@
+import { FC } from 'react';
+import {
+  makeStyles,
+  Theme,
+  Typography,
+  CircularProgress,
+} from '@material-ui/core';
+import { InsertStatusEnum, InsertStatusProps } from './Types';
+import successPath from '../../assets/imgs/insert/success.png';
+import failPath from '../../assets/imgs/insert/fail.png';
+import { useTranslation } from 'react-i18next';
+
+const getStyles = makeStyles((theme: Theme) => ({
+  wrapper: {
+    width: '75vw',
+    height: (props: { status: InsertStatusEnum }) =>
+      props.status === InsertStatusEnum.loading ? '288px' : '200px',
+
+    display: 'flex',
+    flexDirection: 'column',
+    alignItems: 'center',
+    justifyContent: 'center',
+  },
+  loadingTip: {
+    marginBottom: theme.spacing(6),
+  },
+  loadingSvg: {
+    color: theme.palette.primary.main,
+  },
+  text: {
+    marginTop: theme.spacing(3),
+  },
+}));
+
+const InsertStatus: FC<InsertStatusProps> = ({ status, failMsg }) => {
+  const { t: insertTrans } = useTranslation('insert');
+  const classes = getStyles({ status });
+
+  const InsertSuccess = () => (
+    <>
+      <img src={successPath} alt="insert success" />
+      <Typography variant="h4" className={classes.text}>
+        {insertTrans('statusSuccess')}
+      </Typography>
+    </>
+  );
+
+  const InsertLoading = () => (
+    <>
+      <CircularProgress
+        size={64}
+        thickness={5}
+        classes={{ svg: classes.loadingSvg }}
+      />
+      <Typography variant="h4" className={classes.text}>
+        {insertTrans('statusLoading')}
+      </Typography>
+      <Typography
+        variant="h5"
+        className={`${classes.text} ${classes.loadingTip}`}
+      >
+        {insertTrans('statusLoadingTip')}
+      </Typography>
+    </>
+  );
+  const InsertError = () => (
+    <>
+      <img src={failPath} alt="insert error" />
+      <Typography variant="h4" className={classes.text}>
+        {insertTrans('statusError')}
+      </Typography>
+      {failMsg && <Typography className={classes.text}>{failMsg}</Typography>}
+    </>
+  );
+
+  const generateStatus = (status: InsertStatusEnum) => {
+    switch (status) {
+      case InsertStatusEnum.loading:
+        return <InsertLoading />;
+      case InsertStatusEnum.success:
+        return <InsertSuccess />;
+      // status error or init as default
+      default:
+        return <InsertError />;
+    }
+  };
+
+  return (
+    <section className={classes.wrapper}>{generateStatus(status)}</section>
+  );
+};
+
+export default InsertStatus;

+ 81 - 0
client/src/components/insert/Types.ts

@@ -0,0 +1,81 @@
+import { CollectionData } from '../../pages/collections/Types';
+import { PartitionView } from '../../pages/partitions/Types';
+import { FieldData } from '../../pages/schema/Types';
+import { Option } from '../customSelector/Types';
+
+export interface InsertContentProps {
+  // optional on partition page since its collection is fixed
+  collections?: CollectionData[];
+  // required on partition page since user can't select collection to get schema
+  schema?: FieldData[];
+  // required on partition page
+  partitions?: PartitionView[];
+
+  // insert default selected collection
+  // if default value is not '', collections not selectable
+  defaultSelectedCollection: string;
+
+  // insert default selected partition
+  // if default value is not '', partitions not selectable
+  defaultSelectedPartition: string;
+
+  handleInsert: (
+    collectionName: string,
+    partitionName: string,
+    fieldData: any[]
+  ) => Promise<{ result: boolean; msg: string }>;
+}
+
+export enum InsertStepperEnum {
+  import,
+  preview,
+  status,
+}
+
+export enum InsertStatusEnum {
+  // init means not begin yet
+  init = 'init',
+  loading = 'loading',
+  success = 'success',
+  error = 'error',
+}
+
+export interface InsertImportProps {
+  // selectors options
+  collectionOptions: Option[];
+  partitionOptions: Option[];
+  // selectors value
+  selectedCollection: string;
+  selectedPartition: string;
+
+  // selectors change methods
+  // optional if collection not selectable
+  handleCollectionChange?: (collectionName: string) => void;
+  handlePartitionChange: (partitionName: string) => void;
+  // handle uploaded data
+  handleUploadedData: (data: string, uploader: HTMLFormElement) => void;
+  handleUploadFileChange: (file: File, uploader: HTMLFormElement) => void;
+  fileName: string;
+  setFileName: (fileName: string) => void;
+}
+
+export interface InsertPreviewProps {
+  schemaOptions: Option[];
+  data: any[];
+
+  tableHeads: string[];
+  setTableHeads: (heads: string[]) => void;
+
+  isContainFieldNames: number;
+  handleIsContainedChange: (isContained: number) => void;
+  file: File | null; // csv file
+}
+
+export interface InsertStatusProps {
+  status: InsertStatusEnum;
+  failMsg: string;
+}
+
+export interface SchemaOption extends Option {
+  isPrimaryKey: boolean;
+}

+ 30 - 7
client/src/components/layout/Layout.tsx

@@ -17,6 +17,18 @@ const useStyles = makeStyles((theme: Theme) =>
     },
     },
     content: {
     content: {
       display: 'flex',
       display: 'flex',
+
+      '& .normalSearchIcon': {
+        '& path': {
+          fill: theme.palette.milvusGrey.dark,
+        },
+      },
+
+      '& .activeSearchIcon': {
+        '& path': {
+          fill: theme.palette.primary.main,
+        },
+      },
     },
     },
     body: {
     body: {
       flex: 1,
       flex: 1,
@@ -34,13 +46,17 @@ const Layout = (props: any) => {
   const { t: navTrans } = useTranslation('nav');
   const { t: navTrans } = useTranslation('nav');
   const classes = useStyles();
   const classes = useStyles();
   const location = useLocation();
   const location = useLocation();
-  const defaultActive = useMemo(
-    () =>
-      location.pathname.includes('collection')
-        ? navTrans('collection')
-        : navTrans('overview'),
-    [location, navTrans]
-  );
+  const defaultActive = useMemo(() => {
+    if (location.pathname.includes('collection')) {
+      return navTrans('collection');
+    }
+
+    if (location.pathname.includes('search')) {
+      return navTrans('search');
+    }
+
+    return navTrans('overview');
+  }, [location, navTrans]);
 
 
   const menuItems: NavMenuItem[] = [
   const menuItems: NavMenuItem[] = [
     {
     {
@@ -53,6 +69,13 @@ 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',
+    },
   ];
   ];
 
 
   return (
   return (

+ 26 - 6
client/src/components/menu/SimpleMenu.tsx

@@ -8,16 +8,31 @@ import CustomButton from '../customButton/CustomButton';
 import { makeStyles, Theme } from '@material-ui/core';
 import { makeStyles, Theme } from '@material-ui/core';
 
 
 const getStyles = makeStyles((theme: Theme) => ({
 const getStyles = makeStyles((theme: Theme) => ({
+  menuPaper: {
+    boxShadow: '0px 4px 24px rgba(0, 0, 0, 0.08)',
+    borderRadius: '4px',
+  },
   menuItem: {
   menuItem: {
-    minWidth: '160px',
+    minWidth: (props: { minWidth: string }) => props.minWidth,
+    padding: theme.spacing(1),
+
+    '&:hover': {
+      backgroundColor: '#f9f9f9',
+    },
   },
   },
 }));
 }));
 
 
 const SimpleMenu: FC<SimpleMenuType> = props => {
 const SimpleMenu: FC<SimpleMenuType> = props => {
-  const { label, menuItems, buttonProps, className = '' } = props;
+  const {
+    label,
+    menuItems,
+    buttonProps,
+    menuItemWidth = '160px',
+    className = '',
+  } = props;
   const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
   const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
 
 
-  const classes = getStyles();
+  const classes = getStyles({ minWidth: menuItemWidth });
   const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
   const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
     setAnchorEl(event.currentTarget);
     setAnchorEl(event.currentTarget);
   };
   };
@@ -44,9 +59,10 @@ const SimpleMenu: FC<SimpleMenuType> = props => {
         keepMounted
         keepMounted
         open={Boolean(anchorEl)}
         open={Boolean(anchorEl)}
         onClose={handleClose}
         onClose={handleClose}
+        classes={{ paper: classes.menuPaper }}
         getContentAnchorEl={null}
         getContentAnchorEl={null}
-        anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
-        transformOrigin={{ vertical: 'top', horizontal: 'center' }}
+        // anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
+        // transformOrigin={{ vertical: 'top', horizontal: 'center' }}
       >
       >
         <div>
         <div>
           {menuItems.map((v, i) =>
           {menuItems.map((v, i) =>
@@ -59,7 +75,11 @@ const SimpleMenu: FC<SimpleMenuType> = props => {
                 }}
                 }}
                 key={v.label + i}
                 key={v.label + i}
               >
               >
-                {v.label}
+                {v.wrapperClass ? (
+                  <span className={v.wrapperClass}>{v.label}</span>
+                ) : (
+                  v.label
+                )}
               </MenuItem>
               </MenuItem>
             ) : (
             ) : (
               <span key={i}>{v.label}</span>
               <span key={i}>{v.label}</span>

+ 7 - 1
client/src/components/menu/Types.ts

@@ -3,9 +3,15 @@ import { ReactElement } from 'react';
 
 
 export type SimpleMenuType = {
 export type SimpleMenuType = {
   label: string;
   label: string;
-  menuItems: { label: string | ReactElement; callback?: () => void }[];
+  menuItems: {
+    label: string | ReactElement;
+    callback?: () => void;
+    wrapperClass?: string;
+  }[];
   buttonProps?: ButtonProps;
   buttonProps?: ButtonProps;
   className?: string;
   className?: string;
+  // e.g. 160px
+  menuItemWidth?: string;
 };
 };
 
 
 export type NavMenuItem = {
 export type NavMenuItem = {

+ 15 - 0
client/src/components/uploader/Types.ts

@@ -0,0 +1,15 @@
+export interface UploaderProps {
+  label: string;
+  accept: string;
+  btnClass?: string;
+  // unit should be byte
+  maxSize?: number;
+  // snackbar warning when uploaded file size is over limit
+  overSizeWarning?: string;
+  setFileName: (fileName: string) => void;
+  // handle uploader uploaded
+  handleUploadedData: (data: string, uploader: HTMLFormElement) => void;
+  // handle uploader onchange
+  handleUploadFileChange?: (file: File, uploader: HTMLFormElement) => void;
+  handleUploadError?: () => void;
+}

+ 86 - 0
client/src/components/uploader/Uploader.tsx

@@ -0,0 +1,86 @@
+import { makeStyles, Theme } from '@material-ui/core';
+import { FC, useContext, useRef } from 'react';
+import { rootContext } from '../../context/Root';
+import CustomButton from '../customButton/CustomButton';
+import { UploaderProps } from './Types';
+
+const getStyles = makeStyles((theme: Theme) => ({
+  btn: {},
+}));
+
+const Uploader: FC<UploaderProps> = ({
+  label,
+  accept,
+  btnClass = '',
+  maxSize,
+  overSizeWarning = '',
+  handleUploadedData,
+  handleUploadFileChange,
+  handleUploadError,
+  setFileName,
+}) => {
+  const inputRef = useRef(null);
+  const classes = getStyles();
+
+  const { openSnackBar } = useContext(rootContext);
+
+  const handleUpload = () => {
+    const uploader = inputRef.current! as HTMLFormElement;
+    const reader = new FileReader();
+    // handle uploaded data
+    reader.onload = async e => {
+      const data = reader.result;
+      if (data) {
+        handleUploadedData(data as string, inputRef.current!);
+      }
+    };
+    // handle upload error
+    reader.onerror = e => {
+      if (handleUploadError) {
+        handleUploadError();
+      }
+      console.error(e);
+    };
+    uploader!.onchange = (e: Event) => {
+      const target = e.target as HTMLInputElement;
+      const file: File = (target.files as FileList)[0];
+      const isSizeOverLimit = file && maxSize && maxSize < file.size;
+
+      if (!file) {
+        return;
+      }
+      if (isSizeOverLimit) {
+        openSnackBar(overSizeWarning, 'error');
+        const uploader = inputRef.current! as HTMLFormElement;
+        uploader.value = null;
+        return;
+      }
+
+      setFileName(file.name || 'file');
+      handleUploadFileChange && handleUploadFileChange(file, inputRef.current!);
+      reader.readAsText(file, 'utf8');
+    };
+    uploader.click();
+  };
+
+  return (
+    <form>
+      <CustomButton
+        variant="contained"
+        className={`${classes.btn} ${btnClass}`}
+        onClick={handleUpload}
+      >
+        {label}
+      </CustomButton>
+      <input
+        ref={inputRef}
+        id="fileId"
+        type="file"
+        accept={accept}
+        style={{ display: 'none' }}
+      />
+    </form>
+  );
+};
+
+export default Uploader;

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

@@ -0,0 +1,4 @@
+export const INSERT_CSV_SAMPLE = `Date, Country, Units, Revenue,\n
+1, 183, [13848...], [318998...]\n
+909,3898,[3898...], [84981...]\n
+...`;

+ 108 - 31
client/src/consts/Milvus.tsx

@@ -1,11 +1,13 @@
+import { DataTypeEnum } from '../pages/collections/Types';
+
 export enum METRIC_TYPES_VALUES {
 export enum METRIC_TYPES_VALUES {
   L2 = 'L2',
   L2 = 'L2',
   IP = 'IP',
   IP = 'IP',
-  HAMMING = 'Hamming',
-  JACCARD = 'Jaccard',
-  TANIMOTO = 'Tanimoto',
-  SUBSTRUCTURE = 'Substructure',
-  SUPERSTRUCTURE = 'Superstructure',
+  HAMMING = 'HAMMING',
+  JACCARD = 'JACCARD',
+  TANIMOTO = 'TANIMOTO',
+  SUBSTRUCTURE = 'SUBSTRUCTURE',
+  SUPERSTRUCTURE = 'SUPERSTRUCTURE',
 }
 }
 
 
 export const METRIC_TYPES = [
 export const METRIC_TYPES = [
@@ -19,44 +21,46 @@ export const METRIC_TYPES = [
   },
   },
   {
   {
     value: METRIC_TYPES_VALUES.SUBSTRUCTURE,
     value: METRIC_TYPES_VALUES.SUBSTRUCTURE,
-    label: 'Substructure',
+    label: 'SUBSTRUCTURE',
   },
   },
   {
   {
     value: METRIC_TYPES_VALUES.SUPERSTRUCTURE,
     value: METRIC_TYPES_VALUES.SUPERSTRUCTURE,
-    label: 'Superstructure',
+    label: 'SUPERSTRUCTURE',
   },
   },
   {
   {
     value: METRIC_TYPES_VALUES.HAMMING,
     value: METRIC_TYPES_VALUES.HAMMING,
-    label: 'Hamming',
+    label: 'HAMMING',
   },
   },
   {
   {
     value: METRIC_TYPES_VALUES.JACCARD,
     value: METRIC_TYPES_VALUES.JACCARD,
-    label: 'Jaccard',
+    label: 'JACCARD',
   },
   },
   {
   {
     value: METRIC_TYPES_VALUES.TANIMOTO,
     value: METRIC_TYPES_VALUES.TANIMOTO,
-    label: 'Tanimoto',
+    label: 'TANIMOTO',
   },
   },
 ];
 ];
 
 
 export type MetricType =
 export type MetricType =
   | 'L2'
   | 'L2'
   | 'IP'
   | 'IP'
-  | 'Hamming'
-  | 'Substructure'
-  | 'Superstructure'
-  | 'Jaccard'
-  | 'Tanimoto';
+  | 'HAMMING'
+  | 'SUBSTRUCTURE'
+  | 'SUPERSTRUCTURE'
+  | 'JACCARD'
+  | 'TANIMOTO';
 
 
 export type searchKeywordsType = 'nprobe' | 'ef' | 'search_k' | 'search_length';
 export type searchKeywordsType = 'nprobe' | 'ef' | 'search_k' | 'search_length';
 
 
-// index
-export const INDEX_CONFIG: {
+export type indexConfigType = {
   [x: string]: {
   [x: string]: {
     create: string[];
     create: string[];
     search: searchKeywordsType[];
     search: searchKeywordsType[];
   };
   };
-} = {
+};
+
+// index
+export const FLOAT_INDEX_CONFIG: indexConfigType = {
   IVF_FLAT: {
   IVF_FLAT: {
     create: ['nlist'],
     create: ['nlist'],
     search: ['nprobe'],
     search: ['nprobe'],
@@ -65,10 +69,10 @@ export const INDEX_CONFIG: {
     create: ['nlist', 'm'],
     create: ['nlist', 'm'],
     search: ['nprobe'],
     search: ['nprobe'],
   },
   },
-  // IVF_SQ8: {
-  //   create: ['nlist'],
-  //   search: ['nprobe'],
-  // },
+  IVF_SQ8: {
+    create: ['nlist'],
+    search: ['nprobe'],
+  },
   // IVF_SQ8_HYBRID: {
   // IVF_SQ8_HYBRID: {
   //   create: ['nlist'],
   //   create: ['nlist'],
   //   search: ['nprobe'],
   //   search: ['nprobe'],
@@ -88,7 +92,24 @@ export const INDEX_CONFIG: {
   // RNSG: {
   // RNSG: {
   //   create: ['out_degree', 'candidate_pool_size', 'search_length', 'knng'],
   //   create: ['out_degree', 'candidate_pool_size', 'search_length', 'knng'],
   //   search: ['search_length'],
   //   search: ['search_length'],
+  // },}
+};
+
+export const BINARY_INDEX_CONFIG: indexConfigType = {
   // },
   // },
+  BIN_FLAT: {
+    create: ['nlist'],
+    search: ['nprobe'],
+  },
+  BIN_IVF_FLAT: {
+    create: ['nlist'],
+    search: ['nprobe'],
+  },
+};
+
+export const INDEX_CONFIG: indexConfigType = {
+  ...FLOAT_INDEX_CONFIG,
+  ...BINARY_INDEX_CONFIG,
 };
 };
 
 
 export const COLLECTION_NAME_REGX = /^[0-9,a-z,A-Z$_]+$/;
 export const COLLECTION_NAME_REGX = /^[0-9,a-z,A-Z$_]+$/;
@@ -102,16 +123,72 @@ export const m_OPTIONS = [
 ];
 ];
 
 
 export const INDEX_OPTIONS_MAP = {
 export const INDEX_OPTIONS_MAP = {
-  FLOAT_POINT: Object.keys(INDEX_CONFIG).map(v => ({ label: v, value: v })),
-  BINARY: [
-    { label: 'FLAT', value: 'FLAT' },
-    { label: 'IVF_FLAT', value: 'IVF_FLAT' },
-  ],
+  [DataTypeEnum.FloatVector]: Object.keys(FLOAT_INDEX_CONFIG).map(v => ({
+    label: v,
+    value: v,
+  })),
+  [DataTypeEnum.BinaryVector]: Object.keys(BINARY_INDEX_CONFIG).map(v => ({
+    label: v,
+    value: v,
+  })),
 };
 };
 
 
 export const PRIMARY_KEY_FIELD = 'INT64 (Primary key)';
 export const PRIMARY_KEY_FIELD = 'INT64 (Primary key)';
 
 
-export enum EmbeddingTypeEnum {
-  float = 'FLOAT_POINT',
-  binary = 'BINARY',
-}
+export const METRIC_OPTIONS_MAP = {
+  [DataTypeEnum.FloatVector]: [
+    {
+      value: METRIC_TYPES_VALUES.L2,
+      label: METRIC_TYPES_VALUES.L2,
+    },
+    {
+      value: METRIC_TYPES_VALUES.IP,
+      label: METRIC_TYPES_VALUES.IP,
+    },
+  ],
+  [DataTypeEnum.BinaryVector]: [
+    {
+      value: METRIC_TYPES_VALUES.SUBSTRUCTURE,
+      label: METRIC_TYPES_VALUES.SUBSTRUCTURE,
+    },
+    {
+      value: METRIC_TYPES_VALUES.SUPERSTRUCTURE,
+      label: METRIC_TYPES_VALUES.SUPERSTRUCTURE,
+    },
+    {
+      value: METRIC_TYPES_VALUES.HAMMING,
+      label: METRIC_TYPES_VALUES.HAMMING,
+    },
+    {
+      value: METRIC_TYPES_VALUES.JACCARD,
+      label: METRIC_TYPES_VALUES.JACCARD,
+    },
+    {
+      value: METRIC_TYPES_VALUES.TANIMOTO,
+      label: METRIC_TYPES_VALUES.TANIMOTO,
+    },
+  ],
+};
+
+/**
+ * use L2 as float default metric type
+ * use Hamming as binary default metric type
+ */
+export const DEFAULT_METRIC_VALUE_MAP = {
+  [DataTypeEnum.FloatVector]: METRIC_TYPES_VALUES.L2,
+  [DataTypeEnum.BinaryVector]: METRIC_TYPES_VALUES.HAMMING,
+};
+
+// search params default value map
+export const DEFAULT_SEARCH_PARAM_VALUE_MAP: {
+  [key in searchKeywordsType]: number;
+} = {
+  // range: [top_k, 32768]
+  ef: 250,
+  // range: [1, nlist]
+  nprobe: 1,
+  // range: {-1} ∪ [top_k, n × n_trees]
+  search_k: 250,
+  // range: [10, 300]
+  search_length: 10,
+};

+ 8 - 0
client/src/context/Auth.tsx

@@ -11,12 +11,14 @@ export const authContext = createContext<AuthContextType>({
 
 
 const { Provider } = authContext;
 const { Provider } = authContext;
 export const AuthProvider = (props: { children: React.ReactNode }) => {
 export const AuthProvider = (props: { children: React.ReactNode }) => {
+  // get milvus address from local storage
   const [address, setAddress] = useState<string>(
   const [address, setAddress] = useState<string>(
     window.localStorage.getItem(MILVUS_ADDRESS) || ''
     window.localStorage.getItem(MILVUS_ADDRESS) || ''
   );
   );
   const isAuth = useMemo(() => !!address, [address]);
   const isAuth = useMemo(() => !!address, [address]);
 
 
   useEffect(() => {
   useEffect(() => {
+    // check if the milvus is still available
     const check = async () => {
     const check = async () => {
       const milvusAddress = window.localStorage.getItem(MILVUS_ADDRESS) || '';
       const milvusAddress = window.localStorage.getItem(MILVUS_ADDRESS) || '';
       if (!milvusAddress) {
       if (!milvusAddress) {
@@ -36,6 +38,12 @@ export const AuthProvider = (props: { children: React.ReactNode }) => {
     check();
     check();
   }, [setAddress]);
   }, [setAddress]);
 
 
+  useEffect(() => {
+    document.title = address
+      ? `${address} - Milvus Insight`
+      : 'Milvus Insight';
+  }, [address]);
+
   return (
   return (
     <Provider value={{ isAuth, address, setAddress }}>
     <Provider value={{ isAuth, address, setAddress }}>
       {props.children}
       {props.children}

+ 28 - 4
client/src/hooks/Dialog.tsx

@@ -1,17 +1,20 @@
-import { useContext } from 'react';
+import { ReactElement, useContext } from 'react';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 import { Typography } from '@material-ui/core';
 import { Typography } from '@material-ui/core';
 import { rootContext } from '../context/Root';
 import { rootContext } from '../context/Root';
 import { CollectionView } from '../pages/collections/Types';
 import { CollectionView } from '../pages/collections/Types';
 import { PartitionView } from '../pages/partitions/Types';
 import { PartitionView } from '../pages/partitions/Types';
 import { StatusEnum } from '../components/status/Types';
 import { StatusEnum } from '../components/status/Types';
+import { CollectionData } from '../pages/overview/collectionCard/Types';
 
 
 // handle release and load dialog
 // handle release and load dialog
-export interface DialogHookProps {
+export interface LoadAndReleaseDialogHookProps {
   type: 'partition' | 'collection';
   type: 'partition' | 'collection';
 }
 }
 
 
-export const useDialogHook = (props: DialogHookProps) => {
+export const useLoadAndReleaseDialogHook = (
+  props: LoadAndReleaseDialogHookProps
+) => {
   const { type } = props;
   const { type } = props;
   const { setDialog } = useContext(rootContext);
   const { setDialog } = useContext(rootContext);
   const { t: dialogTrans } = useTranslation('dialog');
   const { t: dialogTrans } = useTranslation('dialog');
@@ -46,7 +49,7 @@ export const useDialogHook = (props: DialogHookProps) => {
   };
   };
 
 
   const handleAction = (
   const handleAction = (
-    data: PartitionView | CollectionView,
+    data: PartitionView | CollectionView | CollectionData,
     cb: (data: any) => Promise<any>
     cb: (data: any) => Promise<any>
   ) => {
   ) => {
     const actionType: 'release' | 'load' =
     const actionType: 'release' | 'load' =
@@ -69,3 +72,24 @@ export const useDialogHook = (props: DialogHookProps) => {
     handleAction,
     handleAction,
   };
   };
 };
 };
+
+export const useInsertDialogHook = () => {
+  const { setDialog } = useContext(rootContext);
+
+  const handleInsertDialog = (
+    // stepper container, contains all contents
+    component: ReactElement
+  ) => {
+    setDialog({
+      open: true,
+      type: 'custom',
+      params: {
+        component,
+      },
+    });
+  };
+
+  return {
+    handleInsertDialog,
+  };
+};

+ 4 - 1
client/src/hooks/Form.ts

@@ -51,7 +51,10 @@ export const useFormValidation = (form: IForm[]): IValidationInfo => {
   // validation detail about form item
   // validation detail about form item
   const [validation, setValidation] = useState(initValidation);
   const [validation, setValidation] = useState(initValidation);
   // overall validation result to control following actions
   // overall validation result to control following actions
-  const [disabled, setDisabled] = useState<boolean>(true);
+  const isOverallValid = Object.values(validation).every(
+    v => !(v as IValidationItem).result
+  );
+  const [disabled, setDisabled] = useState<boolean>(!isOverallValid);
 
 
   const checkIsValid = (param: ICheckValidParam): IValidationItem => {
   const checkIsValid = (param: ICheckValidParam): IValidationItem => {
     const { value, key, rules } = param;
     const { value, key, rules } = param;

+ 6 - 7
client/src/hooks/Navigation.ts

@@ -1,6 +1,5 @@
 import { useContext, useEffect } from 'react';
 import { useContext, useEffect } from 'react';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
-// import { useParams } from 'react-router-dom';
 import { navContext } from '../context/Navigation';
 import { navContext } from '../context/Navigation';
 import { ALL_ROUTER_TYPES, NavInfo } from '../router/Types';
 import { ALL_ROUTER_TYPES, NavInfo } from '../router/Types';
 
 
@@ -10,7 +9,7 @@ export const useNavigationHook = (
     collectionName: string;
     collectionName: string;
   }
   }
 ) => {
 ) => {
-  const { t } = useTranslation('nav');
+  const { t: navTrans } = useTranslation('nav');
   const { setNavInfo } = useContext(navContext);
   const { setNavInfo } = useContext(navContext);
   const { collectionName } = extraParam || { collectionName: '' };
   const { collectionName } = extraParam || { collectionName: '' };
 
 
@@ -18,7 +17,7 @@ export const useNavigationHook = (
     switch (type) {
     switch (type) {
       case ALL_ROUTER_TYPES.OVERVIEW: {
       case ALL_ROUTER_TYPES.OVERVIEW: {
         const navInfo: NavInfo = {
         const navInfo: NavInfo = {
-          navTitle: t('overview'),
+          navTitle: navTrans('overview'),
           backPath: '',
           backPath: '',
         };
         };
         setNavInfo(navInfo);
         setNavInfo(navInfo);
@@ -26,7 +25,7 @@ export const useNavigationHook = (
       }
       }
       case ALL_ROUTER_TYPES.COLLECTIONS: {
       case ALL_ROUTER_TYPES.COLLECTIONS: {
         const navInfo: NavInfo = {
         const navInfo: NavInfo = {
-          navTitle: t('collection'),
+          navTitle: navTrans('collection'),
           backPath: '',
           backPath: '',
         };
         };
         setNavInfo(navInfo);
         setNavInfo(navInfo);
@@ -40,9 +39,9 @@ export const useNavigationHook = (
         setNavInfo(navInfo);
         setNavInfo(navInfo);
         break;
         break;
       }
       }
-      case ALL_ROUTER_TYPES.CONSOLE: {
+      case ALL_ROUTER_TYPES.SEARCH: {
         const navInfo: NavInfo = {
         const navInfo: NavInfo = {
-          navTitle: t('console'),
+          navTitle: navTrans('search'),
           backPath: '',
           backPath: '',
         };
         };
         setNavInfo(navInfo);
         setNavInfo(navInfo);
@@ -51,5 +50,5 @@ export const useNavigationHook = (
       default:
       default:
         break;
         break;
     }
     }
-  }, [type, t, setNavInfo, collectionName]);
+  }, [type, navTrans, setNavInfo, collectionName]);
 };
 };

+ 1 - 1
client/src/http/Axios.ts

@@ -10,7 +10,7 @@ export const url =
 
 
 const axiosInstance = axios.create({
 const axiosInstance = axios.create({
   baseURL: `${url}/api/v1`,
   baseURL: `${url}/api/v1`,
-  timeout: 10000,
+  timeout: 60000,
 });
 });
 
 
 axiosInstance.interceptors.request.use(
 axiosInstance.interceptors.request.use(

+ 6 - 0
client/src/http/BaseModel.ts

@@ -78,4 +78,10 @@ export default class BaseModel {
     const res = await http.post(path, data);
     const res = await http.post(path, data);
     return res.data;
     return res.data;
   }
   }
+
+  static async vectorSearch(options: updateParamsType) {
+    const { path, data } = options;
+    const res = await http.post(path, data);
+    return res.data.data;
+  }
 }
 }

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

@@ -1,7 +1,11 @@
 import { ChildrenStatusType, StatusEnum } from '../components/status/Types';
 import { ChildrenStatusType, StatusEnum } from '../components/status/Types';
-import { CollectionView } from '../pages/collections/Types';
+import { CollectionView, InsertDataParam } from '../pages/collections/Types';
+import { Field } from '../pages/schema/Types';
+import { VectorSearchParam } from '../pages/seach/Types';
 import { IndexState, ShowCollectionsType } from '../types/Milvus';
 import { IndexState, ShowCollectionsType } from '../types/Milvus';
+import { formatNumber } from '../utils/Common';
 import BaseModel from './BaseModel';
 import BaseModel from './BaseModel';
+import { FieldHttp } from './Field';
 
 
 export class CollectionHttp extends BaseModel implements CollectionView {
 export class CollectionHttp extends BaseModel implements CollectionView {
   private autoID!: string;
   private autoID!: string;
@@ -11,6 +15,9 @@ export class CollectionHttp extends BaseModel implements CollectionView {
   private index_status!: string;
   private index_status!: string;
   private id!: string;
   private id!: string;
   private isLoaded!: boolean;
   private isLoaded!: boolean;
+  private schema!: {
+    fields: Field[];
+  };
 
 
   static COLLECTIONS_URL = '/collections';
   static COLLECTIONS_URL = '/collections';
   static COLLECTIONS_INDEX_STATUS_URL = '/collections/indexes/status';
   static COLLECTIONS_INDEX_STATUS_URL = '/collections/indexes/status';
@@ -29,6 +36,13 @@ export class CollectionHttp extends BaseModel implements CollectionView {
     return super.findAll({ path: this.COLLECTIONS_URL, params: data || {} });
     return super.findAll({ path: this.COLLECTIONS_URL, params: data || {} });
   }
   }
 
 
+  static getCollection(name: string) {
+    return super.search({
+      path: `${this.COLLECTIONS_URL}/${name}`,
+      params: {},
+    });
+  }
+
   static createCollection(data: any) {
   static createCollection(data: any) {
     return super.create({ path: this.COLLECTIONS_URL, data });
     return super.create({ path: this.COLLECTIONS_URL, data });
   }
   }
@@ -60,6 +74,20 @@ export class CollectionHttp extends BaseModel implements CollectionView {
     return super.search({ path: this.COLLECTIONS_STATISTICS_URL, params: {} });
     return super.search({ path: this.COLLECTIONS_STATISTICS_URL, params: {} });
   }
   }
 
 
+  static insertData(collectionName: string, param: InsertDataParam) {
+    return super.create({
+      path: `${this.COLLECTIONS_URL}/${collectionName}/insert`,
+      data: param,
+    });
+  }
+
+  static vectorSearchData(collectionName: string, params: VectorSearchParam) {
+    return super.vectorSearch({
+      path: `${this.COLLECTIONS_URL}/${collectionName}/search`,
+      data: params,
+    });
+  }
+
   get _autoId() {
   get _autoId() {
     return this.autoID;
     return this.autoID;
   }
   }
@@ -77,13 +105,17 @@ export class CollectionHttp extends BaseModel implements CollectionView {
   }
   }
 
 
   get _rowCount() {
   get _rowCount() {
-    return this.rowCount;
+    return formatNumber(Number(this.rowCount));
   }
   }
 
 
   get _status() {
   get _status() {
     return this.isLoaded === true ? StatusEnum.loaded : StatusEnum.unloaded;
     return this.isLoaded === true ? StatusEnum.loaded : StatusEnum.unloaded;
   }
   }
 
 
+  get _fields() {
+    return this.schema.fields.map(f => new FieldHttp(f));
+  }
+
   get _indexState() {
   get _indexState() {
     switch (this.index_status) {
     switch (this.index_status) {
       case IndexState.InProgress:
       case IndexState.InProgress:

+ 13 - 1
client/src/http/Index.ts

@@ -6,6 +6,8 @@ import {
 } from '../pages/schema/Types';
 } from '../pages/schema/Types';
 import { ManageRequestMethods } from '../types/Common';
 import { ManageRequestMethods } from '../types/Common';
 import { IndexState } from '../types/Milvus';
 import { IndexState } from '../types/Milvus';
+import { findKeyValue } from '../utils/Common';
+import { getKeyValueListFromJsonString } from '../utils/Format';
 import BaseModel from './BaseModel';
 import BaseModel from './BaseModel';
 
 
 export class IndexHttp extends BaseModel implements IndexView {
 export class IndexHttp extends BaseModel implements IndexView {
@@ -62,10 +64,20 @@ export class IndexHttp extends BaseModel implements IndexView {
   }
   }
 
 
   get _indexParameterPairs() {
   get _indexParameterPairs() {
-    return this.params.filter(p => p.key !== 'index_type');
+    const metricType = this.params.filter(v => v.key === 'metric_type');
+    // parms is json string, so we need parse it to key value array
+    const params = findKeyValue(this.params, 'params');
+    if (params) {
+      return [...metricType, ...getKeyValueListFromJsonString(params)];
+    }
+    return metricType;
   }
   }
 
 
   get _fieldName() {
   get _fieldName() {
     return this.field_name;
     return this.field_name;
   }
   }
+
+  get _metricType() {
+    return this.params.find(p => p.key === 'metric_type')?.value || '';
+  }
 }
 }

+ 10 - 0
client/src/http/Milvus.ts

@@ -3,6 +3,7 @@ import BaseModel from './BaseModel';
 export class MilvusHttp extends BaseModel {
 export class MilvusHttp extends BaseModel {
   static CONNECT_URL = '/milvus/connect';
   static CONNECT_URL = '/milvus/connect';
   static CHECK_URL = '/milvus/check';
   static CHECK_URL = '/milvus/check';
+  static FLUSH_URL = '/milvus/flush';
 
 
   constructor(props: {}) {
   constructor(props: {}) {
     super(props);
     super(props);
@@ -16,4 +17,13 @@ export class MilvusHttp extends BaseModel {
   static check(address: string) {
   static check(address: string) {
     return super.search({ path: this.CHECK_URL, params: { address } });
     return super.search({ path: this.CHECK_URL, params: { address } });
   }
   }
+
+  static flush(collectionName: string) {
+    return super.update({
+      path: this.FLUSH_URL,
+      data: {
+        collection_names: [collectionName],
+      },
+    });
+  }
 }
 }

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

@@ -11,6 +11,10 @@ const btnTrans = {
   release: 'Release',
   release: 'Release',
   create: 'Create',
   create: 'Create',
   load: 'Load',
   load: 'Load',
+  insert: 'Import Data',
+  next: 'Next',
+  previous: 'Previous',
+  done: 'Done',
 };
 };
 
 
 export default btnTrans;
 export default btnTrans;

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

@@ -7,6 +7,7 @@ const collectionTrans = {
 
 
   create: 'Create Collection',
   create: 'Create Collection',
   delete: 'delete',
   delete: 'delete',
+  deleteTooltip: 'Please select at least one item to delete.',
 
 
   collection: 'Collection',
   collection: 'Collection',
 
 

+ 28 - 0
client/src/i18n/cn/insert.ts

@@ -0,0 +1,28 @@
+const insertTrans = {
+  import: 'Import Data',
+  targetTip: 'Where to put your data',
+  file: 'File',
+  uploaderLabel: 'Choose CSV File',
+  fileNamePlaceHolder: 'No file selected',
+  sample: 'CSV Sample',
+  noteTitle: 'Note',
+  notes: [
+    `Make sure column names in the data are same as the field label names in Schema.`,
+    `Data size should be less than 5MB and the number of rows should be less than 100000, for the data to be imported properly.`,
+    `The "Import Data" option will only append new records. You cannot update existing records using this option.`,
+  ],
+  overSizeWarning: 'File data size should less than 5MB',
+  isContainFieldNames: 'First row contains field names?',
+  uploadFieldNamesLenWarning:
+    'Uploaded data column count is not equal to schema count',
+  previewTipData: 'Data Preview(Top 4 rows shown)',
+  previewTipAction: '*Change header cell selector value to edit field name',
+  requiredFieldName: 'Field Name*',
+
+  statusLoading: 'Your data is importing now...It may take few minutes',
+  statusLoadingTip: 'Please wait patiently, thank you',
+  statusSuccess: 'Import Data Successfully!',
+  statusError: 'Import Data Failed!',
+};
+
+export default insertTrans;

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

@@ -2,6 +2,7 @@ const navTrans = {
   overview: 'Overview',
   overview: 'Overview',
   collection: 'Collection',
   collection: 'Collection',
   console: 'Search Console',
   console: 'Search Console',
+  search: 'Vector Search',
 };
 };
 
 
 export default navTrans;
 export default navTrans;

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

@@ -0,0 +1,15 @@
+const searchTrans = {
+  firstTip: '1. Enter vector value',
+  secondTip: '2. Choose collection and field',
+  thirdTip: '3. Set search parameters',
+  vectorPlaceholder: 'Please input your vector value here, e.g. [1, 2, 3, 4]',
+  collection: 'Choose Collection',
+  field: 'Choose Field',
+  startTip: 'Start your vector search',
+  empty: 'No result',
+  result: 'Search Results',
+  topK: 'TopK {{number}}',
+  filter: 'Advanced Filter',
+};
+
+export default searchTrans;

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

@@ -3,6 +3,8 @@ const warningTrans = {
   positive: '{{name}} should be positive',
   positive: '{{name}} should be positive',
   integer: '{{name}} should be integers',
   integer: '{{name}} should be integers',
   range: 'range is {{min}} ~ {{max}}',
   range: 'range is {{min}} ~ {{max}}',
+  specValueOrRange:
+    '{{name}} should be {{specValue}}, or in range {{min}} ~ {{max}}',
 };
 };
 
 
 export default warningTrans;
 export default warningTrans;

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

@@ -11,6 +11,10 @@ const btnTrans = {
   delete: 'Delete',
   delete: 'Delete',
   release: 'Release',
   release: 'Release',
   load: 'Load',
   load: 'Load',
+  insert: 'Import Data',
+  next: 'Next',
+  previous: 'Previous',
+  done: 'Done',
 };
 };
 
 
 export default btnTrans;
 export default btnTrans;

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

@@ -7,6 +7,7 @@ const collectionTrans = {
 
 
   create: 'Create Collection',
   create: 'Create Collection',
   delete: 'delete',
   delete: 'delete',
+  deleteTooltip: 'Please select at least one item to delete.',
 
 
   collection: 'Collection',
   collection: 'Collection',
 
 

+ 28 - 0
client/src/i18n/en/insert.ts

@@ -0,0 +1,28 @@
+const insertTrans = {
+  import: 'Import Data',
+  targetTip: 'Where to put your data',
+  file: 'File',
+  uploaderLabel: 'Choose CSV File',
+  fileNamePlaceHolder: 'No file selected',
+  sample: 'CSV Sample',
+  noteTitle: 'Note',
+  notes: [
+    `Make sure column names in the data are same as the field label names in Schema.`,
+    `Data size should be less than 5MB and the number of rows should be less than 100000, for the data to be imported properly.`,
+    `The "Import Data" option will only append new records. You cannot update existing records using this option.`,
+  ],
+  overSizeWarning: 'File data size should less than 5MB',
+  isContainFieldNames: 'First row contains field names?',
+  uploadFieldNamesLenWarning:
+    'Uploaded data column count is not equal to schema count',
+  previewTipData: 'Data Preview(Top 4 rows shown)',
+  previewTipAction: '*Change header cell selector value to edit field name',
+  requiredFieldName: 'Field Name*',
+
+  statusLoading: 'Your data is importing now...It may take few minutes',
+  statusLoadingTip: 'Please wait patiently, thank you',
+  statusSuccess: 'Import Data Successfully!',
+  statusError: 'Import Data Failed!',
+};
+
+export default insertTrans;

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

@@ -2,6 +2,7 @@ const navTrans = {
   overview: 'Overview',
   overview: 'Overview',
   collection: 'Collection',
   collection: 'Collection',
   console: 'Search Console',
   console: 'Search Console',
+  search: 'Vector Search',
 };
 };
 
 
 export default navTrans;
 export default navTrans;

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

@@ -0,0 +1,15 @@
+const searchTrans = {
+  firstTip: '1. Enter vector value',
+  secondTip: '2. Choose collection and field',
+  thirdTip: '3. Set search parameters',
+  vectorPlaceholder: 'Please input your vector value here, e.g. [1, 2, 3, 4]',
+  collection: 'Choose Collection',
+  field: 'Choose Field',
+  startTip: 'Start your vector search',
+  empty: 'No result',
+  result: 'Search Results',
+  topK: 'TopK {{number}}',
+  filter: 'Advanced Filter',
+};
+
+export default searchTrans;

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

@@ -3,6 +3,8 @@ const warningTrans = {
   positive: '{{name}} should be positive',
   positive: '{{name}} should be positive',
   integer: '{{name}} should be integers',
   integer: '{{name}} should be integers',
   range: 'range is {{min}} ~ {{max}}',
   range: 'range is {{min}} ~ {{max}}',
+  specValueOrRange:
+    '{{name}} should be {{specValue}}, or in range {{min}} ~ {{max}}',
 };
 };
 
 
 export default warningTrans;
 export default warningTrans;

+ 8 - 0
client/src/i18n/index.ts

@@ -21,6 +21,10 @@ import successEn from './en/success';
 import successCn from './cn/success';
 import successCn from './cn/success';
 import indexEn from './en/index';
 import indexEn from './en/index';
 import indexCn from './cn/index';
 import indexCn from './cn/index';
+import insertEn from './en/insert';
+import insertCn from './cn/insert';
+import searchEn from './en/search';
+import searchCn from './cn/search';
 
 
 export const resources = {
 export const resources = {
   cn: {
   cn: {
@@ -34,6 +38,8 @@ export const resources = {
     partition: partitionCn,
     partition: partitionCn,
     success: successCn,
     success: successCn,
     index: indexCn,
     index: indexCn,
+    insert: insertCn,
+    search: searchCn,
   },
   },
   en: {
   en: {
     translation: commonEn,
     translation: commonEn,
@@ -46,6 +52,8 @@ export const resources = {
     partition: partitionEn,
     partition: partitionEn,
     success: successEn,
     success: successEn,
     index: indexEn,
     index: indexEn,
+    insert: insertEn,
+    search: searchEn,
   },
   },
 };
 };
 
 

+ 69 - 5
client/src/pages/collections/Collections.tsx

@@ -4,7 +4,12 @@ import { useNavigationHook } from '../../hooks/Navigation';
 import { ALL_ROUTER_TYPES } from '../../router/Types';
 import { ALL_ROUTER_TYPES } from '../../router/Types';
 import MilvusGrid from '../../components/grid/Grid';
 import MilvusGrid from '../../components/grid/Grid';
 import CustomToolBar from '../../components/grid/ToolBar';
 import CustomToolBar from '../../components/grid/ToolBar';
-import { CollectionCreateParam, CollectionView, DataTypeEnum } from './Types';
+import {
+  CollectionCreateParam,
+  CollectionView,
+  DataTypeEnum,
+  InsertDataParam,
+} from './Types';
 import { ColDefinitionsType, ToolBarConfig } from '../../components/grid/Types';
 import { ColDefinitionsType, ToolBarConfig } from '../../components/grid/Types';
 import { usePaginationHook } from '../../hooks/Pagination';
 import { usePaginationHook } from '../../hooks/Pagination';
 import icons from '../../components/icons/Icons';
 import icons from '../../components/icons/Icons';
@@ -19,9 +24,14 @@ import { rootContext } from '../../context/Root';
 import CreateCollection from './Create';
 import CreateCollection from './Create';
 import DeleteTemplate from '../../components/customDialog/DeleteDialogTemplate';
 import DeleteTemplate from '../../components/customDialog/DeleteDialogTemplate';
 import { CollectionHttp } from '../../http/Collection';
 import { CollectionHttp } from '../../http/Collection';
-import { useDialogHook } from '../../hooks/Dialog';
+import {
+  useInsertDialogHook,
+  useLoadAndReleaseDialogHook,
+} from '../../hooks/Dialog';
 import Highlighter from 'react-highlight-words';
 import Highlighter from 'react-highlight-words';
 import { parseLocationSearch } from '../../utils/Format';
 import { parseLocationSearch } from '../../utils/Format';
+import InsertContainer from '../../components/insert/Container';
+import { MilvusHttp } from '../../http/Milvus';
 
 
 const useStyles = makeStyles((theme: Theme) => ({
 const useStyles = makeStyles((theme: Theme) => ({
   emptyWrapper: {
   emptyWrapper: {
@@ -52,7 +62,8 @@ const { search = '' } = parseLocationSearch(window.location.search);
 
 
 const Collections = () => {
 const Collections = () => {
   useNavigationHook(ALL_ROUTER_TYPES.COLLECTIONS);
   useNavigationHook(ALL_ROUTER_TYPES.COLLECTIONS);
-  const { handleAction } = useDialogHook({ type: 'collection' });
+  const { handleAction } = useLoadAndReleaseDialogHook({ type: 'collection' });
+  const { handleInsertDialog } = useInsertDialogHook();
   const [collections, setCollections] = useState<CollectionView[]>([]);
   const [collections, setCollections] = useState<CollectionView[]>([]);
   const [searchedCollections, setSearchedCollections] = useState<
   const [searchedCollections, setSearchedCollections] = useState<
     CollectionView[]
     CollectionView[]
@@ -128,6 +139,31 @@ const Collections = () => {
     fetchData();
     fetchData();
   }, [fetchData]);
   }, [fetchData]);
 
 
+  const handleInsert = async (
+    collectionName: string,
+    partitionName: string,
+    fieldData: any[]
+  ): Promise<{ result: boolean; msg: string }> => {
+    const param: InsertDataParam = {
+      partition_names: [partitionName],
+      fields_data: fieldData,
+    };
+    try {
+      await CollectionHttp.insertData(collectionName, param);
+      await MilvusHttp.flush(collectionName);
+      // update collections
+      fetchData();
+      return { result: true, msg: '' };
+    } catch (err) {
+      const {
+        response: {
+          data: { message },
+        },
+      } = err;
+      return { result: false, msg: message || '' };
+    }
+  };
+
   const handleCreateCollection = async (param: CollectionCreateParam) => {
   const handleCreateCollection = async (param: CollectionCreateParam) => {
     const data: CollectionCreateParam = JSON.parse(JSON.stringify(param));
     const data: CollectionCreateParam = JSON.parse(JSON.stringify(param));
     const vectorType = [DataTypeEnum.BinaryVector, DataTypeEnum.FloatVector];
     const vectorType = [DataTypeEnum.BinaryVector, DataTypeEnum.FloatVector];
@@ -224,6 +260,32 @@ const Collections = () => {
       },
       },
       icon: 'add',
       icon: 'add',
     },
     },
+    {
+      label: btnTrans('insert'),
+      onClick: () => {
+        handleInsertDialog(
+          <InsertContainer
+            collections={collections}
+            defaultSelectedCollection={
+              selectedCollections.length === 1
+                ? selectedCollections[0]._name
+                : ''
+            }
+            // user can't select partition on collection page, so default value is ''
+            defaultSelectedPartition={''}
+            handleInsert={handleInsert}
+          />
+        );
+      },
+      /**
+       * insert validation:
+       * 1. At least 1 available collection
+       * 2. selected collections quantity shouldn't over 1
+       */
+      disabled: () =>
+        collectionList.length === 0 || selectedCollections.length > 1,
+      btnVariant: 'outlined',
+    },
     {
     {
       type: 'iconBtn',
       type: 'iconBtn',
       onClick: () => {
       onClick: () => {
@@ -246,6 +308,8 @@ const Collections = () => {
       },
       },
       label: collectionTrans('delete'),
       label: collectionTrans('delete'),
       icon: 'delete',
       icon: 'delete',
+      // tooltip: collectionTrans('deleteTooltip'),
+      disabledTooltip: collectionTrans('deleteTooltip'),
       disabled: data => data.length === 0,
       disabled: data => data.length === 0,
     },
     },
     {
     {
@@ -269,12 +333,14 @@ const Collections = () => {
       id: 'nameElement',
       id: 'nameElement',
       align: 'left',
       align: 'left',
       disablePadding: true,
       disablePadding: true,
+      sortBy: '_name',
       label: collectionTrans('name'),
       label: collectionTrans('name'),
     },
     },
     {
     {
       id: 'statusElement',
       id: 'statusElement',
       align: 'left',
       align: 'left',
       disablePadding: false,
       disablePadding: false,
+      sortBy: '_status',
       label: collectionTrans('status'),
       label: collectionTrans('status'),
     },
     },
     {
     {
@@ -348,8 +414,6 @@ const Collections = () => {
           rows={collectionList}
           rows={collectionList}
           rowCount={total}
           rowCount={total}
           primaryKey="_name"
           primaryKey="_name"
-          openCheckBox={true}
-          showHoverStyle={true}
           selected={selectedCollections}
           selected={selectedCollections}
           setSelected={handleSelectChange}
           setSelected={handleSelectChange}
           page={currentPage}
           page={currentPage}

+ 5 - 2
client/src/pages/collections/Create.tsx

@@ -160,13 +160,16 @@ const CreateCollection: FC<CollectionCreateProps> = ({ handleCreate }) => {
     const param: CollectionCreateParam = {
     const param: CollectionCreateParam = {
       ...form,
       ...form,
       fields: fields.map(v => {
       fields: fields.map(v => {
-        return {
+        const data: any = {
           name: v.name,
           name: v.name,
           description: v.description,
           description: v.description,
           is_primary_key: v.is_primary_key,
           is_primary_key: v.is_primary_key,
           data_type: v.data_type,
           data_type: v.data_type,
           dimension: vectorType.includes(v.data_type) ? v.dimension : undefined,
           dimension: vectorType.includes(v.data_type) ? v.dimension : undefined,
         };
         };
+
+        v.is_primary_key && (data.autoID = form.autoID);
+        return data;
       }),
       }),
     };
     };
     handleCreate(param);
     handleCreate(param);
@@ -175,7 +178,7 @@ const CreateCollection: FC<CollectionCreateProps> = ({ handleCreate }) => {
   return (
   return (
     <DialogTemplate
     <DialogTemplate
       title={collectionTrans('createTitle')}
       title={collectionTrans('createTitle')}
-      handleCancel={handleCloseDialog}
+      handleClose={handleCloseDialog}
       confirmLabel={btnTrans('create')}
       confirmLabel={btnTrans('create')}
       handleConfirm={handleCreateCollection}
       handleConfirm={handleCreateCollection}
       confirmDisabled={disabled || !allFieldsValid}
       confirmDisabled={disabled || !allFieldsValid}

+ 3 - 3
client/src/pages/collections/CreateFields.tsx

@@ -148,6 +148,7 @@ const CreateFields: FC<CreateFieldsProps> = ({
   ) => {
   ) => {
     return (
     return (
       <CustomSelector
       <CustomSelector
+        wrapperClass={classes.select}
         options={type === 'all' ? ALL_OPTIONS : VECTOR_FIELDS_OPTIONS}
         options={type === 'all' ? ALL_OPTIONS : VECTOR_FIELDS_OPTIONS}
         onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
         onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
           onChange(e.target.value as DataTypeEnum);
           onChange(e.target.value as DataTypeEnum);
@@ -155,7 +156,6 @@ const CreateFields: FC<CreateFieldsProps> = ({
         value={value}
         value={value}
         variant="filled"
         variant="filled"
         label={label}
         label={label}
-        classes={{ root: classes.select }}
       />
       />
     );
     );
   };
   };
@@ -175,7 +175,7 @@ const CreateFields: FC<CreateFieldsProps> = ({
       <TextField
       <TextField
         label={label}
         label={label}
         // value={value}
         // value={value}
-        onBlur={(e: React.ChangeEvent<{ value: unknown }>) => {
+        onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
           handleChange(e.target.value as string);
           handleChange(e.target.value as string);
         }}
         }}
         variant="filled"
         variant="filled"
@@ -349,7 +349,7 @@ const CreateFields: FC<CreateFieldsProps> = ({
             setAutoID(autoId);
             setAutoID(autoId);
           }}
           }}
           variant="filled"
           variant="filled"
-          classes={{ root: classes.select }}
+          wrapperClass={classes.select}
         />
         />
 
 
         {generateDesc(field)}
         {generateDesc(field)}

+ 8 - 0
client/src/pages/collections/Types.ts

@@ -1,5 +1,6 @@
 import { Dispatch, ReactElement, SetStateAction } from 'react';
 import { Dispatch, ReactElement, SetStateAction } from 'react';
 import { ChildrenStatusType, StatusEnum } from '../../components/status/Types';
 import { ChildrenStatusType, StatusEnum } from '../../components/status/Types';
+import { FieldData } from '../schema/Types';
 
 
 export interface CollectionData {
 export interface CollectionData {
   _name: string;
   _name: string;
@@ -8,6 +9,7 @@ export interface CollectionData {
   _rowCount: string;
   _rowCount: string;
   _desc: string;
   _desc: string;
   _indexState: ChildrenStatusType;
   _indexState: ChildrenStatusType;
+  _fields?: FieldData[];
 }
 }
 
 
 export interface CollectionView extends CollectionData {
 export interface CollectionView extends CollectionData {
@@ -75,3 +77,9 @@ export interface CreateFieldsProps {
   autoID: boolean;
   autoID: boolean;
   setAutoID: (value: boolean) => void;
   setAutoID: (value: boolean) => void;
 }
 }
+
+export interface InsertDataParam {
+  partition_names: string[];
+  // e.g. [{vector: [1,2,3], age: 10}]
+  fields_data: any[];
+}

+ 33 - 3
client/src/pages/connect/Connect.tsx

@@ -13,6 +13,8 @@ import { authContext } from '../../context/Auth';
 import { MilvusHttp } from '../../http/Milvus';
 import { MilvusHttp } from '../../http/Milvus';
 import { rootContext } from '../../context/Root';
 import { rootContext } from '../../context/Root';
 import { MILVUS_ADDRESS } from '../../consts/Localstorage';
 import { MILVUS_ADDRESS } from '../../consts/Localstorage';
+import { formatAddress } from '../../utils/Format';
+// import { io } from "socket.io-client";
 
 
 const useStyles = makeStyles((theme: Theme) => ({
 const useStyles = makeStyles((theme: Theme) => ({
   wrapper: {
   wrapper: {
@@ -73,10 +75,11 @@ const Connect = () => {
   };
   };
 
 
   const handleConnect = async () => {
   const handleConnect = async () => {
-    await MilvusHttp.connect(form.address);
+    const address = formatAddress(form.address);
+    await MilvusHttp.connect(address);
     openSnackBar(successTrans('connect'));
     openSnackBar(successTrans('connect'));
-    setAddress(form.address);
-    window.localStorage.setItem(MILVUS_ADDRESS, form.address);
+    setAddress(address);
+    window.localStorage.setItem(MILVUS_ADDRESS, address);
     history.push('/');
     history.push('/');
   };
   };
 
 
@@ -97,6 +100,33 @@ const Connect = () => {
     defaultValue: form.address,
     defaultValue: form.address,
   };
   };
 
 
+  // test code for socket
+  // useEffect(() => {
+  //   const socket = io('http://localhost:3002');
+  //   socket.on('connect', function () {
+  //     console.log('Connected');
+
+  //     socket.emit('identity', 0, (res: any) =>
+  //       console.log(res));
+
+  //     socket.emit('events', { test: 'events' });
+
+  //     socket.emit('senddata', { test: 'senddata' });
+  //   });
+  //   socket.on('events', (data: any) => {
+  //     console.log('event', data);
+  //   });
+  //   socket.on('senddata', (data: any) => {
+  //     console.log('senddata', data);
+  //   });
+  //   socket.on('exception', (data: any) => {
+  //     console.log('event', data);
+  //   });
+  //   socket.on('disconnect', () => {
+  //     console.log('Disconnected');
+  //   });
+  // }, []);
+
   return (
   return (
     <ConnectContainer>
     <ConnectContainer>
       <section className={classes.wrapper}>
       <section className={classes.wrapper}>

+ 39 - 15
client/src/pages/overview/Overview.tsx

@@ -1,9 +1,11 @@
 import { makeStyles, Theme, Typography } from '@material-ui/core';
 import { makeStyles, Theme, Typography } from '@material-ui/core';
-import { useEffect, useMemo, useState } from 'react';
+import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 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 { StatusEnum } from '../../components/status/Types';
 import { StatusEnum } from '../../components/status/Types';
+import { rootContext } from '../../context/Root';
+import { useLoadAndReleaseDialogHook } from '../../hooks/Dialog';
 import { useNavigationHook } from '../../hooks/Navigation';
 import { useNavigationHook } from '../../hooks/Navigation';
 import { CollectionHttp } from '../../http/Collection';
 import { CollectionHttp } from '../../http/Collection';
 import { ALL_ROUTER_TYPES } from '../../router/Types';
 import { ALL_ROUTER_TYPES } from '../../router/Types';
@@ -29,9 +31,11 @@ const useStyles = makeStyles((theme: Theme) => ({
 
 
 const Overview = () => {
 const Overview = () => {
   useNavigationHook(ALL_ROUTER_TYPES.OVERVIEW);
   useNavigationHook(ALL_ROUTER_TYPES.OVERVIEW);
+  const { handleAction } = useLoadAndReleaseDialogHook({ type: 'collection' });
   const classes = useStyles();
   const classes = useStyles();
   const { t: overviewTrans } = useTranslation('overview');
   const { t: overviewTrans } = useTranslation('overview');
   const { t: collectionTrans } = useTranslation('collection');
   const { t: collectionTrans } = useTranslation('collection');
+  const { t: successTrans } = useTranslation('success');
   const [statistics, setStatistics] = useState<{
   const [statistics, setStatistics] = useState<{
     collectionCount: number;
     collectionCount: number;
     totalData: number;
     totalData: number;
@@ -41,18 +45,34 @@ const Overview = () => {
   });
   });
 
 
   const [loadCollections, setLoadCollections] = useState<CollectionHttp[]>([]);
   const [loadCollections, setLoadCollections] = useState<CollectionHttp[]>([]);
+  const { openSnackBar } = useContext(rootContext);
+
+  const fetchData = useCallback(async () => {
+    const res = await CollectionHttp.getStatistics();
+    const loadCollections = await CollectionHttp.getCollections({
+      type: ShowCollectionsType.InMemory,
+    });
+    setStatistics(res);
+    setLoadCollections(loadCollections);
+  }, []);
 
 
   useEffect(() => {
   useEffect(() => {
-    const fetchData = async () => {
-      const res = await CollectionHttp.getStatistics();
-      const loadCollections = await CollectionHttp.getCollections({
-        type: ShowCollectionsType.InMemory,
-      });
-      setStatistics(res);
-      setLoadCollections(loadCollections);
-    };
     fetchData();
     fetchData();
-  }, []);
+  }, [fetchData]);
+
+  const fetchRelease = async (data: CollectionData) => {
+    const name = data._name;
+    const res = await CollectionHttp.releaseCollection(name);
+    openSnackBar(
+      successTrans('release', { name: collectionTrans('collection') })
+    );
+    fetchData();
+    return res;
+  };
+
+  const handleRelease = (data: CollectionData) => {
+    handleAction(data, fetchRelease);
+  };
 
 
   const statisticsData = useMemo(() => {
   const statisticsData = useMemo(() => {
     return {
     return {
@@ -80,10 +100,10 @@ const Overview = () => {
 
 
   const loadCollectionsData: CollectionData[] = useMemo(() => {
   const loadCollectionsData: CollectionData[] = useMemo(() => {
     return loadCollections.map(v => ({
     return loadCollections.map(v => ({
-      id: v._id,
-      name: v._name,
-      status: StatusEnum.loaded,
-      rowCount: Number(v._rowCount),
+      _id: v._id,
+      _name: v._name,
+      _status: StatusEnum.loaded,
+      _rowCount: v._rowCount,
     }));
     }));
   }, [loadCollections]);
   }, [loadCollections]);
 
 
@@ -98,7 +118,11 @@ const Overview = () => {
       {loadCollectionsData.length > 0 ? (
       {loadCollectionsData.length > 0 ? (
         <div className={classes.cardsWrapper}>
         <div className={classes.cardsWrapper}>
           {loadCollectionsData.map(collection => (
           {loadCollectionsData.map(collection => (
-            <CollectionCard key={collection.id} data={collection} />
+            <CollectionCard
+              key={collection._id}
+              data={collection}
+              handleRelease={handleRelease}
+            />
           ))}
           ))}
         </div>
         </div>
       ) : (
       ) : (

+ 6 - 3
client/src/pages/overview/collectionCard/CollectionCard.tsx

@@ -53,17 +53,20 @@ const useStyles = makeStyles((theme: Theme) => ({
 
 
 const CollectionCard: FC<CollectionCardProps> = ({
 const CollectionCard: FC<CollectionCardProps> = ({
   data,
   data,
+  handleRelease,
   wrapperClass = '',
   wrapperClass = '',
 }) => {
 }) => {
   const classes = useStyles();
   const classes = useStyles();
-  const { name, status, rowCount } = data;
+  const { _name: name, _status: status, _rowCount: rowCount } = data;
   const RightArrowIcon = icons.rightArrow;
   const RightArrowIcon = icons.rightArrow;
   const InfoIcon = icons.info;
   const InfoIcon = icons.info;
   const ReleaseIcon = icons.release;
   const ReleaseIcon = icons.release;
   const { t: collectionTrans } = useTranslation('collection');
   const { t: collectionTrans } = useTranslation('collection');
   const { t: btnTrans } = useTranslation('btn');
   const { t: btnTrans } = useTranslation('btn');
 
 
-  const handleRelease = () => {};
+  const onReleaseClick = () => {
+    handleRelease(data);
+  };
 
 
   return (
   return (
     <div className={`card-wrapper ${classes.wrapper} ${wrapperClass}`}>
     <div className={`card-wrapper ${classes.wrapper} ${wrapperClass}`}>
@@ -86,7 +89,7 @@ const CollectionCard: FC<CollectionCardProps> = ({
         <Typography className={classes.rowCount}>{rowCount}</Typography>
         <Typography className={classes.rowCount}>{rowCount}</Typography>
       </div>
       </div>
       <Divider classes={{ root: classes.divider }} />
       <Divider classes={{ root: classes.divider }} />
-      <CustomButton variant="contained" onClick={handleRelease}>
+      <CustomButton variant="contained" onClick={onReleaseClick}>
         <ReleaseIcon classes={{ root: `${classes.icon} ${classes.release}` }} />
         <ReleaseIcon classes={{ root: `${classes.icon} ${classes.release}` }} />
         {btnTrans('release')}
         {btnTrans('release')}
       </CustomButton>
       </CustomButton>

+ 5 - 4
client/src/pages/overview/collectionCard/Types.ts

@@ -2,12 +2,13 @@ import { StatusEnum } from '../../../components/status/Types';
 
 
 export interface CollectionCardProps {
 export interface CollectionCardProps {
   data: CollectionData;
   data: CollectionData;
+  handleRelease: (data: CollectionData) => void;
   wrapperClass?: string;
   wrapperClass?: string;
 }
 }
 
 
 export interface CollectionData {
 export interface CollectionData {
-  name: string;
-  status: StatusEnum;
-  id: string;
-  rowCount: number;
+  _name: string;
+  _status: StatusEnum;
+  _id: string;
+  _rowCount: string;
 }
 }

+ 1 - 1
client/src/pages/partitions/Create.tsx

@@ -63,7 +63,7 @@ const CreatePartition: FC<PartitionCreateProps> = ({
   return (
   return (
     <DialogTemplate
     <DialogTemplate
       title={partitionTrans('createTitle')}
       title={partitionTrans('createTitle')}
-      handleCancel={handleClose}
+      handleClose={handleClose}
       confirmLabel={btnTrans('create')}
       confirmLabel={btnTrans('create')}
       handleConfirm={handleCreatePartition}
       handleConfirm={handleCreatePartition}
       confirmDisabled={disabled}
       confirmDisabled={disabled}

+ 70 - 2
client/src/pages/partitions/Partitions.tsx

@@ -21,6 +21,13 @@ import { ManageRequestMethods } from '../../types/Common';
 import DeleteTemplate from '../../components/customDialog/DeleteDialogTemplate';
 import DeleteTemplate from '../../components/customDialog/DeleteDialogTemplate';
 import Highlighter from 'react-highlight-words';
 import Highlighter from 'react-highlight-words';
 import { parseLocationSearch } from '../../utils/Format';
 import { parseLocationSearch } from '../../utils/Format';
+import { useInsertDialogHook } from '../../hooks/Dialog';
+import InsertContainer from '../../components/insert/Container';
+import { CollectionHttp } from '../../http/Collection';
+import { FieldHttp } from '../../http/Field';
+import { Field } from '../schema/Types';
+import { InsertDataParam } from '../collections/Types';
+import { MilvusHttp } from '../../http/Milvus';
 
 
 const useStyles = makeStyles((theme: Theme) => ({
 const useStyles = makeStyles((theme: Theme) => ({
   wrapper: {
   wrapper: {
@@ -49,6 +56,8 @@ const Partitions: FC<{
   const { t: btnTrans } = useTranslation('btn');
   const { t: btnTrans } = useTranslation('btn');
   const { t: dialogTrans } = useTranslation('dialog');
   const { t: dialogTrans } = useTranslation('dialog');
   const InfoIcon = icons.info;
   const InfoIcon = icons.info;
+
+  const { handleInsertDialog } = useInsertDialogHook();
   // const LoadIcon = icons.load;
   // const LoadIcon = icons.load;
   // const ReleaseIcon = icons.release;
   // const ReleaseIcon = icons.release;
 
 
@@ -102,6 +111,11 @@ const Partitions: FC<{
     [classes.highlight]
     [classes.highlight]
   );
   );
 
 
+  const fetchCollectionDetail = async (name: string) => {
+    const res = await CollectionHttp.getCollection(name);
+    return res;
+  };
+
   useEffect(() => {
   useEffect(() => {
     fetchPartitions(collectionName);
     fetchPartitions(collectionName);
   }, [collectionName, fetchPartitions]);
   }, [collectionName, fetchPartitions]);
@@ -172,6 +186,32 @@ const Partitions: FC<{
     }, 300);
     }, 300);
   };
   };
 
 
+  const handleInsert = async (
+    collectionName: string,
+    partitionName: string,
+    fieldData: any[]
+  ): Promise<{ result: boolean; msg: string }> => {
+    const param: InsertDataParam = {
+      partition_names: [partitionName],
+      fields_data: fieldData,
+    };
+    try {
+      await CollectionHttp.insertData(collectionName, param);
+      await MilvusHttp.flush(collectionName);
+      // update partitions
+      fetchPartitions(collectionName);
+
+      return { result: true, msg: '' };
+    } catch (err) {
+      const {
+        response: {
+          data: { message },
+        },
+      } = err;
+      return { result: false, msg: message || '' };
+    }
+  };
+
   const toolbarConfigs: ToolBarConfig[] = [
   const toolbarConfigs: ToolBarConfig[] = [
     {
     {
       label: t('create'),
       label: t('create'),
@@ -191,6 +231,36 @@ const Partitions: FC<{
       },
       },
       icon: 'add',
       icon: 'add',
     },
     },
+    {
+      label: btnTrans('insert'),
+      onClick: async () => {
+        const collection = await fetchCollectionDetail(collectionName);
+        const schema = collection.schema.fields.map(
+          (f: Field) => new FieldHttp(f)
+        );
+
+        handleInsertDialog(
+          <InsertContainer
+            schema={schema}
+            defaultSelectedCollection={collectionName}
+            defaultSelectedPartition={
+              selectedPartitions.length === 1
+                ? selectedPartitions[0]._formatName
+                : ''
+            }
+            partitions={partitions}
+            handleInsert={handleInsert}
+          />
+        );
+      },
+      /**
+       * insert validation:
+       * 1. At least 1 available partition
+       * 2. selected partition quantity shouldn't over 1
+       */
+      disabled: () => partitions.length === 0 || selectedPartitions.length > 1,
+      btnVariant: 'outlined',
+    },
     {
     {
       type: 'iconBtn',
       type: 'iconBtn',
       onClick: () => {
       onClick: () => {
@@ -319,8 +389,6 @@ const Partitions: FC<{
         rows={partitionList}
         rows={partitionList}
         rowCount={total}
         rowCount={total}
         primaryKey="id"
         primaryKey="id"
-        openCheckBox={true}
-        showHoverStyle={true}
         selected={selectedPartitions}
         selected={selectedPartitions}
         setSelected={handleSelectChange}
         setSelected={handleSelectChange}
         page={currentPage}
         page={currentPage}

+ 31 - 15
client/src/pages/schema/Create.tsx

@@ -2,16 +2,17 @@ import { useEffect, useMemo, useState } from 'react';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 import DialogTemplate from '../../components/customDialog/DialogTemplate';
 import DialogTemplate from '../../components/customDialog/DialogTemplate';
 import {
 import {
-  EmbeddingTypeEnum,
   INDEX_CONFIG,
   INDEX_CONFIG,
   INDEX_OPTIONS_MAP,
   INDEX_OPTIONS_MAP,
   MetricType,
   MetricType,
+  METRIC_TYPES_VALUES,
 } from '../../consts/Milvus';
 } from '../../consts/Milvus';
 import { useFormValidation } from '../../hooks/Form';
 import { useFormValidation } from '../../hooks/Form';
 import { formatForm, getMetricOptions } from '../../utils/Form';
 import { formatForm, getMetricOptions } from '../../utils/Form';
+import { getEmbeddingType } from '../../utils/search';
 import { DataType } from '../collections/Types';
 import { DataType } from '../collections/Types';
 import CreateForm from './CreateForm';
 import CreateForm from './CreateForm';
-import { IndexType, ParamPair } from './Types';
+import { IndexType, ParamPair, INDEX_TYPES_ENUM } from './Types';
 
 
 const CreateIndex = (props: {
 const CreateIndex = (props: {
   collectionName: string;
   collectionName: string;
@@ -25,14 +26,21 @@ const CreateIndex = (props: {
   const { t: dialogTrans } = useTranslation('dialog');
   const { t: dialogTrans } = useTranslation('dialog');
   const { t: btnTrans } = useTranslation('btn');
   const { t: btnTrans } = useTranslation('btn');
 
 
-  const defaultMetricType = fieldType === 'BinaryVector' ? 'Hamming' : 'L2';
+  const defaultIndexType =
+    fieldType === 'BinaryVector'
+      ? INDEX_TYPES_ENUM.BIN_IVF_FLAT
+      : INDEX_TYPES_ENUM.IVF_FLAT;
+  const defaultMetricType =
+    fieldType === 'BinaryVector'
+      ? METRIC_TYPES_VALUES.HAMMING
+      : METRIC_TYPES_VALUES.L2;
 
 
   const [indexSetting, setIndexSetting] = useState<{
   const [indexSetting, setIndexSetting] = useState<{
     index_type: IndexType;
     index_type: IndexType;
     metric_type: MetricType;
     metric_type: MetricType;
     [x: string]: string;
     [x: string]: string;
   }>({
   }>({
-    index_type: 'IVF_FLAT',
+    index_type: defaultIndexType,
     metric_type: defaultMetricType,
     metric_type: defaultMetricType,
     M: '',
     M: '',
     m: '4',
     m: '4',
@@ -57,11 +65,16 @@ const CreateIndex = (props: {
     [indexSetting.index_type, fieldType]
     [indexSetting.index_type, fieldType]
   );
   );
 
 
+  const indexParams = useMemo(() => {
+    const params: { [x: string]: string } = {};
+    indexCreateParams.forEach(v => {
+      params[v] = indexSetting[v];
+    });
+    return params;
+  }, [indexCreateParams, indexSetting]);
+
   const indexOptions = useMemo(() => {
   const indexOptions = useMemo(() => {
-    const type =
-      fieldType === 'BinaryVector'
-        ? EmbeddingTypeEnum.binary
-        : EmbeddingTypeEnum.float;
+    const type = getEmbeddingType(fieldType);
     return INDEX_OPTIONS_MAP[type];
     return INDEX_OPTIONS_MAP[type];
   }, [fieldType]);
   }, [fieldType]);
 
 
@@ -77,8 +90,10 @@ const CreateIndex = (props: {
   const { validation, checkIsValid, disabled, setDisabled, resetValidation } =
   const { validation, checkIsValid, disabled, setDisabled, resetValidation } =
     useFormValidation(checkedForm);
     useFormValidation(checkedForm);
 
 
+  // reset index params
   useEffect(() => {
   useEffect(() => {
-    setDisabled(true);
+    // no need
+    // setDisabled(true);
     setIndexSetting(v => ({
     setIndexSetting(v => ({
       ...v,
       ...v,
       metric_type: defaultMetricType,
       metric_type: defaultMetricType,
@@ -106,7 +121,8 @@ const CreateIndex = (props: {
       .forEach(item => {
       .forEach(item => {
         paramsForm[item] = '';
         paramsForm[item] = '';
       });
       });
-
+    // if no other params, the form should be valid.
+    setDisabled((INDEX_CONFIG[type].create || []).length === 0 ? false : true);
     const form = formatForm(paramsForm);
     const form = formatForm(paramsForm);
     resetValidation(form);
     resetValidation(form);
   };
   };
@@ -123,10 +139,10 @@ const CreateIndex = (props: {
         key: 'metric_type',
         key: 'metric_type',
         value: metric_type,
         value: metric_type,
       },
       },
-      ...indexCreateParams.map(p => ({
-        key: p,
-        value: indexSetting[p],
-      })),
+      {
+        key: 'params',
+        value: JSON.stringify(indexParams),
+      },
     ];
     ];
 
 
     handleCreate(params);
     handleCreate(params);
@@ -138,7 +154,7 @@ const CreateIndex = (props: {
         type: indexTrans('index'),
         type: indexTrans('index'),
         name: collectionName,
         name: collectionName,
       })}
       })}
-      handleCancel={handleCancel}
+      handleClose={handleCancel}
       confirmLabel={btnTrans('create')}
       confirmLabel={btnTrans('create')}
       handleConfirm={handleCreateIndex}
       handleConfirm={handleCreateIndex}
       confirmDisabled={disabled}
       confirmDisabled={disabled}

+ 3 - 3
client/src/pages/schema/CreateForm.tsx

@@ -159,7 +159,7 @@ const CreateForm = (
           indexTypeChange && indexTypeChange(type as string);
           indexTypeChange && indexTypeChange(type as string);
         }}
         }}
         variant="filled"
         variant="filled"
-        classes={{ root: classes.select }}
+        wrapperClass={classes.select}
       />
       />
 
 
       <Typography className={classes.paramTitle}>
       <Typography className={classes.paramTitle}>
@@ -174,7 +174,7 @@ const CreateForm = (
           updateForm('metric_type', type as string);
           updateForm('metric_type', type as string);
         }}
         }}
         variant="filled"
         variant="filled"
-        classes={{ root: classes.select }}
+        wrapperClass={classes.select}
       />
       />
 
 
       {indexParams.includes('m') && (
       {indexParams.includes('m') && (
@@ -186,7 +186,7 @@ const CreateForm = (
             updateForm('m', e.target.value as string)
             updateForm('m', e.target.value as string)
           }
           }
           variant="filled"
           variant="filled"
-          classes={{ root: classes.select }}
+          wrapperClass={classes.select}
         />
         />
       )}
       )}
 
 

+ 4 - 1
client/src/pages/schema/Schema.tsx

@@ -155,6 +155,7 @@ const Schema: FC<{
       align: 'left',
       align: 'left',
       disablePadding: true,
       disablePadding: true,
       label: collectionTrans('fieldName'),
       label: collectionTrans('fieldName'),
+      sortBy: '_fieldName',
     },
     },
     {
     {
       id: '_fieldType',
       id: '_fieldType',
@@ -180,12 +181,14 @@ const Schema: FC<{
       align: 'left',
       align: 'left',
       disablePadding: true,
       disablePadding: true,
       label: indexTrans('type'),
       label: indexTrans('type'),
+      sortBy: '_indexType',
     },
     },
     {
     {
       id: '_indexParamElement',
       id: '_indexParamElement',
       align: 'left',
       align: 'left',
       disablePadding: false,
       disablePadding: false,
       label: indexTrans('param'),
       label: indexTrans('param'),
+      notSort: true,
     },
     },
     {
     {
       id: '_desc',
       id: '_desc',
@@ -207,13 +210,13 @@ const Schema: FC<{
         rows={schemaList}
         rows={schemaList}
         rowCount={total}
         rowCount={total}
         primaryKey="_fieldId"
         primaryKey="_fieldId"
-        openCheckBox={false}
         showHoverStyle={false}
         showHoverStyle={false}
         page={currentPage}
         page={currentPage}
         onChangePage={handlePageChange}
         onChangePage={handlePageChange}
         rowsPerPage={pageSize}
         rowsPerPage={pageSize}
         setRowsPerPage={handlePageSize}
         setRowsPerPage={handlePageSize}
         isLoading={loading}
         isLoading={loading}
+        openCheckBox={false}
       />
       />
     </section>
     </section>
   );
   );

+ 21 - 6
client/src/pages/schema/Types.ts

@@ -1,5 +1,5 @@
 import { ReactElement } from 'react';
 import { ReactElement } from 'react';
-import { ManageRequestMethods } from '../../types/Common';
+import { MetricType } from '../../consts/Milvus';
 import { DataType } from '../collections/Types';
 import { DataType } from '../collections/Types';
 
 
 export enum INDEX_TYPES_ENUM {
 export enum INDEX_TYPES_ENUM {
@@ -11,6 +11,17 @@ export enum INDEX_TYPES_ENUM {
   HNSW = 'HNSW',
   HNSW = 'HNSW',
   ANNOY = 'ANNOY',
   ANNOY = 'ANNOY',
   RNSG = 'RNSG',
   RNSG = 'RNSG',
+  BIN_IVF_FLAT = 'BIN_IVF_FLAT',
+  BIN_FLAT = 'BIN_FLAT',
+}
+
+export interface Field {
+  data_type: DataType;
+  fieldID: string;
+  type_params: { key: string; value: string }[];
+  is_primary_key: true;
+  name: string;
+  description: string;
 }
 }
 
 
 export interface FieldData {
 export interface FieldData {
@@ -37,17 +48,21 @@ export interface IndexView {
   _indexTypeElement?: ReactElement;
   _indexTypeElement?: ReactElement;
   _indexParameterPairs: { key: string; value: string }[];
   _indexParameterPairs: { key: string; value: string }[];
   _indexParamElement?: ReactElement;
   _indexParamElement?: ReactElement;
+  _metricType?: MetricType | string;
 }
 }
 
 
 export type IndexType =
 export type IndexType =
-  | 'FLAT'
-  | 'IVF_FLAT'
+  | INDEX_TYPES_ENUM.FLAT
+  | INDEX_TYPES_ENUM.IVF_FLAT
   // | 'IVF_SQ8'
   // | 'IVF_SQ8'
   // | 'IVF_SQ8_HYBRID'
   // | 'IVF_SQ8_HYBRID'
-  | 'IVF_PQ'
+  | INDEX_TYPES_ENUM.IVF_PQ
   // | 'RNSG'
   // | 'RNSG'
-  | 'HNSW'
-  | 'ANNOY';
+  | INDEX_TYPES_ENUM.HNSW
+  | INDEX_TYPES_ENUM.ANNOY
+  | INDEX_TYPES_ENUM.BIN_IVF_FLAT
+  | INDEX_TYPES_ENUM.BIN_FLAT
+
 
 
 export interface IndexManageParam {
 export interface IndexManageParam {
   collection_name: string;
   collection_name: string;

+ 1 - 0
client/src/pages/seach/Constants.ts

@@ -0,0 +1 @@
+export const TOP_K_OPTIONS = [50, 100, 150, 200, 250];

+ 259 - 0
client/src/pages/seach/SearchParams.tsx

@@ -0,0 +1,259 @@
+import { makeStyles, Theme } from '@material-ui/core';
+import { FC, useCallback, useEffect, useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+import CustomInput from '../../components/customInput/CustomInput';
+import { ITextfieldConfig } from '../../components/customInput/Types';
+import CustomSelector from '../../components/customSelector/CustomSelector';
+import { Option } from '../../components/customSelector/Types';
+import {
+  DEFAULT_SEARCH_PARAM_VALUE_MAP,
+  INDEX_CONFIG,
+  METRIC_OPTIONS_MAP,
+  searchKeywordsType,
+} from '../../consts/Milvus';
+import { useFormValidation } from '../../hooks/Form';
+import { formatForm } from '../../utils/Form';
+import { SearchParamInputConfig, SearchParamsProps } from './Types';
+
+const getStyles = makeStyles((theme: Theme) => ({
+  selector: {
+    width: '100%',
+    marginTop: theme.spacing(2),
+  },
+  input: {
+    marginTop: theme.spacing(2),
+  },
+}));
+
+const SearchParams: FC<SearchParamsProps> = ({
+  indexType,
+  indexParams,
+  searchParamsForm,
+  handleFormChange,
+  embeddingType,
+  metricType,
+  topK,
+  setParamsDisabled,
+  wrapperClass = '',
+}) => {
+  const { t: indexTrans } = useTranslation('index');
+  const { t: warningTrans } = useTranslation('warning');
+  const classes = getStyles();
+
+  const metricOptions: Option[] = METRIC_OPTIONS_MAP[embeddingType];
+
+  // search params key list, depends on index type
+  // e.g. ['nprobe']
+  const searchParams = useMemo(
+    () => (indexType !== '' ? INDEX_CONFIG[indexType].search : []),
+    [indexType]
+  );
+
+  const handleInputChange = useCallback(
+    (key: string, value: number) => {
+      const form = { ...searchParamsForm, [key]: value };
+      handleFormChange(form);
+    },
+    [handleFormChange, searchParamsForm]
+  );
+
+  /**
+   * function to transfer search params to CustomInput need config type
+   */
+  const getNumberInputConfig = useCallback(
+    (params: SearchParamInputConfig): ITextfieldConfig => {
+      const {
+        label,
+        key,
+        min,
+        max,
+        value,
+        handleChange,
+        isInt = true,
+      } = params;
+
+      // search_k range is special compared to others,need to be handled separately
+      // range: {-1} ∪ [top_k, n × n_trees]
+      const isSearchK = label === 'search_k';
+
+      const config: ITextfieldConfig = {
+        label,
+        key,
+        onChange: value => {
+          handleChange(value);
+        },
+        className: classes.input,
+        variant: 'filled',
+        type: 'number',
+        value,
+        validations: [
+          {
+            rule: 'require',
+            errorText: warningTrans('required', { name: label }),
+          },
+        ],
+      };
+      if (!isSearchK && min && max) {
+        config.validations?.push({
+          rule: 'range',
+          errorText: warningTrans('range', { min, max }),
+          extraParam: {
+            min,
+            max,
+            type: 'number',
+          },
+        });
+      }
+
+      if (isInt) {
+        config.validations?.push({
+          rule: 'integer',
+          errorText: warningTrans('integer', { name: label }),
+        });
+      }
+
+      // search_k
+      if (isSearchK) {
+        config.validations?.push({
+          rule: 'specValueOrRange',
+          errorText: warningTrans('specValueOrRange', {
+            name: label,
+            min,
+            max,
+            specValue: -1,
+          }),
+          extraParam: {
+            min,
+            max,
+            compareValue: -1,
+            type: 'number',
+          },
+        });
+      }
+      return config;
+    },
+    [warningTrans, classes.input]
+  );
+
+  const getSearchInputConfig = useCallback(
+    (paramKey: searchKeywordsType): ITextfieldConfig => {
+      const nlist = Number(
+        indexParams.find(p => p.key === 'nlist')?.value || 0
+      );
+
+      const configParamMap: {
+        [key in searchKeywordsType]: SearchParamInputConfig;
+      } = {
+        nprobe: {
+          label: 'nprobe',
+          key: 'nprobe',
+          value: searchParamsForm['nprobe'] || '',
+          min: 1,
+          max: nlist,
+          isInt: true,
+          handleChange: value => {
+            handleInputChange('nprobe', value);
+          },
+        },
+        ef: {
+          label: 'ef',
+          key: 'ef',
+          value: searchParamsForm['ef'] || '',
+          min: topK,
+          max: 32768,
+          isInt: true,
+          handleChange: value => {
+            handleInputChange('ef', value);
+          },
+        },
+        search_k: {
+          label: 'search_k',
+          key: 'search_k',
+          value: searchParamsForm['search_k'] || '',
+          min: topK,
+          // n * n_trees can be infinity
+          max: Infinity,
+          isInt: true,
+          handleChange: value => {
+            handleInputChange('search_k', value);
+          },
+        },
+        search_length: {
+          label: 'search_length',
+          key: 'search_length',
+          value: searchParamsForm['search_length'] || '',
+          min: 10,
+          max: 300,
+          isInt: true,
+          handleChange: value => {
+            handleInputChange('search_length', value);
+          },
+        },
+      };
+
+      const param = configParamMap[paramKey];
+      return getNumberInputConfig(param);
+    },
+    [
+      indexParams,
+      topK,
+      searchParamsForm,
+      getNumberInputConfig,
+      handleInputChange,
+    ]
+  );
+
+  useEffect(() => {
+    // generate different form according to search params
+    const form = searchParams.reduce(
+      (paramsForm, param) => ({
+        ...paramsForm,
+        [param]: DEFAULT_SEARCH_PARAM_VALUE_MAP[param],
+      }),
+      {}
+    );
+    handleFormChange(form);
+  }, [searchParams, handleFormChange]);
+
+  const checkedForm = useMemo(() => {
+    const { ...needCheckItems } = searchParamsForm;
+    return formatForm(needCheckItems);
+  }, [searchParamsForm]);
+
+  const { validation, checkIsValid, disabled } = useFormValidation(checkedForm);
+
+  useEffect(() => {
+    setParamsDisabled(disabled);
+  }, [disabled, setParamsDisabled]);
+
+  return (
+    <div className={wrapperClass}>
+      {/* metric type */}
+      <CustomSelector
+        options={metricOptions}
+        value={metricType}
+        label={indexTrans('metric')}
+        wrapperClass={classes.selector}
+        variant="filled"
+        onChange={(e: { target: { value: unknown } }) => {
+          // not selectable now, so not set onChange event
+        }}
+        // not selectable now
+        readOnly={true}
+      />
+
+      {/* dynamic params, now every type only has one param except metric type */}
+      {searchParams.map(param => (
+        <CustomInput
+          key={param}
+          type="text"
+          textConfig={getSearchInputConfig(param)}
+          checkValid={checkIsValid}
+          validInfo={validation}
+        />
+      ))}
+    </div>
+  );
+};
+
+export default SearchParams;

+ 119 - 0
client/src/pages/seach/Styles.ts

@@ -0,0 +1,119 @@
+import { makeStyles, Theme } from '@material-ui/core';
+
+export const getVectorSearchStyles = makeStyles((theme: Theme) => ({
+  form: {
+    display: 'flex',
+    justifyContent: 'space-between',
+
+    '& .field': {
+      display: 'flex',
+      flexDirection: 'column',
+      flexBasis: '33%',
+
+      padding: theme.spacing(2, 3, 3),
+      backgroundColor: '#fff',
+      borderRadius: theme.spacing(0.5),
+      boxShadow: '3px 3px 10px rgba(0, 0, 0, 0.05)',
+
+      '& .textarea': {
+        border: `1px solid ${theme.palette.milvusGrey.main}`,
+        borderRadius: theme.spacing(0.5),
+        padding: theme.spacing(1),
+        paddingBottom: '18px',
+        marginTop: theme.spacing(2),
+      },
+
+      // reset default style
+      '& .textfield': {
+        padding: 0,
+        fontSize: '14px',
+        lineHeight: '20px',
+        fontWeight: 400,
+
+        '&::before': {
+          borderBottom: 'none',
+        },
+
+        '&::after': {
+          borderBottom: 'none',
+        },
+      },
+
+      '& .multiline': {
+        '& textarea': {
+          overflow: 'auto',
+          // change scrollbar style
+          '&::-webkit-scrollbar': {
+            width: '8px',
+          },
+
+          '&::-webkit-scrollbar-track': {
+            backgroundColor: '#f9f9f9',
+          },
+
+          '&::-webkit-scrollbar-thumb': {
+            borderRadius: '8px',
+            backgroundColor: '#eee',
+          },
+        },
+      },
+    },
+
+    '& .field-second': {
+      flexGrow: 1,
+      margin: theme.spacing(0, 1),
+    },
+
+    '& .text': {
+      color: theme.palette.milvusGrey.dark,
+      fontWeight: 500,
+    },
+  },
+  selector: {
+    width: '100%',
+    marginTop: theme.spacing(2),
+  },
+  paramsWrapper: {
+    display: 'flex',
+    flexDirection: 'column',
+  },
+  toolbar: {
+    display: 'flex',
+    justifyContent: 'space-between',
+    alignItems: 'center',
+
+    padding: theme.spacing(2, 0),
+
+    '& .left': {
+      display: 'flex',
+      alignItems: 'center',
+
+      '& .text': {
+        color: theme.palette.milvusGrey.main,
+      },
+    },
+    '& .right': {
+      '& .btn': {
+        marginRight: theme.spacing(1),
+      },
+      '& .icon': {
+        fontSize: '16px',
+      },
+    },
+  },
+  menuLabel: {
+    minWidth: '108px',
+
+    padding: theme.spacing(0, 1),
+    margin: theme.spacing(0, 1),
+
+    backgroundColor: '#fff',
+    color: theme.palette.milvusGrey.dark,
+  },
+  menuItem: {
+    fontWeight: 500,
+    fontSize: '12px',
+    lineHeight: '16px',
+    color: theme.palette.milvusGrey.dark,
+  },
+}));

+ 62 - 0
client/src/pages/seach/Types.ts

@@ -0,0 +1,62 @@
+import { Option } from '../../components/customSelector/Types';
+import { searchKeywordsType } from '../../consts/Milvus';
+import { DataType, DataTypeEnum } from '../collections/Types';
+import { IndexView } from '../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: DataType;
+  // 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;
+}
+
+export interface SearchParamInputConfig {
+  label: string;
+  key: searchKeywordsType;
+  min: number;
+  max: number;
+  isInt?: boolean;
+  // no value: empty string
+  value: number | string;
+  handleChange: (value: number) => void;
+}
+
+export interface VectorSearchParam {
+  expr?: string;
+  search_params: { key: string; value: string | number }[];
+  vectors: any;
+  output_fields: string[];
+  vector_type: number | DataTypeEnum;
+}
+
+export interface SearchResult {
+  // dynamic field names
+  [key: string]: string | number;
+  score: number;
+}

+ 422 - 0
client/src/pages/seach/VectorSearch.tsx

@@ -0,0 +1,422 @@
+import { TextField, Typography } from '@material-ui/core';
+import { useTranslation } from 'react-i18next';
+import { useNavigationHook } from '../../hooks/Navigation';
+import { ALL_ROUTER_TYPES } from '../../router/Types';
+import CustomSelector from '../../components/customSelector/CustomSelector';
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import SearchParams from './SearchParams';
+import { DEFAULT_METRIC_VALUE_MAP } from '../../consts/Milvus';
+import { FieldOption, SearchResultView, VectorSearchParam } from './Types';
+import MilvusGrid from '../../components/grid/Grid';
+import EmptyCard from '../../components/cards/EmptyCard';
+import icons from '../../components/icons/Icons';
+import { usePaginationHook } from '../../hooks/Pagination';
+import CustomButton from '../../components/customButton/CustomButton';
+import SimpleMenu from '../../components/menu/SimpleMenu';
+import { TOP_K_OPTIONS } from './Constants';
+import { Option } from '../../components/customSelector/Types';
+import { CollectionHttp } from '../../http/Collection';
+import { CollectionData, DataTypeEnum } from '../collections/Types';
+import { IndexHttp } from '../../http/Index';
+import { getVectorSearchStyles } from './Styles';
+import { parseValue } from '../../utils/Insert';
+import {
+  classifyFields,
+  getDefaultIndexType,
+  getEmbeddingType,
+  getNonVectorFieldsForFilter,
+  getVectorFieldOptions,
+  transferSearchResult,
+} from '../../utils/search';
+import { ColDefinitionsType } from '../../components/grid/Types';
+import Filter from '../../components/advancedSearch';
+import { Field } from '../../components/advancedSearch/Types';
+
+const VectorSearch = () => {
+  useNavigationHook(ALL_ROUTER_TYPES.SEARCH);
+  const { t: searchTrans } = useTranslation('search');
+  const { t: btnTrans } = useTranslation('btn');
+  const classes = getVectorSearchStyles();
+
+  // data stored inside the component
+  const [tableLoading, setTableLoading] = useState<boolean>(false);
+  const [collections, setCollections] = useState<CollectionData[]>([]);
+  const [selectedCollection, setSelectedCollection] = useState<string>('');
+  const [fieldOptions, setFieldOptions] = useState<FieldOption[]>([]);
+  // fields for advanced filter
+  const [filterFields, setFilterFields] = useState<Field[]>([]);
+  const [selectedField, setSelectedField] = useState<string>('');
+  // search params form
+  const [searchParam, setSearchParam] = useState<{ [key in string]: number }>(
+    {}
+  );
+  // search params disable state
+  const [paramDisabled, setParamDisabled] = useState<boolean>(true);
+  // use null as init value before search, empty array means no results
+  const [searchResult, setSearchResult] = useState<SearchResultView[] | null>(
+    null
+  );
+  // default topK is 100
+  const [topK, setTopK] = useState<number>(100);
+  const [expression, setExpression] = useState<string>('');
+  const [vectors, setVectors] = useState<string>('');
+
+  const {
+    pageSize,
+    handlePageSize,
+    currentPage,
+    handleCurrentPage,
+    total,
+    data: result,
+  } = usePaginationHook(searchResult || []);
+
+  const searchDisabled = useMemo(() => {
+    /**
+     * before search, user must:
+     * 1. enter vector value
+     * 2. choose collection and field
+     * 3. set extra search params
+     */
+    const isInvalid =
+      vectors === '' ||
+      selectedCollection === '' ||
+      selectedField === '' ||
+      paramDisabled;
+    return isInvalid;
+  }, [paramDisabled, selectedField, selectedCollection, vectors]);
+
+  const collectionOptions: Option[] = useMemo(
+    () =>
+      collections.map(c => ({
+        label: c._name,
+        value: c._name,
+      })),
+    [collections]
+  );
+
+  const outputFields: string[] = useMemo(() => {
+    const fields =
+      collections.find(c => c._name === selectedCollection)?._fields || [];
+    // vector field can't be output fields
+    const invalidTypes = ['BinaryVector', 'FloatVector'];
+    const nonVectorFields = fields.filter(
+      field => !invalidTypes.includes(field._fieldType)
+    );
+    return nonVectorFields.map(f => f._fieldName);
+  }, [selectedCollection, collections]);
+
+  const colDefinitions: ColDefinitionsType[] = useMemo(() => {
+    // filter id and score
+    return searchResult && searchResult.length > 0
+      ? Object.keys(searchResult[0])
+          .filter(item => item !== 'id' && item !== 'score')
+          .map(key => ({
+            id: key,
+            align: 'left',
+            disablePadding: false,
+            label: key,
+          }))
+      : [];
+  }, [searchResult]);
+
+  const { metricType, indexType, indexParams, fieldType, embeddingType } =
+    useMemo(() => {
+      if (selectedField !== '') {
+        // field options must contain selected field, so selectedFieldInfo will never undefined
+        const selectedFieldInfo = fieldOptions.find(
+          f => f.value === selectedField
+        );
+        const index = selectedFieldInfo?.indexInfo;
+        const embeddingType = getEmbeddingType(selectedFieldInfo!.fieldType);
+        const metric =
+          index?._metricType || DEFAULT_METRIC_VALUE_MAP[embeddingType];
+        const indexParams = index?._indexParameterPairs || [];
+
+        return {
+          metricType: metric,
+          indexType: index?._indexType || getDefaultIndexType(embeddingType),
+          indexParams,
+          fieldType: DataTypeEnum[selectedFieldInfo?.fieldType!],
+          embeddingType,
+        };
+      }
+
+      return {
+        metricType: '',
+        indexType: '',
+        indexParams: [],
+        fieldType: 0,
+        embeddingType: DataTypeEnum.FloatVector,
+      };
+    }, [selectedField, fieldOptions]);
+
+  // fetch data
+  const fetchCollections = useCallback(async () => {
+    const collections = await CollectionHttp.getCollections();
+    setCollections(collections);
+  }, []);
+
+  const fetchFieldsWithIndex = useCallback(
+    async (collectionName: string, collections: CollectionData[]) => {
+      const fields =
+        collections.find(c => c._name === collectionName)?._fields || [];
+      const indexes = await IndexHttp.getIndexInfo(collectionName);
+
+      const { vectorFields, nonVectorFields } = classifyFields(fields);
+
+      // only vector type fields can be select
+      const fieldOptions = getVectorFieldOptions(vectorFields, indexes);
+      setFieldOptions(fieldOptions);
+      // only non vector type fields can be advanced filter
+      const filterFields = getNonVectorFieldsForFilter(nonVectorFields);
+      setFilterFields(filterFields);
+    },
+    []
+  );
+
+  useEffect(() => {
+    fetchCollections();
+  }, [fetchCollections]);
+
+  // get field options with index when selected collection changed
+  useEffect(() => {
+    if (selectedCollection !== '') {
+      fetchFieldsWithIndex(selectedCollection, collections);
+    }
+  }, [selectedCollection, collections, fetchFieldsWithIndex]);
+
+  // icons
+  const VectorSearchIcon = icons.vectorSearch;
+  const ResetIcon = icons.refresh;
+  const ArrowIcon = icons.dropdown;
+
+  // methods
+  const handlePageChange = (e: any, page: number) => {
+    handleCurrentPage(page);
+  };
+  const handleReset = () => {
+    /**
+     * reset search includes:
+     * 1. reset vectors
+     * 2. reset selected collection and field
+     * 3. reset search params
+     * 4. reset advanced filter expression
+     * 5. clear search result
+     */
+    setVectors('');
+    setSelectedField('');
+    setSelectedCollection('');
+    setSearchResult(null);
+    setFilterFields([]);
+    setExpression('');
+  };
+  const handleSearch = async (topK: number, expr = expression) => {
+    const searhParamPairs = [
+      // dynamic search params
+      {
+        key: 'params',
+        value: JSON.stringify(searchParam),
+      },
+      {
+        key: 'anns_field',
+        value: selectedField,
+      },
+      {
+        key: 'topk',
+        value: topK,
+      },
+      {
+        key: 'metric_type',
+        value: metricType,
+      },
+    ];
+
+    const params: VectorSearchParam = {
+      output_fields: outputFields,
+      expr,
+      search_params: searhParamPairs,
+      vectors: [parseValue(vectors)],
+      vector_type: fieldType,
+    };
+
+    setTableLoading(true);
+    try {
+      const res = await CollectionHttp.vectorSearchData(
+        selectedCollection,
+        params
+      );
+      setTableLoading(false);
+
+      const result = transferSearchResult(res.results);
+      setSearchResult(result);
+    } catch (err) {
+      setTableLoading(false);
+    }
+  };
+  const handleAdvancedFilterChange = (expression: string) => {
+    setExpression(expression);
+    if (!searchDisabled) {
+      handleSearch(topK, expression);
+    }
+  };
+
+  const handleVectorChange = (value: string) => {
+    setVectors(value);
+  };
+
+  return (
+    <section className="page-wrapper">
+      {/* form section */}
+      <form className={classes.form}>
+        {/* vector value textarea */}
+        <fieldset className="field">
+          <Typography className="text">{searchTrans('firstTip')}</Typography>
+          <TextField
+            className="textarea"
+            InputProps={{
+              classes: {
+                root: 'textfield',
+                multiline: 'multiline',
+              },
+            }}
+            multiline
+            rows={5}
+            placeholder={searchTrans('vectorPlaceholder')}
+            value={vectors}
+            onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
+              handleVectorChange(e.target.value as string);
+            }}
+          />
+        </fieldset>
+        {/* collection and field selectors */}
+        <fieldset className="field field-second">
+          <Typography className="text">{searchTrans('secondTip')}</Typography>
+          <CustomSelector
+            options={collectionOptions}
+            wrapperClass={classes.selector}
+            variant="filled"
+            label={searchTrans('collection')}
+            value={selectedCollection}
+            onChange={(e: { target: { value: unknown } }) => {
+              const collection = e.target.value;
+              setSelectedCollection(collection as string);
+              // every time selected collection changed, reset field
+              setSelectedField('');
+            }}
+          />
+          <CustomSelector
+            options={fieldOptions}
+            readOnly={selectedCollection === ''}
+            wrapperClass={classes.selector}
+            variant="filled"
+            label={searchTrans('field')}
+            value={selectedField}
+            onChange={(e: { target: { value: unknown } }) => {
+              const field = e.target.value;
+              setSelectedField(field as string);
+            }}
+          />
+        </fieldset>
+        {/* search params selectors */}
+        <fieldset className="field">
+          <Typography className="text">{searchTrans('thirdTip')}</Typography>
+          <SearchParams
+            wrapperClass={classes.paramsWrapper}
+            metricType={metricType!}
+            embeddingType={
+              embeddingType as
+                | DataTypeEnum.BinaryVector
+                | DataTypeEnum.FloatVector
+            }
+            indexType={indexType}
+            indexParams={indexParams!}
+            searchParamsForm={searchParam}
+            handleFormChange={setSearchParam}
+            topK={topK}
+            setParamsDisabled={setParamDisabled}
+          />
+        </fieldset>
+      </form>
+
+      {/**
+       * search toolbar section
+       * including topK selector, advanced filter, search and reset btn
+       */}
+      <section className={classes.toolbar}>
+        <div className="left">
+          <Typography variant="h5" className="text">
+            {`${searchTrans('result')}: `}
+          </Typography>
+          {/* topK selector */}
+          <SimpleMenu
+            label={searchTrans('topK', { number: topK })}
+            menuItems={TOP_K_OPTIONS.map(item => ({
+              label: item.toString(),
+              callback: () => {
+                setTopK(item);
+                if (!searchDisabled) {
+                  handleSearch(item);
+                }
+              },
+              wrapperClass: classes.menuItem,
+            }))}
+            buttonProps={{
+              className: classes.menuLabel,
+              endIcon: <ArrowIcon />,
+            }}
+            menuItemWidth="108px"
+          />
+
+          <Filter
+            title="Advanced Filter"
+            fields={filterFields}
+            filterDisabled={selectedField === '' || selectedCollection === ''}
+            onSubmit={handleAdvancedFilterChange}
+          />
+        </div>
+        <div className="right">
+          <CustomButton className="btn" onClick={handleReset}>
+            <ResetIcon classes={{ root: 'icon' }} />
+            {btnTrans('reset')}
+          </CustomButton>
+          <CustomButton
+            variant="contained"
+            disabled={searchDisabled}
+            onClick={() => handleSearch(topK)}
+          >
+            {btnTrans('search')}
+          </CustomButton>
+        </div>
+      </section>
+
+      {/* search result table section */}
+      {(searchResult && searchResult.length > 0) || tableLoading ? (
+        <MilvusGrid
+          toolbarConfigs={[]}
+          colDefinitions={colDefinitions}
+          rows={result}
+          rowCount={total}
+          primaryKey="rank"
+          page={currentPage}
+          onChangePage={handlePageChange}
+          rowsPerPage={pageSize}
+          setRowsPerPage={handlePageSize}
+          openCheckBox={false}
+          isLoading={tableLoading}
+        />
+      ) : (
+        <EmptyCard
+          wrapperClass={`page-empty-card`}
+          icon={<VectorSearchIcon />}
+          text={
+            searchResult !== null
+              ? searchTrans('empty')
+              : searchTrans('startTip')
+          }
+        />
+      )}
+    </section>
+  );
+};
+
+export default VectorSearch;

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

@@ -2,6 +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 { RouterConfigType } from './Types';
 import { RouterConfigType } from './Types';
 
 
 const RouterConfig: RouterConfigType[] = [
 const RouterConfig: RouterConfigType[] = [
@@ -25,6 +26,11 @@ const RouterConfig: RouterConfigType[] = [
     component: Collection,
     component: Collection,
     auth: true,
     auth: true,
   },
   },
+  {
+    path: '/search',
+    component: VectorSearch,
+    auth: true,
+  },
 ];
 ];
 
 
 export default RouterConfig;
 export default RouterConfig;

+ 2 - 2
client/src/router/Types.ts

@@ -5,8 +5,8 @@ export enum ALL_ROUTER_TYPES {
   COLLECTIONS = 'collections',
   COLLECTIONS = 'collections',
   // '/collections/:collectionId'
   // '/collections/:collectionId'
   COLLECTION_DETAIL = 'collection_detail',
   COLLECTION_DETAIL = 'collection_detail',
-  // '/console'
-  CONSOLE = 'console',
+  // 'search'
+  SEARCH = 'search',
 }
 }
 
 
 export type NavInfo = {
 export type NavInfo = {

+ 5 - 0
client/src/styles/common.css

@@ -1,3 +1,8 @@
+/**
+ * we usually use Material makeStyles to write component and page style
+ * this file is used for some global scope reusable style
+ */
+
 /* reset some elements styles */
 /* reset some elements styles */
 fieldset {
 fieldset {
   border: 0;
   border: 0;

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

@@ -158,5 +158,14 @@ export const theme = createMuiTheme({
         marginLeft: 0,
         marginLeft: 0,
       },
       },
     },
     },
+    MuiFilledInput: {
+      root: {
+        backgroundColor: '#f9f9f9',
+
+        '&:hover': {
+          backgroundColor: '#f9f9f9',
+        },
+      },
+    },
   },
   },
 });
 });

+ 28 - 0
client/src/utils/Common.ts

@@ -33,3 +33,31 @@ export const formatNumber = (number: number): string => {
 export const throwErrorForDev = (text: string) => {
 export const throwErrorForDev = (text: string) => {
   throw new Error(text);
   throw new Error(text);
 };
 };
+
+/**
+ *
+ * @param obj key value pair Array
+ * @param key the target you want to find.
+ * @returns undefined | string
+ */
+export const findKeyValue = (
+  obj: { key: string; value: string }[],
+  key: string
+) => obj.find(v => v.key === key)?.value;
+
+export const generateHashCode = (source: string) => {
+  let hash = 0,
+    i,
+    chr;
+  if (source.length === 0) return hash;
+  for (i = 0; i < source.length; i++) {
+    chr = source.charCodeAt(i);
+    hash = (hash << 5) - hash + chr;
+    hash |= 0; // Convert to 32bit integer
+  }
+  return hash.toString();
+};
+
+export const generateIdByHash = (salt?: string) => {
+  return generateHashCode(`${new Date().getTime().toString()}-${salt}`);
+};

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

@@ -38,15 +38,15 @@ export const getMetricOptions = (
   const baseBinaryOptions = [
   const baseBinaryOptions = [
     {
     {
       value: METRIC_TYPES_VALUES.HAMMING,
       value: METRIC_TYPES_VALUES.HAMMING,
-      label: 'Hamming',
+      label: 'HAMMING',
     },
     },
     {
     {
       value: METRIC_TYPES_VALUES.JACCARD,
       value: METRIC_TYPES_VALUES.JACCARD,
-      label: 'Jaccard',
+      label: 'JACCARD',
     },
     },
     {
     {
       value: METRIC_TYPES_VALUES.TANIMOTO,
       value: METRIC_TYPES_VALUES.TANIMOTO,
-      label: 'Tanimoto',
+      label: 'TANIMOTO',
     },
     },
   ];
   ];
 
 
@@ -54,18 +54,18 @@ export const getMetricOptions = (
 
 
   const baseOptionsMap: { [key: string]: any } = {
   const baseOptionsMap: { [key: string]: any } = {
     BinaryVector: {
     BinaryVector: {
-      FLAT: [
+      BIN_FLAT: [
         ...baseBinaryOptions,
         ...baseBinaryOptions,
         {
         {
           value: METRIC_TYPES_VALUES.SUBSTRUCTURE,
           value: METRIC_TYPES_VALUES.SUBSTRUCTURE,
-          label: 'Substructure',
+          label: 'SUBSTRUCTURE',
         },
         },
         {
         {
           value: METRIC_TYPES_VALUES.SUPERSTRUCTURE,
           value: METRIC_TYPES_VALUES.SUPERSTRUCTURE,
-          label: 'Superstructure',
+          label: 'SUPERSTRUCTURE',
         },
         },
       ],
       ],
-      IVF_FLAT: baseBinaryOptions,
+      BIN_IVF_FLAT: baseBinaryOptions,
     },
     },
     FloatVector: {
     FloatVector: {
       ALL: baseFloatOptions,
       ALL: baseFloatOptions,

+ 24 - 10
client/src/utils/Format.ts

@@ -74,18 +74,29 @@ export const getEnumKeyByValue = (enumObj: any, enumValue: any) => {
   return '--';
   return '--';
 };
 };
 
 
-export const getKeyValueListFromJSON = (
-  paramJSON: string
+/**
+ *
+ * @param obj e.g. {name: 'test'}
+ * @returns key value pair, e.g. [{key: 'name', value: 'test'}]
+ */
+export const getKeyValuePairFromObj = (
+  obj: { [key in string]: any }
+): { key: string; value: any }[] => {
+  const pairs: { key: string; value: string }[] = Object.entries(obj).map(
+    ([key, value]) => ({
+      key,
+      value: value as string,
+    })
+  );
+  return pairs;
+};
+
+export const getKeyValueListFromJsonString = (
+  json: string
 ): { key: string; value: string }[] => {
 ): { key: string; value: string }[] => {
   try {
   try {
-    const obj = JSON.parse(paramJSON);
-
-    const pairs: { key: string; value: string }[] = Object.entries(obj).map(
-      ([key, value]) => ({
-        key,
-        value: value as string,
-      })
-    );
+    const obj = JSON.parse(json);
+    const pairs = getKeyValuePairFromObj(obj);
 
 
     return pairs;
     return pairs;
   } catch (err) {
   } catch (err) {
@@ -114,3 +125,6 @@ export const getCreateFieldType = (config: Field): CreateFieldType => {
 
 
   return 'number';
   return 'number';
 };
 };
+
+// Trim the address
+export const formatAddress = (address: string): string => address.trim();

+ 51 - 0
client/src/utils/Insert.ts

@@ -0,0 +1,51 @@
+import { generateId } from './Common';
+
+/**
+ * function to convert uploaded csv to MilvusGrid component accepted data type
+ * @param data uploaded csv data, e.g. [['name1', 12], ['name2', 14]]
+ * @returns key value pair object array, use index as key, e.g. [{0: 'name1', 1: 12}, {0: 'name2', 1: 14}]
+ */
+export const transferCsvArrayToTableData = (data: any[][]) => {
+  return data.reduce(
+    (result, arr) => [...result, { ...arr, id: generateId() }],
+    []
+  );
+};
+
+/**
+ * function to replace object key
+ * @param obj e.g. {0: 'name1', 1: 12, 2: 'red'}
+ * @param newKeys e.g. ['name', 'age', 'color']
+ * @returns e.g. {name: 'name1', age: 12, color: 'red'}
+ */
+const replaceKeysByIndex = (obj: any, newKeys: string[]) => {
+  const keyValues = Object.keys(obj).map(key => {
+    const newKey = newKeys[Number(key)] || key;
+    return { [newKey]: parseValue(obj[key]) };
+  });
+  return Object.assign({}, ...keyValues);
+};
+
+export const parseValue = (value: string) => {
+  try {
+    return JSON.parse(value);
+  } catch (err) {
+    return value;
+  }
+};
+
+/**
+ *
+ * @param heads table heads, e.g. ['field1', 'field2', 'field3']
+ * @param data table data, e.g. [[23, [2,3,34,4,5,56], [1,1,1,1,1,1,1,1,1,1,1]]]
+ * @returns key value pair object array, with user selected heads or csv heads
+ */
+export const combineHeadsAndData = (heads: string[], data: any[]) => {
+  // use index as key, flatten two-dimensional array
+  // filter useless row
+  const flatTableData = data
+    .filter(d => d.some((item: string) => item !== ''))
+    .reduce((result, arr) => [...result, { ...arr }], []);
+  // replace flatTableData key with real head rely on index
+  return flatTableData.map((d: any) => replaceKeysByIndex(d, heads));
+};

+ 29 - 5
client/src/utils/Validation.ts

@@ -14,15 +14,16 @@ export type ValidType =
   | 'dimension'
   | 'dimension'
   | 'multiple'
   | 'multiple'
   | 'partitionName'
   | 'partitionName'
-  | 'firstCharacter';
+  | 'firstCharacter'
+  | 'specValueOrRange';
 export interface ICheckMapParam {
 export interface ICheckMapParam {
   value: string;
   value: string;
   extraParam?: IExtraParam;
   extraParam?: IExtraParam;
   rule: ValidType;
   rule: ValidType;
 }
 }
 export interface IExtraParam {
 export interface IExtraParam {
-  // used for confirm type
-  compareValue?: string;
+  // used for confirm or any compare type
+  compareValue?: string | number;
   // used for length type
   // used for length type
   min?: number;
   min?: number;
   max?: number;
   max?: number;
@@ -64,13 +65,13 @@ export const checkPasswordStrength = (value: string): boolean => {
 };
 };
 
 
 export const checkRange = (param: {
 export const checkRange = (param: {
-  value: string;
+  value: string | number;
   min?: number;
   min?: number;
   max?: number;
   max?: number;
   type?: 'string' | 'number';
   type?: 'string' | 'number';
 }): boolean => {
 }): boolean => {
   const { value, min = 0, max = 0, type } = param;
   const { value, min = 0, max = 0, type } = param;
-  const length = type === 'number' ? Number(value) : value.length;
+  const length = type === 'number' ? Number(value) : (value as string).length;
 
 
   let result = true;
   let result = true;
   const conditionMap = {
   const conditionMap = {
@@ -181,6 +182,23 @@ export const checkDimension = (param: {
   return checkMultiple({ value, multipleNumber });
   return checkMultiple({ value, multipleNumber });
 };
 };
 
 
+/**
+ * function to check whether value(type: number) is equal to specified value or in valid range
+ * @param param specValue and params checkRange function needed
+ * @returns whether input is valid
+ */
+export const checkSpecValueOrRange = (param: {
+  value: number;
+  min: number;
+  max: number;
+  compareValue: number;
+}): boolean => {
+  const { value, min, max, compareValue } = param;
+  return (
+    value === compareValue || checkRange({ min, max, value, type: 'number' })
+  );
+};
+
 export const getCheckResult = (param: ICheckMapParam): boolean => {
 export const getCheckResult = (param: ICheckMapParam): boolean => {
   const { value, extraParam = {}, rule } = param;
   const { value, extraParam = {}, rule } = param;
   const numberValue = Number(value);
   const numberValue = Number(value);
@@ -215,6 +233,12 @@ export const getCheckResult = (param: ICheckMapParam): boolean => {
       value,
       value,
       invalidTypes: extraParam?.invalidTypes,
       invalidTypes: extraParam?.invalidTypes,
     }),
     }),
+    specValueOrRange: checkSpecValueOrRange({
+      value: Number(value),
+      min: extraParam?.min || 0,
+      max: extraParam?.max || 0,
+      compareValue: Number(extraParam.compareValue) || 0,
+    }),
   };
   };
 
 
   return checkMap[rule];
   return checkMap[rule];

+ 104 - 0
client/src/utils/search.ts

@@ -0,0 +1,104 @@
+import { Field } from '../components/advancedSearch/Types';
+import { DataType, DataTypeEnum } from '../pages/collections/Types';
+import {
+  FieldData,
+  IndexType,
+  IndexView,
+  INDEX_TYPES_ENUM,
+} from '../pages/schema/Types';
+import {
+  FieldOption,
+  SearchResult,
+  SearchResultView,
+} from '../pages/seach/Types';
+
+export const transferSearchResult = (
+  result: SearchResult[]
+): SearchResultView[] => {
+  const resultView = result
+    .sort((a, b) => a.score - b.score)
+    .map((r, index) => ({
+      rank: index + 1,
+      distance: r.score,
+      ...r,
+    }));
+
+  return resultView;
+};
+
+/**
+ * function to get EmbeddingType
+ * @param fieldType only vector type fields: 'BinaryVector' or 'FloatVector'
+ */
+export const getEmbeddingType = (
+  fieldType: DataType
+): DataTypeEnum.BinaryVector | DataTypeEnum.FloatVector => {
+  const type =
+    fieldType === 'BinaryVector'
+      ? DataTypeEnum.BinaryVector
+      : DataTypeEnum.FloatVector;
+  return type;
+};
+
+/**
+ * function to get default index type according to embedding type
+ * use FLAT as default float index type, BIN_FLAT as default binary index type
+ * @param embeddingType float or binary
+ * @returns index type
+ */
+export const getDefaultIndexType = (embeddingType: DataTypeEnum): IndexType => {
+  const defaultIndexType =
+    embeddingType === DataTypeEnum.FloatVector
+      ? INDEX_TYPES_ENUM.FLAT
+      : INDEX_TYPES_ENUM.BIN_FLAT;
+  return defaultIndexType;
+};
+
+/**
+ * funtion to divide fields into two categories: vector or nonVector
+ */
+export const classifyFields = (
+  fields: FieldData[]
+): { vectorFields: FieldData[]; nonVectorFields: FieldData[] } => {
+  const vectorTypes: DataType[] = ['BinaryVector', 'FloatVector'];
+  return fields.reduce(
+    (result, cur) => {
+      const changedFieldType = vectorTypes.includes(cur._fieldType)
+        ? 'vectorFields'
+        : 'nonVectorFields';
+
+      result[changedFieldType].push(cur);
+
+      return result;
+    },
+    { vectorFields: [] as FieldData[], nonVectorFields: [] as FieldData[] }
+  );
+};
+
+export const getVectorFieldOptions = (
+  fields: FieldData[],
+  indexes: IndexView[]
+): FieldOption[] => {
+  const options: FieldOption[] = fields.map(f => {
+    const embeddingType = getEmbeddingType(f._fieldType);
+    const defaultIndex = getDefaultIndexType(embeddingType);
+    const index = indexes.find(i => i._fieldName === f._fieldName);
+
+    return {
+      label: `${f._fieldName} (${index?._indexType || defaultIndex})`,
+      value: f._fieldName,
+      fieldType: f._fieldType,
+      indexInfo: index || null,
+    };
+  });
+
+  return options;
+};
+
+export const getNonVectorFieldsForFilter = (fields: FieldData[]): Field[] => {
+  const intTypes: DataType[] = ['Int8', 'Int16', 'Int32', 'Int64'];
+  return fields.map(f => ({
+    name: f._fieldName,
+    type: intTypes.includes(f._fieldType) ? 'int' : 'float',
+  }));
+};

+ 100 - 2
client/yarn.lock

@@ -1831,6 +1831,11 @@
   dependencies:
   dependencies:
     "@babel/types" "^7.3.0"
     "@babel/types" "^7.3.0"
 
 
+"@types/component-emitter@^1.2.10":
+  version "1.2.10"
+  resolved "https://registry.yarnpkg.com/@types/component-emitter/-/component-emitter-1.2.10.tgz#ef5b1589b9f16544642e473db5ea5639107ef3ea"
+  integrity sha512-bsjleuRKWmGqajMerkzox19aGbscQX5rmmvvXl3wlIp5gMG1HgkiwPxsN5p070fBDKTNSPgojVbuY1+HWMbFhg==
+
 "@types/eslint@^7.2.6":
 "@types/eslint@^7.2.6":
   version "7.2.13"
   version "7.2.13"
   resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-7.2.13.tgz#e0ca7219ba5ded402062ad6f926d491ebb29dd53"
   resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-7.2.13.tgz#e0ca7219ba5ded402062ad6f926d491ebb29dd53"
@@ -1931,6 +1936,13 @@
   resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e"
   resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e"
   integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==
   integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==
 
 
+"@types/papaparse@^5.2.6":
+  version "5.2.6"
+  resolved "https://registry.yarnpkg.com/@types/papaparse/-/papaparse-5.2.6.tgz#0bba18de4d15eff65883bc7c0794e0134de9e7c7"
+  integrity sha512-xGKSd0UTn58N1h0+zf8mW863Rv8BvXcGibEgKFtBIXZlcDXAmX/T4RdDO2mwmrmOypUDt5vRgo2v32a78JdqUA==
+  dependencies:
+    "@types/node" "*"
+
 "@types/parse-json@^4.0.0":
 "@types/parse-json@^4.0.0":
   version "4.0.0"
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
   resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
@@ -2928,11 +2940,21 @@ babylon@^6.18.0:
   resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3"
   resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3"
   integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==
   integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==
 
 
+backo2@~1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947"
+  integrity sha1-MasayLEpNjRj41s+u2n038+6eUc=
+
 balanced-match@^1.0.0:
 balanced-match@^1.0.0:
   version "1.0.2"
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
   resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
   integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
   integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
 
 
+base64-arraybuffer@0.1.4:
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz#9818c79e059b1355f97e0428a017c838e90ba812"
+  integrity sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=
+
 base64-js@^1.0.2:
 base64-js@^1.0.2:
   version "1.5.1"
   version "1.5.1"
   resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
   resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
@@ -3592,7 +3614,7 @@ commondir@^1.0.1:
   resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
   resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
   integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=
   integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=
 
 
-component-emitter@^1.2.1:
+component-emitter@^1.2.1, component-emitter@~1.3.0:
   version "1.3.0"
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
   resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
   integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==
   integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==
@@ -4134,6 +4156,13 @@ debug@^3.1.1, debug@^3.2.6, debug@^3.2.7:
   dependencies:
   dependencies:
     ms "^2.1.1"
     ms "^2.1.1"
 
 
+debug@~4.3.1:
+  version "4.3.2"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b"
+  integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==
+  dependencies:
+    ms "2.1.2"
+
 decamelize@^1.2.0:
 decamelize@^1.2.0:
   version "1.2.0"
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
   resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
@@ -4497,6 +4526,28 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0:
   dependencies:
   dependencies:
     once "^1.4.0"
     once "^1.4.0"
 
 
+engine.io-client@~5.1.2:
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-5.1.2.tgz#27108da9b39ae03262443d945caf2caa3655c4cb"
+  integrity sha512-blRrgXIE0A/eurWXRzvfCLG7uUFJqfTGFsyJzXSK71srMMGJ2VraBLg8Mdw28uUxSpVicepBN9X7asqpD1mZcQ==
+  dependencies:
+    base64-arraybuffer "0.1.4"
+    component-emitter "~1.3.0"
+    debug "~4.3.1"
+    engine.io-parser "~4.0.1"
+    has-cors "1.1.0"
+    parseqs "0.0.6"
+    parseuri "0.0.6"
+    ws "~7.4.2"
+    yeast "0.1.2"
+
+engine.io-parser@~4.0.1:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-4.0.2.tgz#e41d0b3fb66f7bf4a3671d2038a154024edb501e"
+  integrity sha512-sHfEQv6nmtJrq6TKuIz5kyEKH/qSdK56H/A+7DnAuUPWosnIZAS2NHNcPLmyjtY3cGS/MqJdZbUjW97JU72iYg==
+  dependencies:
+    base64-arraybuffer "0.1.4"
+
 enhanced-resolve@^4.3.0:
 enhanced-resolve@^4.3.0:
   version "4.5.0"
   version "4.5.0"
   resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz#2f3cfd84dbe3b487f18f2db2ef1e064a571ca5ec"
   resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz#2f3cfd84dbe3b487f18f2db2ef1e064a571ca5ec"
@@ -5534,6 +5585,11 @@ has-bigints@^1.0.1:
   resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113"
   resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113"
   integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==
   integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==
 
 
+has-cors@1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39"
+  integrity sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=
+
 has-flag@^3.0.0:
 has-flag@^3.0.0:
   version "3.0.0"
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
   resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
@@ -8107,6 +8163,11 @@ pako@~1.0.5:
   resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
   resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
   integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
   integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
 
 
+papaparse@^5.3.1:
+  version "5.3.1"
+  resolved "https://registry.yarnpkg.com/papaparse/-/papaparse-5.3.1.tgz#770b7a9124d821d4b2132132b7bd7dce7194b5b1"
+  integrity sha512-Dbt2yjLJrCwH2sRqKFFJaN5XgIASO9YOFeFP8rIBRG2Ain8mqk5r1M6DkfvqEVozVcz3r3HaUGw253hA1nLIcA==
+
 parallel-transform@^1.1.0:
 parallel-transform@^1.1.0:
   version "1.2.0"
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.2.0.tgz#9049ca37d6cb2182c3b1d2c720be94d14a5814fc"
   resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.2.0.tgz#9049ca37d6cb2182c3b1d2c720be94d14a5814fc"
@@ -8165,6 +8226,16 @@ parse5@6.0.1:
   resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
   resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
   integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
   integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
 
 
+parseqs@0.0.6:
+  version "0.0.6"
+  resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.6.tgz#8e4bb5a19d1cdc844a08ac974d34e273afa670d5"
+  integrity sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==
+
+parseuri@0.0.6:
+  version "0.0.6"
+  resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.6.tgz#e1496e829e3ac2ff47f39a4dd044b32823c4a25a"
+  integrity sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==
+
 parseurl@~1.3.2, parseurl@~1.3.3:
 parseurl@~1.3.2, parseurl@~1.3.3:
   version "1.3.3"
   version "1.3.3"
   resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
   resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
@@ -10239,6 +10310,28 @@ snapdragon@^0.8.1:
     source-map-resolve "^0.5.0"
     source-map-resolve "^0.5.0"
     use "^3.1.0"
     use "^3.1.0"
 
 
+socket.io-client@^4.1.3:
+  version "4.1.3"
+  resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.1.3.tgz#236daa642a9f229932e00b7221e843bf74232a62"
+  integrity sha512-hISFn6PDpgDifVUiNklLHVPTMv1LAk8poHArfIUdXa+gKgbr0MZbAlquDFqCqsF30yBqa+jg42wgos2FK50BHA==
+  dependencies:
+    "@types/component-emitter" "^1.2.10"
+    backo2 "~1.0.2"
+    component-emitter "~1.3.0"
+    debug "~4.3.1"
+    engine.io-client "~5.1.2"
+    parseuri "0.0.6"
+    socket.io-parser "~4.0.4"
+
+socket.io-parser@~4.0.4:
+  version "4.0.4"
+  resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.0.4.tgz#9ea21b0d61508d18196ef04a2c6b9ab630f4c2b0"
+  integrity sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==
+  dependencies:
+    "@types/component-emitter" "^1.2.10"
+    component-emitter "~1.3.0"
+    debug "~4.3.1"
+
 sockjs-client@^1.5.0:
 sockjs-client@^1.5.0:
   version "1.5.1"
   version "1.5.1"
   resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.5.1.tgz#256908f6d5adfb94dabbdbd02c66362cca0f9ea6"
   resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.5.1.tgz#256908f6d5adfb94dabbdbd02c66362cca0f9ea6"
@@ -11739,7 +11832,7 @@ ws@^6.2.1:
   dependencies:
   dependencies:
     async-limiter "~1.0.0"
     async-limiter "~1.0.0"
 
 
-ws@^7.4.5:
+ws@^7.4.5, ws@~7.4.2:
   version "7.4.6"
   version "7.4.6"
   resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c"
   resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c"
   integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==
   integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==
@@ -11828,6 +11921,11 @@ yargs@^15.4.1:
     y18n "^4.0.0"
     y18n "^4.0.0"
     yargs-parser "^18.1.2"
     yargs-parser "^18.1.2"
 
 
+yeast@0.1.2:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419"
+  integrity sha1-AI4G2AlDIMNy28L47XagymyKxBk=
+
 yocto-queue@^0.1.0:
 yocto-queue@^0.1.0:
   version "0.1.0"
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
   resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"

+ 33 - 0
server/generate-csv.ts

@@ -0,0 +1,33 @@
+import { createObjectCsvWriter as createCsvWriter } from 'csv-writer';
+
+// use to test vector insert
+const csvWriter = createCsvWriter({
+  path: './vectors.csv',
+  header: [
+    { id: 'vector', title: 'vector' },
+    { id: 'age', title: 'age' },
+  ],
+});
+
+const records = [];
+
+const generateVector = (dimension) => {
+  let index = 0;
+  const vectors = [];
+  while (index < dimension) {
+    vectors.push(1 + Math.random());
+    index++;
+  }
+  return JSON.stringify(vectors);
+};
+
+while (records.length < 50000) {
+  const value = generateVector(128);
+  records.push({ vector: value, age: 10 });
+}
+
+csvWriter
+  .writeRecords(records) // returns a promise
+  .then(() => {
+    console.log('...Done');
+  });

+ 8 - 1
server/package.json

@@ -26,11 +26,15 @@
     "@nestjs/jwt": "^7.2.0",
     "@nestjs/jwt": "^7.2.0",
     "@nestjs/passport": "^7.1.5",
     "@nestjs/passport": "^7.1.5",
     "@nestjs/platform-express": "^7.6.15",
     "@nestjs/platform-express": "^7.6.15",
+    "@nestjs/platform-socket.io": "^8.0.4",
     "@nestjs/serve-static": "^2.1.4",
     "@nestjs/serve-static": "^2.1.4",
     "@nestjs/swagger": "^4.8.0",
     "@nestjs/swagger": "^4.8.0",
+    "@nestjs/websockets": "^8.0.4",
     "@types/passport-jwt": "^3.0.5",
     "@types/passport-jwt": "^3.0.5",
     "@types/passport-local": "^1.0.33",
     "@types/passport-local": "^1.0.33",
-    "@zilliz/milvus2-sdk-node": "^0.1.0",
+    "@zilliz/milvus2-sdk-node": "^1.0.4",
+    "body-parser": "^1.19.0",
+    "cache-manager": "^3.4.4",
     "class-transformer": "^0.4.0",
     "class-transformer": "^0.4.0",
     "class-validator": "^0.13.1",
     "class-validator": "^0.13.1",
     "passport": "^0.4.1",
     "passport": "^0.4.1",
@@ -39,18 +43,21 @@
     "reflect-metadata": "^0.1.13",
     "reflect-metadata": "^0.1.13",
     "rimraf": "^3.0.2",
     "rimraf": "^3.0.2",
     "rxjs": "^6.6.6",
     "rxjs": "^6.6.6",
+    "socket.io": "^4.1.3",
     "swagger-ui-express": "^4.1.6"
     "swagger-ui-express": "^4.1.6"
   },
   },
   "devDependencies": {
   "devDependencies": {
     "@nestjs/cli": "^7.6.0",
     "@nestjs/cli": "^7.6.0",
     "@nestjs/schematics": "^7.3.0",
     "@nestjs/schematics": "^7.3.0",
     "@nestjs/testing": "^7.6.15",
     "@nestjs/testing": "^7.6.15",
+    "@types/cache-manager": "^3.4.2",
     "@types/express": "^4.17.11",
     "@types/express": "^4.17.11",
     "@types/jest": "^26.0.22",
     "@types/jest": "^26.0.22",
     "@types/node": "^14.14.36",
     "@types/node": "^14.14.36",
     "@types/supertest": "^2.0.10",
     "@types/supertest": "^2.0.10",
     "@typescript-eslint/eslint-plugin": "^4.19.0",
     "@typescript-eslint/eslint-plugin": "^4.19.0",
     "@typescript-eslint/parser": "^4.19.0",
     "@typescript-eslint/parser": "^4.19.0",
+    "csv-writer": "^1.6.0",
     "eslint": "^7.22.0",
     "eslint": "^7.22.0",
     "eslint-config-prettier": "^8.1.0",
     "eslint-config-prettier": "^8.1.0",
     "eslint-plugin-prettier": "^3.3.1",
     "eslint-plugin-prettier": "^3.3.1",

+ 9 - 9
server/src/app.module.ts

@@ -1,4 +1,4 @@
-import { Module, MiddlewareConsumer, RequestMethod } from '@nestjs/common';
+import { Module } from '@nestjs/common';
 import { APP_INTERCEPTOR } from '@nestjs/core';
 import { APP_INTERCEPTOR } from '@nestjs/core';
 import { ServeStaticModule } from '@nestjs/serve-static';
 import { ServeStaticModule } from '@nestjs/serve-static';
 import { AppController } from './app.controller';
 import { AppController } from './app.controller';
@@ -12,7 +12,8 @@ import { AuthModule } from './auth/auth.module';
 import { join } from 'path';
 import { join } from 'path';
 import { PartitionsModule } from './partitions/partitions.module';
 import { PartitionsModule } from './partitions/partitions.module';
 import { SchemaModule } from './schema/schema.module';
 import { SchemaModule } from './schema/schema.module';
-import { LoggerMiddleware } from './middlewares/logger';
+import { EventsModule } from './events/events.module';
+import { LoggingInterceptor } from './interceptors/index';
 
 
 @Module({
 @Module({
   imports: [
   imports: [
@@ -26,6 +27,7 @@ import { LoggerMiddleware } from './middlewares/logger';
     AuthModule,
     AuthModule,
     PartitionsModule,
     PartitionsModule,
     SchemaModule,
     SchemaModule,
+    EventsModule,
   ],
   ],
   controllers: [AppController],
   controllers: [AppController],
   providers: [
   providers: [
@@ -39,13 +41,11 @@ import { LoggerMiddleware } from './middlewares/logger';
       provide: APP_INTERCEPTOR,
       provide: APP_INTERCEPTOR,
       useClass: TransformResInterceptor,
       useClass: TransformResInterceptor,
     },
     },
+    {
+      provide: APP_INTERCEPTOR,
+      useClass: LoggingInterceptor,
+    },
     UsersService,
     UsersService,
   ],
   ],
 })
 })
-export class AppModule {
-  configure(consumer: MiddlewareConsumer) {
-    consumer
-      .apply(LoggerMiddleware)
-      .forRoutes({ path: '*', method: RequestMethod.ALL });
-  }
-}
+export class AppModule { }

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно