Forráskód Böngészése

Merge branch 'milvus-io:main' into mergify/shanghaikid/config-update

zhuanghong.chen 4 éve
szülő
commit
ba08256182
100 módosított fájl, 4292 hozzáadás és 593 törlés
  1. 4 15
      .github/ISSUE_TEMPLATE/Bug_report.md
  2. 3 1
      .github/ISSUE_TEMPLATE/Feature_request.md
  3. BIN
      .github/images/screenshot.png
  4. 14 0
      .github/workflows/client.ovpn
  5. 84 0
      .github/workflows/dev.yml
  6. 34 0
      .github/workflows/release.yml
  7. 2 0
      .gitignore
  8. 201 0
      LICENSE
  9. 101 1
      README.md
  10. 22 0
      checkInsight.js
  11. 1 1
      client/.prettierrc
  12. 41 26
      client/README.md
  13. 23 2
      client/package.json
  14. 2 2
      client/public/.env
  15. 2 2
      client/public/env-config.js
  16. 5 1
      client/public/index.html
  17. 4 0
      client/src/assets/icons/copy.svg
  18. 3 0
      client/src/assets/icons/key.svg
  19. 3 0
      client/src/assets/icons/nav-search.svg
  20. 5 0
      client/src/assets/icons/search.svg
  21. 5 0
      client/src/assets/icons/upload.svg
  22. BIN
      client/src/assets/imgs/insert/fail.png
  23. BIN
      client/src/assets/imgs/insert/success.png
  24. 20 0
      client/src/components/__test__/cards/EmptyCard.spec.tsx
  25. 0 31
      client/src/components/__test__/copy/Copy.spec.tsx
  26. 0 0
      client/src/components/__test__/customButton/CustomButton.spec.tsx
  27. 28 0
      client/src/components/__test__/customButton/CustomIconButton.spec.tsx
  28. 6 0
      client/src/components/__test__/customDialog/CustomDialog.spec.tsx
  29. 20 0
      client/src/components/__test__/customDialog/CustomDialogTitle.spec.tsx
  30. 50 0
      client/src/components/__test__/customDialog/DeleteDialogTemplate.spec.tsx
  31. 53 0
      client/src/components/__test__/customDialog/DialogTemplate.spec.tsx
  32. 4 0
      client/src/components/__test__/customInput/CustomInput.spec.tsx
  33. 76 0
      client/src/components/__test__/customInput/SearchInput.spec.tsx
  34. 32 0
      client/src/components/__test__/customTabList/CustomTabList.spec.tsx
  35. 1 1
      client/src/components/__test__/grid/Grid.spec.tsx
  36. 26 1
      client/src/components/__test__/layout/Layout.spec.tsx
  37. 7 2
      client/src/components/__test__/status/Status.spec.tsx
  38. 6 0
      client/src/components/__test__/utils/provideTheme.tsx
  39. 191 0
      client/src/components/advancedSearch/Condition.tsx
  40. 234 0
      client/src/components/advancedSearch/ConditionGroup.tsx
  41. 55 0
      client/src/components/advancedSearch/CopyButton.tsx
  42. 220 0
      client/src/components/advancedSearch/Dialog.tsx
  43. 320 0
      client/src/components/advancedSearch/Filter.tsx
  44. 84 0
      client/src/components/advancedSearch/Types.ts
  45. 3 0
      client/src/components/advancedSearch/index.tsx
  46. 1 1
      client/src/components/cards/EmptyCard.tsx
  47. 63 0
      client/src/components/code/CodeBlock.tsx
  48. 113 0
      client/src/components/code/CodeView.tsx
  49. 20 0
      client/src/components/code/Types.ts
  50. 0 55
      client/src/components/copy/Copy.tsx
  51. 2 2
      client/src/components/customButton/CustomButton.tsx
  52. 27 7
      client/src/components/customButton/CustomIconButton.tsx
  53. 11 11
      client/src/components/customDialog/CustomDialog.tsx
  54. 21 8
      client/src/components/customDialog/CustomDialogTitle.tsx
  55. 10 4
      client/src/components/customDialog/DeleteDialogTemplate.tsx
  56. 86 19
      client/src/components/customDialog/DialogTemplate.tsx
  57. 13 3
      client/src/components/customDialog/Types.ts
  58. 5 3
      client/src/components/customInput/CustomInput.tsx
  59. 94 37
      client/src/components/customInput/SearchInput.tsx
  60. 1 0
      client/src/components/customInput/Types.ts
  61. 64 0
      client/src/components/customProgress/CustomLinearProgress.tsx
  62. 6 0
      client/src/components/customProgress/Types.ts
  63. 19 24
      client/src/components/customSelector/CustomSelector.tsx
  64. 3 1
      client/src/components/customSelector/Types.ts
  65. 33 0
      client/src/components/customSwitch/CustomSwitch.tsx
  66. 3 0
      client/src/components/customSwitch/Types.ts
  67. 5 2
      client/src/components/customTabList/CustomTabList.tsx
  68. 1 0
      client/src/components/customTabList/Types.ts
  69. 14 3
      client/src/components/grid/Grid.tsx
  70. 73 27
      client/src/components/grid/Table.tsx
  71. 36 0
      client/src/components/grid/TableEditableHead.tsx
  72. 9 5
      client/src/components/grid/TableHead.tsx
  73. 4 2
      client/src/components/grid/TablePaginationActions.tsx
  74. 8 3
      client/src/components/grid/ToolBar.tsx
  75. 25 0
      client/src/components/grid/Types.ts
  76. 26 0
      client/src/components/icons/Icons.tsx
  77. 9 1
      client/src/components/icons/Types.ts
  78. 406 0
      client/src/components/insert/Container.tsx
  79. 201 0
      client/src/components/insert/Import.tsx
  80. 210 0
      client/src/components/insert/Preview.tsx
  81. 93 0
      client/src/components/insert/Status.tsx
  82. 77 0
      client/src/components/insert/Types.ts
  83. 0 105
      client/src/components/layout/GlobalToolbar.tsx
  84. 2 2
      client/src/components/layout/Header.tsx
  85. 38 17
      client/src/components/layout/Layout.tsx
  86. 126 57
      client/src/components/menu/NavMenu.tsx
  87. 43 21
      client/src/components/menu/SimpleMenu.tsx
  88. 7 1
      client/src/components/menu/Types.ts
  89. 2 2
      client/src/components/status/Status.tsx
  90. 1 1
      client/src/components/status/StatusIcon.tsx
  91. 17 0
      client/src/components/uploader/Types.ts
  92. 90 0
      client/src/components/uploader/Uploader.tsx
  93. 6 0
      client/src/consts/Insert.ts
  94. 120 65
      client/src/consts/Milvus.tsx
  95. 11 2
      client/src/context/Auth.tsx
  96. 1 1
      client/src/context/Root.tsx
  97. 1 1
      client/src/context/Types.ts
  98. 28 4
      client/src/hooks/Dialog.tsx
  99. 6 3
      client/src/hooks/Form.ts
  100. 6 7
      client/src/hooks/Navigation.ts

+ 4 - 15
.github/ISSUE_TEMPLATE/Bug_report.md

@@ -1,31 +1,20 @@
 ---
 name: Bug report
 about: Things break. Help us identify those things so we can fix them!
-labels: bug
+labels: defect
 
 ---
 
-**Milvus-insight version:**
-
-**Milvus version:**
-
-**Browser version:**
+**Describe the bug:**
 
-**Browser OS version:**
 
-**Describe the bug:**
 
 **Steps to reproduce:**
 1.
 2.
 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
 about: Milvus-insight can't do all the things, but maybe it can do your things.
+labels: feat
 
 ---
 
 **Describe the feature:**
 
-**Describe a specific use case for the feature:**
+
+**Describe a specific use case for the feature:**

BIN
.github/images/screenshot.png


+ 14 - 0
.github/workflows/client.ovpn

@@ -0,0 +1,14 @@
+client
+dev tun
+proto udp
+remote sssd.cvpn-endpoint-0dee3f0de72d9b0a5.prod.clientvpn.us-west-2.amazonaws.com 443
+resolv-retry infinite
+nobind
+remote-cert-tls server
+cipher AES-256-GCM
+verb 3
+ca ca.crt
+cert user.crt
+key user.key
+
+reneg-sec 0

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

@@ -0,0 +1,84 @@
+name: Milvus insight dev release
+
+on:
+  pull_request_target:
+    branches: [main]
+    types: [closed]
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    if: github.event.pull_request.merged == true
+    steps:
+      - uses: actions/checkout@v2
+      - name: Setup Node.js
+        uses: actions/setup-node@v1
+        with:
+          node-version: 12
+
+      - name: Login to DockerHub
+        uses: docker/login-action@v1
+        with:
+          username: ${{ secrets.DOCKER_USERNAME }}
+          password: ${{ secrets.DOCKER_PWD }}
+
+      - name: Docker Build
+        run: docker build -t milvusdb/milvus-insight:dev --build-arg VERSION=dev .
+
+      - name: Docker Push Dev
+        run: docker push milvusdb/milvus-insight:dev
+
+  k8s:
+    runs-on: ubuntu-latest
+    needs: build
+    steps:
+      - uses: actions/checkout@v2
+      - name: Setup Node.js
+        uses: actions/setup-node@v1
+        with:
+          node-version: 12
+
+      - name: Install OpenVPN and kubectl
+        run: |
+          sudo apt-get update
+          sudo apt-get install openvpn -y
+          sudo apt-get install -y apt-transport-https ca-certificates curl
+          sudo curl -fsSLo /usr/share/keyrings/kubernetes-archive-keyring.gpg https://packages.cloud.google.com/apt/doc/apt-key.gpg
+          echo "deb [signed-by=/usr/share/keyrings/kubernetes-archive-keyring.gpg] https://apt.kubernetes.io/ kubernetes-xenial main" | sudo tee /etc/apt/sources.list.d/kubernetes.list
+          sudo apt-get update
+          sudo apt-get install kubectl -y
+
+      - name: Connect VPN
+        uses: golfzaptw/action-connect-ovpn@master
+        id: connect_vpn
+        with:
+          FILE_OVPN: '.github/workflows/client.ovpn'
+        env:
+          CA_CRT: ${{ secrets.VPN_CA}}
+          USER_CRT: ${{ secrets.VPN_CRT }}
+          USER_KEY: ${{ secrets.VPN_KEY }}
+
+      - name: Deploy to cluster
+        run: |
+          echo ${{ secrets.kubeconfig }} > config64
+          base64 -d config64 > kubeconfig
+          kubectl delete pods -n ued -l app=milvus-insight --kubeconfig=kubeconfig
+          sleep 60
+
+  check:
+    runs-on: ubuntu-latest
+    needs: [build, k8s]
+    steps:
+      - uses: actions/checkout@v2
+      - name: Setup Node.js
+        uses: actions/setup-node@v1
+        with:
+          node-version: 12
+
+      - name: Check insight status
+        env:
+          INSIGHT_URL: ${{ secrets.INSIGHT_URL }}
+        run: |
+          yarn add axios
+          yarn add @actions/core
+          node checkInsight.js

+ 34 - 0
.github/workflows/release.yml

@@ -0,0 +1,34 @@
+name: Milvus insight prod release
+
+on:
+  release:
+    types: [released]
+    branches: [main]
+
+jobs:
+  publish:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+      - name: Setup Node.js
+        uses: actions/setup-node@v1
+        with:
+          node-version: 12
+
+      - name: Login to DockerHub
+        uses: docker/login-action@v1
+        with:
+          username: ${{ secrets.DOCKER_USERNAME }}
+          password: ${{ secrets.DOCKER_PWD }}
+
+      - name: Docker Build
+        run: docker build -t milvusdb/milvus-insight:${GITHUB_REF#refs/tags/} --build-arg VERSION=${GITHUB_REF#refs/tags/} .
+
+      - name: Docker tag
+        run: docker tag milvusdb/milvus-insight:${GITHUB_REF#refs/tags/} milvusdb/milvus-insight:latest
+
+      # - name: Docker Push version
+      #   run: docker push milvusdb/cloud-ui:${GITHUB_REF#refs/tags/}
+
+      - name: Docker Push lastest
+        run: docker push milvusdb/milvus-insight

+ 2 - 0
.gitignore

@@ -1,6 +1,7 @@
 # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
 
 # dependencies
+node_modules
 /client/node_modules
 /client/build
 /.pnp
@@ -31,6 +32,7 @@ server/dist
 server/build
 server/coverage
 server/documentation
+server/vectors.csv
 
 
 # package.lock.json

+ 201 - 0
LICENSE

@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 101 - 1
README.md

@@ -1 +1,101 @@
-# Milvus Insight
+# Milvus insight
+[![typescript](https://badges.aleen42.com/src/typescript.svg)](https://badges.aleen42.com/src/typescript.svg)
+[![downloads](https://img.shields.io/docker/pulls/milvusdb/milvus-insight)](https://img.shields.io/docker/pulls/milvusdb/milvus-insight)
+
+Milvus insight provides an intuitive and efficient GUI for Milvus, allowing you to interact with your databases and manage your data with just few clicks.
+
+<img src="./.github/images/screenshot.png" alt="Miluvs insight" />
+
+## 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)
+- Data view (TBD)
+- View milvus node configuration(TBD)
+- Vector Visualization(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.
+
+### ⭐️ Start a Milvus insight instance
+
+```code
+docker run -p 8000:3000 -e HOST_URL=http://{ your machine IP }:8000 -e MILVUS_URL={your machine IP}:19530 milvusdb/milvus-insight:latest
+```
+
+Once you start the docker, open the browser, type `http://{ your machine IP }:8000`, you can view the Milvus insight.
+
+#### Params
+
+| Parameter  | Example                 | required | description                                 |
+| :--------- | :---------------------- | :------: | ------------------------------------------- |
+| HOST_URL   | http://192.168.0.1:8000 |   true   | Where Milvus insight container is installed |
+| MILVUS_URL | 192.168.0.1:19530       |  false   | Optional, Milvus server URL                 |
+
+Tip: **127.0.0.1 or localhost will not work when runs on docker**
+
+#### Try the dev build
+
+**_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.
+
+```code
+docker run -p 8000:3000 -e HOST_URL=http://{ your machine IP }:8000 -e MILVUS_URL={ your machine IP }:19530 milvusdb/milvus-insight:dev
+```
+
+## ✨ 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
+out an open PR:
+
+### Build server
+
+1. Fork and clone the repo
+2. `cd server` go to the server directory
+3. `yarn install` to install dependencies
+4. Create a branch for your PR
+
+### Build client
+
+1. Fork and clone the repo
+2. `cd client` go to the client directory
+3. `yarn install` to install dependencies
+4. Create a branch for your PR
+
+### Milvus
+
+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 python sdk](https://milvus.io/docs/v2.0.0/explore_pymilvus.md)
+- [Milvus bootcamp](https://milvus.io/bootcamp)
+
+## 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.
+
+<a href="https://join.slack.com/t/milvusio/shared_invite/zt-e0u4qu3k-bI2GDNys3ZqX1YCJ9OM~GQ">
+    <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>
+
+#### ❓ 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).
+  Please check to make sure someone else hasn't already created an issue for the same topic.
+- Need help using Milvus insight? Ask away on our [Milvus insight Discuss Forum](https://github.com/milvus-io/milvus-insight/discussions) and a fellow community member or
+  Milvus engineer will be glad to help you out.
+
+[milvus-doc]: https://milvus.io/docs/home
+[nestjs]: https://docs.nestjs.com/

+ 22 - 0
checkInsight.js

@@ -0,0 +1,22 @@
+const axios = require('axios');
+const core = require('@actions/core');
+
+const BASE_URL = process.env.INSIGHT_URL;
+
+const check = async () => {
+  const clientRes = await axios.get(`${BASE_URL}/connect`);
+  const serverRes = await axios.get(`${BASE_URL}/api/v1/healthy`);
+  if (serverRes.data.statusCode === 200) {
+    console.log('---- Server OK -----');
+  } else {
+    core.setFailed('---- Server has some error ----');
+  }
+
+  if (clientRes.data.includes('<html')) {
+    console.log('---- Client OK -----');
+  } else {
+    core.setFailed('---- Client has some error ----');
+  }
+};
+
+check();

+ 1 - 1
.prettierrc → client/.prettierrc

@@ -7,4 +7,4 @@
   "trailingComma": "es5",
   "bracketSpacing": true,
   "arrowParens": "avoid"
-}
+}

+ 41 - 26
client/README.md

@@ -1,46 +1,61 @@
-# Getting Started with Create React App
+# Milvus insight client
 
-This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
+## How to run
 
-## Available Scripts
+1. yarn install
+2. yarn start
 
-In the project directory, you can run:
+## Folder Structure
 
-### `yarn start`
+    └── public                    # Static resources
+    └── src
+      ├── assets                  # Put images here
+      ├── components              # Components
+      ├── consts                  # Constant values
+      ├── context                 # React context
+      ├── hooks                   # React hooks
+      ├── http                    # Http request api. And we have http interceptor in GlobalEffect.tsx file
+      ├── i18n                    # Language i18n
+      ├── pages                   # All pages , business components and types.
+      ├── router                  # React router, control the page auth.
+      ├── styles                  # Styles, normally we use material to control styles.
+      ├── types                   # Global types
+      └── utils                   # The common functoins
 
-Runs the app in the development mode.\
-Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
+### How to name the file
 
-The page will reload if you make edits.\
-You will also see any lint errors in the console.
+We use Camel-Case to name the file.
 
-### `yarn test`
+In components / pages folder, we need subfolder to wrapper all related files.
 
-Launches the test runner in the interactive watch mode.\
-See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
+### Global Effect
 
-### `yarn build`
+We get global data or take global side effect in components/layout/GlobalEffect
 
-Builds the app for production to the `build` folder.\
-It correctly bundles React in production mode and optimizes the build for the best performance.
+### Http request
 
-The build is minified and the filenames include the hashes.\
-Your app is ready to be deployed!
+We support user to define HOST_URL when docker run and it will write the env-config.js in public folder.
 
-See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
+We use class getter to define our client fields like \_field, because of our server response fields may be changed.
 
-### `yarn eject`
+### Helper Folder
 
-**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
+Like utils / consts / utils / hooks , we dont want put all functions or datas in one file like index.ts because of maintainability.
 
-If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
+So when we need to create new file , treat the file like Class then name it.
 
-Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
+### Icon
 
-You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
+We put all icons in components/icons file. Normally we use material icon.
 
-## Learn More
+If we use custom svg, like: import { ReactComponent as MilvusEmIcon } from xxx/xxx.svg'.
 
-You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
+It's react component because of svgr/webpack in webpack config.
 
-To learn React, check out the [React documentation](https://reactjs.org/).
+### Build
+
+We use react-app-rewired to change webpack config.
+
+If we want to change the webpack config, we can edit config-overrides.js file.
+
+And we use milvus insight server to host our client site. So our build path is `../server/build` .

+ 23 - 2
client/package.json

@@ -1,6 +1,9 @@
 {
-  "name": "milvus-admin",
+  "name": "milvus-insight-client",
   "version": "0.1.0",
+  "description": "Milvus insight UI Client",
+  "license": "Apache-2.0",
+  "bugs": "https://github.com/milvus-io/milvus-insight/issues",
   "private": true,
   "dependencies": {
     "@material-ui/core": "^4.11.4",
@@ -11,26 +14,40 @@
     "@testing-library/user-event": "^12.1.10",
     "@types/jest": "^26.0.15",
     "@types/node": "^12.0.0",
+    "@types/papaparse": "^5.2.6",
     "@types/react": "^17.0.0",
     "@types/react-dom": "^17.0.0",
+    "@types/react-highlight-words": "^0.16.2",
     "@types/react-router-dom": "^5.1.7",
+    "@types/react-syntax-highlighter": "^13.5.2",
     "axios": "^0.21.1",
     "dayjs": "^1.10.5",
     "i18next": "^20.3.1",
+    "papaparse": "^5.3.1",
     "react": "^17.0.2",
     "react-app-rewired": "^2.1.8",
     "react-dom": "^17.0.2",
+    "react-highlight-words": "^0.17.0",
     "react-i18next": "^11.10.0",
     "react-router-dom": "^5.2.0",
     "react-scripts": "4.0.3",
+    "react-syntax-highlighter": "^15.4.4",
+    "socket.io-client": "^4.1.3",
     "typescript": "^4.1.2",
     "web-vitals": "^1.0.1"
   },
+  "jest": {
+    "coverageDirectory": "<rootDir>/coverage/"
+  },
   "scripts": {
     "start": "react-app-rewired start -FAST_REFRESH=true",
     "build": "react-app-rewired build",
     "test": "react-app-rewired test",
-    "eject": "react-app-rewired eject"
+    "test:watch": "react-app-rewired test --watch",
+    "test:cov": "react-app-rewired test --watchAll=false --coverage",
+    "test:report": "react-app-rewired test --watchAll=false --coverage --coverageReporters='text-summary'",
+    "eject": "react-app-rewired eject",
+    "format": "prettier --write '**/*.{ts,js,tsx,jsx,css}'"
   },
   "eslintConfig": {
     "extends": [
@@ -49,5 +66,9 @@
       "last 1 firefox version",
       "last 1 safari version"
     ]
+  },
+  "devDependencies": {
+    "@testing-library/react-hooks": "^7.0.1",
+    "prettier": "2.3.2"
   }
 }

+ 2 - 2
client/public/.env

@@ -1,2 +1,2 @@
-URL=http://
-API_URL=http://127.0.0.1:3000
+MILVUS_URL=127.0.0.1:19530
+HOST_URL=http://127.0.0.1:3000

+ 2 - 2
client/public/env-config.js

@@ -1,4 +1,4 @@
 window._env_ = {
-  URL: 'http://',
-  API_URL: 'http://127.0.0.1:3000',
+  MILVUS_URL: '127.0.0.1:19530',
+  HOST_URL: 'http://127.0.0.1:3000',
 };

+ 5 - 1
client/public/index.html

@@ -19,6 +19,10 @@
       href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700;900&display=swap"
       rel="stylesheet"
     />
+    <link
+      href="https://fonts.googleapis.com/css2?family=Source+Code+Pro:wght@300;400&display=swap"
+      rel="stylesheet"
+    />
     <script src="%PUBLIC_URL%/env-config.js"></script>
 
     <!--
@@ -30,7 +34,7 @@
       work correctly both with client-side routing and a non-root public URL.
       Learn how to configure a non-root public URL by running `npm run build`.
     -->
-    <title>Milvus Admin</title>
+    <title>Milvus Insight</title>
   </head>
 
   <body>

+ 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/key.svg

@@ -0,0 +1,3 @@
+<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="M13.8808 1.50995C14.1182 1.74522 14.1182 2.12668 13.8808 2.36195L13.0948 3.14086L14.4888 4.52223C14.7262 4.7575 14.7262 5.13896 14.4888 5.37423L12.361 7.48283C12.1236 7.7181 11.7386 7.7181 11.5012 7.48283L10.1073 6.10146L8.43759 7.75607C8.6245 8.00658 8.78102 8.27889 8.90336 8.56747C9.10521 9.04361 9.21003 9.55463 9.21178 10.0711C9.21352 10.5875 9.11216 11.0992 8.91353 11.5767C8.7149 12.0542 8.42291 12.488 8.0544 12.8532C7.68588 13.2184 7.24811 13.5077 6.76628 13.7046C6.28446 13.9014 5.7681 14.0019 5.24694 14.0001C4.72578 13.9984 4.21011 13.8945 3.72963 13.6945C3.24915 13.4945 2.81334 13.2022 2.4473 12.8346L2.44274 12.83C1.72292 12.0915 1.32464 11.1022 1.33365 10.0755C1.34265 9.04874 1.75824 8.06657 2.4909 7.34052C3.22356 6.61447 4.21468 6.20263 5.25078 6.19371C6.08538 6.18652 6.89538 6.44123 7.56841 6.9134L13.0211 1.50995C13.2585 1.27468 13.6434 1.27468 13.8808 1.50995ZM12.2351 3.99287L10.967 5.24946L11.9311 6.20483L13.1991 4.94823L12.2351 3.99287ZM3.31509 11.9906C2.81817 11.4796 2.54326 10.7957 2.54948 10.086C2.55572 9.37514 2.84343 8.69517 3.35066 8.19252C3.85789 7.68987 4.54405 7.40475 5.26135 7.39857C5.97865 7.3924 6.6697 7.66565 7.18567 8.15949C7.19118 8.16477 7.19677 8.16992 7.20243 8.17495C7.4495 8.42177 7.64642 8.71346 7.78238 9.03415C7.92213 9.36379 7.99469 9.71757 7.9959 10.0751C7.99711 10.4327 7.92694 10.7869 7.78942 11.1175C7.65191 11.448 7.44976 11.7484 7.19464 12.0012C6.93951 12.254 6.63644 12.4543 6.30286 12.5906C5.96929 12.7269 5.61181 12.7964 5.25101 12.7952C4.89021 12.794 4.53321 12.7221 4.20057 12.5836C3.86893 12.4456 3.56803 12.244 3.31509 11.9906ZM3.31509 11.9906L3.31733 11.9929L2.88005 12.4115L3.3128 11.9883L3.31509 11.9906Z" fill="#010E29"/>
+</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


+ 20 - 0
client/src/components/__test__/cards/EmptyCard.spec.tsx

@@ -0,0 +1,20 @@
+import { render, screen, RenderResult } from '@testing-library/react';
+import EmptyCard from '../../cards/EmptyCard';
+import provideTheme from '../utils/provideTheme';
+
+let body: RenderResult;
+
+describe('test empty card component', () => {
+  beforeEach(() => {
+    body = render(
+      provideTheme(
+        <EmptyCard icon={<span className="icon">icon</span>} text="empty" />
+      )
+    );
+  });
+
+  it('renders default state', () => {
+    expect(screen.getByText('icon')).toHaveClass('icon');
+    expect(screen.getByText('empty')).toBeInTheDocument();
+  });
+});

+ 0 - 31
client/src/components/__test__/copy/Copy.spec.tsx

@@ -1,31 +0,0 @@
-import { render, unmountComponentAtNode } from 'react-dom';
-import { act } from 'react-dom/test-utils';
-import Copy from '../../copy/Copy';
-let container: any = null;
-
-jest.mock('@material-ui/core/Tooltip', () => {
-  return props => {
-    return <div id="tooltip">{props.children}</div>;
-  };
-});
-
-describe('Test Copy Component', () => {
-  beforeEach(() => {
-    container = document.createElement('div');
-    document.body.appendChild(container);
-  });
-
-  afterEach(() => {
-    unmountComponentAtNode(container);
-    container.remove();
-    container = null;
-  });
-
-  it('Test props ', () => {
-    act(() => {
-      render(<Copy data={[]}></Copy>, container);
-    });
-
-    expect(document.querySelectorAll('button').length).toEqual(1);
-  });
-});

+ 0 - 0
client/src/components/__test__/customButton/customButton.spec.tsx → client/src/components/__test__/customButton/CustomButton.spec.tsx


+ 28 - 0
client/src/components/__test__/customButton/CustomIconButton.spec.tsx

@@ -0,0 +1,28 @@
+import { fireEvent, render, screen } from '@testing-library/react';
+import CustomIconButton from '../../customButton/CustomIconButton';
+
+describe('test custom icon button component', () => {
+  it('renders default state', () => {
+    render(
+      <CustomIconButton>
+        <div className="icon">icon</div>
+      </CustomIconButton>
+    );
+
+    expect(screen.getByText('icon')).toHaveClass('icon');
+
+    const tooltip = screen.queryByText('tooltip');
+    expect(tooltip).toBeNull();
+  });
+
+  it('checks tooltip', async () => {
+    render(
+      <CustomIconButton tooltip="tooltip">
+        <div className="icon">icon</div>
+      </CustomIconButton>
+    );
+    // mock hover event
+    fireEvent.mouseOver(screen.getByText('icon'));
+    expect(await screen.findByText('tooltip')).toBeInTheDocument();
+  });
+});

+ 6 - 0
client/src/components/__test__/customDialog/CustomDialog.spec.tsx

@@ -6,6 +6,12 @@ import CustomDialog from '../../customDialog/CustomDialog';
 
 let container: any = null;
 
+jest.mock('react-i18next', () => ({
+  useTranslation: () => ({
+    t: (key: any) => key,
+  }),
+}));
+
 jest.mock('@material-ui/core/Dialog', () => {
   return props => {
     return <div id="dialog-wrapper">{props.children}</div>;

+ 20 - 0
client/src/components/__test__/customDialog/CustomDialogTitle.spec.tsx

@@ -0,0 +1,20 @@
+import { fireEvent, render } from '@testing-library/react';
+import CustomDialogTitle from '../../customDialog/CustomDialogTitle';
+
+describe('test custom dialog title component', () => {
+  it('renders default state', () => {
+    const container = render(<CustomDialogTitle>title</CustomDialogTitle>);
+
+    expect(container.getByText('title')).toBeInTheDocument();
+    expect(container.queryByTestId('clear-icon')).toBeNull();
+  });
+
+  it('checks clear event', () => {
+    const mockClearFn = jest.fn();
+    const container = render(
+      <CustomDialogTitle onClose={mockClearFn}>title</CustomDialogTitle>
+    );
+    fireEvent.click(container.getByTestId('clear-icon'));
+    expect(mockClearFn).toBeCalledTimes(1);
+  });
+});

+ 50 - 0
client/src/components/__test__/customDialog/DeleteDialogTemplate.spec.tsx

@@ -0,0 +1,50 @@
+import { screen, render, fireEvent } from '@testing-library/react';
+import DeleteTemplate from '../../customDialog/DeleteDialogTemplate';
+import provideTheme from '../utils/provideTheme';
+import { I18nextProvider } from 'react-i18next';
+import i18n from '../../../i18n';
+
+describe('test delete dialog template component', () => {
+  const mockDeleteFn = jest.fn();
+  const mockCancelFn = jest.fn();
+
+  beforeEach(() => {
+    render(
+      provideTheme(
+        <I18nextProvider i18n={i18n}>
+          <DeleteTemplate
+            title="delete title"
+            text="delete text"
+            label="delete"
+            handleDelete={mockDeleteFn}
+            handleCancel={mockCancelFn}
+          />
+        </I18nextProvider>
+      )
+    );
+  });
+
+  it('renders default state', () => {
+    expect(screen.getByText('delete title')).toBeInTheDocument();
+    expect(screen.getByText('delete text')).toBeInTheDocument();
+  });
+
+  it('checks button disabled status and callback when input value change', () => {
+    // Material Textfield role should be textbox
+    const input = screen.getByRole('textbox');
+    const deleteBtn = screen.getByRole('button', { name: /delete/i });
+    fireEvent.change(input, { target: { value: 'test' } });
+    expect(deleteBtn).toBeDisabled();
+    fireEvent.change(input, { target: { value: 'delete' } });
+    expect(deleteBtn).not.toBeDisabled();
+    fireEvent.click(deleteBtn);
+    expect(mockDeleteFn).toBeCalledTimes(1);
+  });
+
+  it('checks cancel callback', () => {
+    const cancelBtn = screen.getByRole('button', { name: /cancel/i });
+
+    fireEvent.click(cancelBtn);
+    expect(mockCancelFn).toBeCalledTimes(1);
+  });
+});

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

@@ -0,0 +1,53 @@
+import { screen, render, fireEvent } from '@testing-library/react';
+import DialogTemplate from '../../customDialog/DialogTemplate';
+import provideTheme from '../utils/provideTheme';
+import { I18nextProvider } from 'react-i18next';
+import i18n from '../../../i18n';
+
+describe('test dialog template component', () => {
+  const mockCancelFn = jest.fn();
+  const mockConfirmFn = jest.fn();
+
+  it('renders default state and callbacks', () => {
+    render(
+      provideTheme(
+        <I18nextProvider i18n={i18n}>
+          <DialogTemplate
+            title="dialog template"
+            handleClose={mockCancelFn}
+            handleConfirm={mockConfirmFn}
+          >
+            dialog content
+          </DialogTemplate>
+        </I18nextProvider>
+      )
+    );
+
+    expect(screen.getByText('dialog template')).toBeInTheDocument();
+    expect(screen.getByText('dialog content')).toBeInTheDocument();
+
+    fireEvent.click(screen.getByRole('button', { name: /cancel/i }));
+    expect(mockCancelFn).toBeCalledTimes(1);
+    fireEvent.click(screen.getByRole('button', { name: /confirm/i }));
+    expect(mockConfirmFn).toBeCalledTimes(1);
+  });
+
+  it('checks confirm button disable', () => {
+    render(
+      provideTheme(
+        <I18nextProvider i18n={i18n}>
+          <DialogTemplate
+            title="dialog template"
+            handleClose={mockCancelFn}
+            handleConfirm={mockConfirmFn}
+            confirmDisabled={true}
+          >
+            dialog content
+          </DialogTemplate>
+        </I18nextProvider>
+      )
+    );
+
+    expect(screen.getByRole('button', { name: /confirm/i })).toBeDisabled();
+  });
+});

+ 4 - 0
client/src/components/__test__/textField/customInput.spec.tsx → client/src/components/__test__/customInput/CustomInput.spec.tsx

@@ -10,6 +10,10 @@ import {
 
 let container: any = null;
 
+jest.mock('@material-ui/core/styles/makeStyles', () => {
+  return () => () => ({});
+});
+
 jest.mock('@material-ui/core/FormControl', () => {
   return props => {
     const { children } = props;

+ 76 - 0
client/src/components/__test__/customInput/SearchInput.spec.tsx

@@ -0,0 +1,76 @@
+import { fireEvent, render } from '@testing-library/react';
+import SearchInput from '../../customInput/SearchInput';
+import { I18nextProvider } from 'react-i18next';
+import i18n from '../../../i18n';
+import { Router } from 'react-router-dom';
+
+const mockHistoryPushFn = jest.fn();
+
+jest.mock('react-router-dom', () => ({
+  useHistory: () => ({
+    push: mockHistoryPushFn,
+    location: {
+      search: '',
+    },
+  }),
+}));
+
+// clear the influence of jest.useFakeTimers
+afterEach(() => {
+  jest.useRealTimers();
+});
+
+describe('test search input component', () => {
+  it('renders default state', () => {
+    const mockSearchFn = jest.fn();
+    const container = render(
+      <I18nextProvider i18n={i18n}>
+        <SearchInput searchText="search text" onSearch={mockSearchFn} />
+      </I18nextProvider>
+    );
+
+    // material textfield input role is textbox
+    expect(container.getByRole('textbox')).toBeInTheDocument();
+    expect(container.getByRole('textbox')).toHaveValue('search text');
+  });
+
+  it('checks input value change event', () => {
+    const mockSearchFn = jest.fn();
+    const container = render(
+      <I18nextProvider i18n={i18n}>
+        <SearchInput onSearch={mockSearchFn} />
+      </I18nextProvider>
+    );
+
+    const input = container.getByRole('textbox');
+    fireEvent.change(input, { target: { value: 'search change test' } });
+    expect(input).toHaveValue('search change test');
+    // mock Enter key press event
+    fireEvent.keyPress(input, { key: 'Enter', code: 13, charCode: 13 });
+    expect(mockSearchFn).toBeCalledTimes(1);
+    // mock clear icon click event
+    const clearIcon = container.getByTestId('clear-icon');
+    fireEvent.click(clearIcon);
+    expect(input).toHaveValue('');
+  });
+
+  it('checks location change according to search value', () => {
+    const mockSearchFn = jest.fn();
+    // mock setTimeout
+    jest.useFakeTimers();
+
+    const container = render(
+      <I18nextProvider i18n={i18n}>
+        <SearchInput onSearch={mockSearchFn} />
+      </I18nextProvider>
+    );
+
+    const input = container.getByRole('textbox');
+    fireEvent.change(input, { target: { value: 'route' } });
+    expect(mockHistoryPushFn).not.toBeCalled();
+    // fast-forward until all timers have been executed
+    jest.runAllTimers();
+    expect(mockHistoryPushFn).toBeCalled();
+    expect(mockHistoryPushFn).toBeCalledWith({ search: 'search=route' });
+  });
+});

+ 32 - 0
client/src/components/__test__/customTabList/CustomTabList.spec.tsx

@@ -0,0 +1,32 @@
+import { fireEvent, render, screen } from '@testing-library/react';
+import CustomTabList from '../../customTabList/CustomTabList';
+import provideTheme from '../utils/provideTheme';
+
+const mockTabs = [
+  {
+    label: 'tab-1',
+    component: <div>tab 1 content</div>,
+  },
+  {
+    label: 'tab-2',
+    component: <div>tab 2 content</div>,
+  },
+];
+
+describe('test custom tab list component', () => {
+  beforeEach(() => {
+    render(provideTheme(<CustomTabList tabs={mockTabs} />));
+  });
+
+  it('renders default state', () => {
+    expect(screen.getAllByRole('tab').length).toBe(2);
+    // default active tab should be first one
+    expect(screen.getByText('tab 1 content')).toBeInTheDocument();
+  });
+
+  it('checks click tab event', () => {
+    const tab2 = screen.getByText('tab-2');
+    fireEvent.click(tab2);
+    expect(screen.getByText('tab 2 content')).toBeInTheDocument();
+  });
+});

+ 1 - 1
client/src/components/__test__/grid/index.spec.tsx → client/src/components/__test__/grid/Grid.spec.tsx

@@ -1,6 +1,6 @@
 import { render, unmountComponentAtNode } from 'react-dom';
 import { act } from 'react-dom/test-utils';
-import MilvusGrid from '../../grid/index';
+import MilvusGrid from '../../grid/Grid';
 import { ToolBarConfig } from '../../grid/Types';
 
 let container: any = null;

+ 26 - 1
client/src/components/__test__/layout/Layout.spec.tsx

@@ -1,9 +1,29 @@
 import { render, unmountComponentAtNode } from 'react-dom';
 import { act } from 'react-dom/test-utils';
 import Layout from '../../layout/Layout';
+import { MuiThemeProvider } from '@material-ui/core/styles';
+import { theme } from '../../../styles/theme';
 
 let container: any = null;
 
+jest.mock('react-i18next', () => ({
+  useTranslation: () => ({
+    t: (key: any) => key,
+  }),
+}));
+
+jest.mock('react-router-dom', () => ({
+  useHistory: () => ({
+    push: jest.fn(),
+  }),
+  useLocation: () => ({
+    hash: '',
+    pathname: '/use-location-mock',
+    search: '',
+    state: undefined,
+  }),
+}));
+
 jest.mock('../../layout/GlobalEffect', () => {
   return () => {
     return <div id="global">{}</div>;
@@ -24,7 +44,12 @@ describe('Test Layout', () => {
 
   it('Test Render', () => {
     act(() => {
-      render(<Layout />, container);
+      render(
+        <MuiThemeProvider theme={theme}>
+          <Layout />
+        </MuiThemeProvider>,
+        container
+      );
     });
 
     expect(container.querySelectorAll('#global').length).toEqual(1);

+ 7 - 2
client/src/components/__test__/status/Status.spec.tsx

@@ -1,3 +1,4 @@
+import { ReactNode } from 'react';
 import { render, unmountComponentAtNode } from 'react-dom';
 import { act } from 'react-dom/test-utils';
 import Status from '../../status/Status';
@@ -5,11 +6,15 @@ import { StatusEnum } from '../../status/Types';
 
 let container: any = null;
 
+jest.mock('@material-ui/core/styles/makeStyles', () => {
+  return () => () => ({});
+});
+
 jest.mock('react-i18next', () => {
   return {
     useTranslation: () => {
       return {
-        t: name => {
+        t: () => {
           return {
             loaded: 'loaded',
             unloaded: 'unloaded',
@@ -22,7 +27,7 @@ jest.mock('react-i18next', () => {
 });
 
 jest.mock('@material-ui/core/Typography', () => {
-  return props => {
+  return (props: { children: ReactNode }) => {
     return <div className="label">{props.children}</div>;
   };
 });

+ 6 - 0
client/src/components/__test__/utils/provideTheme.tsx

@@ -0,0 +1,6 @@
+import { ReactElement } from 'react';
+import { MuiThemeProvider } from '@material-ui/core/styles';
+import { theme } from '../../../styles/theme';
+export default (ui: ReactElement): ReactElement => {
+  return <MuiThemeProvider theme={theme}>{ui}</MuiThemeProvider>;
+};

+ 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;

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

@@ -0,0 +1,55 @@
+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';
+import { useTranslation } from 'react-i18next';
+
+const CopyIcon = icons.copyExpression;
+
+const CopyButton: FC<CopyButtonProps> = props => {
+  const { label, icon, className, value = '', ...others } = props;
+  const classes = useStyles();
+  const { t: commonTrans } = useTranslation();
+  const copyTrans = commonTrans('copy');
+  const [tooltipTitle, setTooltipTitle] = useState('Copy');
+
+  const handleClick = (event: React.MouseEvent<HTMLElement>, v: string) => {
+    event.stopPropagation();
+
+    setTooltipTitle(copyTrans.copied);
+    navigator.clipboard.writeText(v);
+    setTimeout(() => {
+      setTooltipTitle(copyTrans.copy);
+    }, 1000);
+  };
+
+  return (
+    <CustomIconButton
+      tooltip={tooltipTitle}
+      aria-label={label}
+      className={`${classes.button} ${className}`}
+      onClick={event => handleClick(event, 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;

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

@@ -0,0 +1,84 @@
+// 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;
+  // needed for accessibility, will not show on page
+  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/cards/EmptyCard.tsx

@@ -11,7 +11,7 @@ const useStyles = makeStyles((theme: Theme) => ({
     marginTop: theme.spacing(4),
     fontSize: '36px',
     lineHeight: '42px',
-    color: '#82838e',
+    color: theme.palette.milvusGrey.dark,
     fontWeight: 'bold',
     letterSpacing: '-0.02em',
   },

+ 63 - 0
client/src/components/code/CodeBlock.tsx

@@ -0,0 +1,63 @@
+import { makeStyles, Theme } from '@material-ui/core';
+import { useTranslation } from 'react-i18next';
+import CopyButton from '../advancedSearch/CopyButton';
+import SyntaxHighlighter from 'react-syntax-highlighter';
+import { docco } from 'react-syntax-highlighter/dist/esm/styles/hljs';
+import { FC } from 'react';
+import { CodeBlockProps } from './Types';
+
+const getStyles = makeStyles((theme: Theme) => ({
+  wrapper: {
+    position: 'relative',
+    padding: theme.spacing(3),
+    borderRadius: 8,
+    backgroundColor: '#fff',
+    color: '#454545',
+  },
+  block: {
+    margin: 0,
+  },
+  copy: {
+    position: 'absolute',
+    top: theme.spacing(2),
+    right: theme.spacing(2),
+  },
+}));
+
+const CodeStyle = {
+  backgroundColor: '#fff',
+  padding: 0,
+  margin: 0,
+  marginRight: 32,
+  fontSize: 14,
+};
+
+const CodeBlock: FC<CodeBlockProps> = ({
+  code,
+  language,
+  wrapperClass = '',
+}) => {
+  const classes = getStyles();
+
+  const { t: commonTrans } = useTranslation();
+  const copyTrans = commonTrans('copy');
+
+  return (
+    <div className={`${classes.wrapper} ${wrapperClass}`}>
+      <CopyButton
+        className={classes.copy}
+        label={copyTrans.label}
+        value={code}
+      />
+      <SyntaxHighlighter
+        language={language}
+        style={docco}
+        customStyle={CodeStyle}
+      >
+        {code}
+      </SyntaxHighlighter>
+    </div>
+  );
+};
+
+export default CodeBlock;

+ 113 - 0
client/src/components/code/CodeView.tsx

@@ -0,0 +1,113 @@
+import { makeStyles, Theme, Typography } from '@material-ui/core';
+import { FC } from 'react';
+import { useTranslation } from 'react-i18next';
+import CustomTabList from '../customTabList/CustomTabList';
+import { ITab } from '../customTabList/Types';
+import CodeBlock from './CodeBlock';
+import { CodeViewProps } from './Types';
+
+const getStyles = makeStyles((theme: Theme) => ({
+  wrapper: {
+    boxSizing: 'border-box',
+    width: '100%',
+
+    padding: theme.spacing(4),
+    backgroundColor: theme.palette.milvusDark.main,
+    borderRadius: 8,
+
+    color: '#fff',
+  },
+  title: {
+    marginBottom: theme.spacing(2),
+  },
+
+  // override tab list style
+  tabs: {
+    minHeight: 0,
+
+    '& .MuiTab-wrapper': {
+      textTransform: 'uppercase',
+      fontWeight: 'bold',
+      color: '#fff',
+    },
+
+    '& .MuiTab-root': {
+      minHeight: 18,
+      marginRight: 0,
+    },
+
+    // disable Ripple Effect
+    '& .MuiTouchRipple-root': {
+      display: 'none',
+    },
+
+    '& .Mui-selected': {
+      '& .MuiTab-wrapper': {
+        color: theme.palette.primary.main,
+      },
+    },
+
+    '& .MuiTabs-indicator': {
+      display: 'flex',
+      justifyContent: 'center',
+
+      top: 32,
+      backgroundColor: 'transparent',
+
+      '& .tab-indicator': {
+        height: 1,
+        width: '100%',
+        maxWidth: 26,
+        backgroundColor: theme.palette.primary.main,
+      },
+    },
+
+    '& .MuiTabs-flexContainer': {
+      borderBottom: 'none',
+    },
+  },
+
+  block: {
+    /**
+     * container height minus:
+     * 1. CodeView padding top and bottom (32 * 2)
+     * 2. CodeBlock padding top and bottom (24 * 2)
+     * 3. title height and margin bottom (24 + 16)
+     * 4. tab title height and margin bottom (36 + 16)
+     */
+    height: (props: { height: number }) =>
+      props.height - 32 * 2 - 24 * 2 - (24 + 16) - (36 + 16),
+    overflowY: 'auto',
+  },
+}));
+
+const CodeView: FC<CodeViewProps> = ({
+  wrapperClass = '',
+  data,
+  height = 0,
+}) => {
+  const classes = getStyles({ height });
+  const { t: commonTrans } = useTranslation();
+
+  const tabs: ITab[] = data.map(item => ({
+    label: item.label,
+    component: (
+      <CodeBlock
+        wrapperClass={height !== 0 ? classes.block : ''}
+        language={item.language}
+        code={item.code}
+      />
+    ),
+  }));
+
+  return (
+    <section className={`${classes.wrapper} ${wrapperClass}`}>
+      <Typography variant="h5" className={classes.title}>
+        {commonTrans('code')}
+      </Typography>
+      <CustomTabList tabs={tabs} wrapperClass={classes.tabs} />
+    </section>
+  );
+};
+
+export default CodeView;

+ 20 - 0
client/src/components/code/Types.ts

@@ -0,0 +1,20 @@
+export interface CodeViewProps {
+  height?: number;
+  wrapperClass?: string;
+  data: CodeViewData[];
+}
+
+export enum CodeLanguageEnum {
+  javascript = 'javascript',
+  python = 'python',
+}
+
+export interface CodeBlockProps {
+  code: string;
+  language: CodeLanguageEnum;
+  wrapperClass?: string;
+}
+
+export interface CodeViewData extends CodeBlockProps {
+  label: string;
+}

+ 0 - 55
client/src/components/copy/Copy.tsx

@@ -1,55 +0,0 @@
-import { IconButton, makeStyles } from '@material-ui/core';
-import { useState } from 'react';
-import React from 'react';
-import { useTranslation } from 'react-i18next';
-import { copyToCommand } from '../../utils/Common';
-import CustomToolTip from '../customToolTip/CustomToolTip';
-import Icons from '../icons/Icons';
-
-const useStyles = makeStyles(theme => ({
-  copy: {
-    cursor: 'pointer',
-    '& svg': {
-      fontSize: '12.8px',
-    },
-  },
-}));
-
-let timer: null | NodeJS.Timeout = null;
-const Copy = (props: { data: any }) => {
-  const classes = useStyles();
-  const { data } = props;
-  const { t } = useTranslation();
-  const copyTrans = t('copy') as any;
-  const [title, setTitle] = useState(copyTrans.copy);
-
-  const handleCopy = (
-    e: React.MouseEvent<HTMLButtonElement, MouseEvent>,
-    data: string
-  ) => {
-    if (timer) {
-      clearTimeout(timer);
-    }
-    e.stopPropagation();
-
-    const cb = () => {
-      setTitle(copyTrans.copied);
-      setTimeout(() => {
-        setTitle(copyTrans.copy);
-      }, 1100);
-    };
-    timer = setTimeout(() => {
-      copyToCommand(data, '', cb);
-    }, 200);
-  };
-
-  return (
-    <CustomToolTip leaveDelay={900} title={title} placement="top">
-      <IconButton className={classes.copy} onClick={e => handleCopy(e, data)}>
-        {Icons.copy()}
-      </IconButton>
-    </CustomToolTip>
-  );
-};
-
-export default Copy;

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

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

+ 27 - 7
client/src/components/customButton/CustomIconButton.tsx

@@ -1,20 +1,40 @@
-import { IconButtonProps, Tooltip, IconButton } from '@material-ui/core';
+import {
+  IconButtonProps,
+  Tooltip,
+  IconButton,
+  makeStyles,
+  Theme,
+} from '@material-ui/core';
+
+const getStyles = makeStyles((theme: Theme) => ({
+  wrapper: {
+    display: 'inline-block',
+  },
+  iconBtn: {
+    padding: theme.spacing(1),
+  },
+}));
 
 const CustomIconButton = (props: IconButtonProps & { tooltip?: string }) => {
-  const { tooltip, ...otherProps } = props;
+  const { tooltip, className, ...otherProps } = props;
+  const classes = getStyles();
 
   return (
-    <>
+    <div className={`${classes.wrapper} ${className}`}>
       {tooltip ? (
-        <Tooltip title={tooltip}>
+        <Tooltip title={tooltip} arrow>
           <span>
-            <IconButton {...otherProps}>{props.children}</IconButton>
+            <IconButton classes={{ root: classes.iconBtn }} {...otherProps}>
+              {props.children}
+            </IconButton>
           </span>
         </Tooltip>
       ) : (
-        <IconButton {...otherProps}>{props.children}</IconButton>
+        <IconButton classes={{ root: classes.iconBtn }} {...otherProps}>
+          {props.children}
+        </IconButton>
       )}
-    </>
+    </div>
   );
 };
 

+ 11 - 11
client/src/components/customDialog/CustomDialog.tsx

@@ -16,34 +16,34 @@ import CustomDialogTitle from './CustomDialogTitle';
 const useStyles = makeStyles((theme: Theme) =>
   createStyles({
     paper: {
-      // maxWidth: '480px',
-      minWidth: '480px',
-      // width: '100%',
-      borderRadius: '8px',
+      minWidth: 480,
+      borderRadius: 8,
       padding: 0,
+
+      backgroundColor: 'transparent',
     },
     noticePaper: {
-      maxWidth: '480px',
+      backgroundColor: '#fff',
+      maxWidth: 480,
     },
     paperSm: {
       maxWidth: '80%',
     },
     dialogContent: {
-      marginTop: theme.spacing(4),
+      marginTop: theme.spacing(2),
     },
     title: {
-      // padding: theme.spacing(4),
       '& p': {
-        fontWeight: '500',
+        fontWeight: 500,
         overflow: 'hidden',
         textOverflow: 'ellipsis',
         whiteSpace: 'nowrap',
-        maxWidth: '300px',
-        fontSize: '20px',
+        maxWidth: 300,
+        fontSize: 20,
       },
     },
     padding: {
-      padding: theme.spacing(4),
+      padding: theme.spacing(3, 4, 4),
     },
     cancel: {
       color: theme.palette.common.black,

+ 21 - 8
client/src/components/customDialog/CustomDialogTitle.tsx

@@ -14,12 +14,12 @@ const getStyles = makeStyles((theme: Theme) => ({
     justifyContent: 'space-between',
     alignItems: 'center',
   },
-  // closeButton: {
-  //   padding: theme.spacing(1),
-  // },
+  title: {
+    fontWeight: 500,
+  },
   icon: {
     fontSize: '24px',
-    color: '#010e29',
+    color: theme.palette.milvusDark.main,
 
     cursor: 'pointer',
   },
@@ -27,10 +27,17 @@ const getStyles = makeStyles((theme: Theme) => ({
 
 interface IProps extends DialogTitleProps {
   onClose?: () => void;
+  showCloseIcon?: boolean;
 }
 
 const CustomDialogTitle = (props: IProps) => {
-  const { children, classes = { root: '' }, onClose, ...other } = props;
+  const {
+    children,
+    classes = { root: '' },
+    onClose,
+    showCloseIcon = true,
+    ...other
+  } = props;
   const innerClass = getStyles();
 
   const ClearIcon = icons.clear;
@@ -41,9 +48,15 @@ const CustomDialogTitle = (props: IProps) => {
       className={`${innerClass.root} ${classes.root}`}
       {...other}
     >
-      <Typography variant="h5">{children}</Typography>
-      {onClose ? (
-        <ClearIcon classes={{ root: innerClass.icon }} onClick={onClose} />
+      <Typography variant="h4" className={innerClass.title}>
+        {children}
+      </Typography>
+      {showCloseIcon && onClose ? (
+        <ClearIcon
+          data-testid="clear-icon"
+          classes={{ root: innerClass.icon }}
+          onClick={onClose}
+        />
       ) : null}
     </MuiDialogTitle>
   );

+ 10 - 4
client/src/components/customDialog/DeleteDialogTemplate.tsx

@@ -16,6 +16,7 @@ import { rootContext } from '../../context/Root';
 const useStyles = makeStyles((theme: Theme) => ({
   root: {
     maxWidth: '480px',
+    backgroundColor: '#fff',
   },
   mb: {
     marginBottom: theme.spacing(2.5),
@@ -33,12 +34,12 @@ const useStyles = makeStyles((theme: Theme) => ({
     padding: '10px 12px',
   },
   cancelBtn: {
-    color: '#82838e',
+    color: theme.palette.milvusGrey.dark,
   },
 }));
 
 const DeleteTemplate: FC<DeleteDialogContentType> = props => {
-  const { title, text, label, handleDelete, handleCancel = () => {} } = props;
+  const { title, text, label, handleDelete, handleCancel } = props;
   const { handleCloseDialog } = useContext(rootContext);
   const classes = useStyles();
   const { t: dialogTrans } = useTranslation('dialog');
@@ -49,7 +50,7 @@ const DeleteTemplate: FC<DeleteDialogContentType> = props => {
 
   const onCancelClick = () => {
     handleCloseDialog();
-    handleCancel();
+    handleCancel && handleCancel();
   };
 
   const onDeleteClick = () => {
@@ -97,7 +98,11 @@ const DeleteTemplate: FC<DeleteDialogContentType> = props => {
       </DialogContent>
 
       <DialogActions className={classes.btnWrapper}>
-        <CustomButton onClick={onCancelClick} className={classes.cancelBtn}>
+        <CustomButton
+          name="cancel"
+          onClick={onCancelClick}
+          className={classes.cancelBtn}
+        >
           {btnTrans('cancel')}
         </CustomButton>
         <CustomButton
@@ -105,6 +110,7 @@ const DeleteTemplate: FC<DeleteDialogContentType> = props => {
           onClick={onDeleteClick}
           color="secondary"
           disabled={!deleteReady}
+          name="delete"
         >
           {label}
         </CustomButton>

+ 86 - 19
client/src/components/customDialog/DialogTemplate.tsx

@@ -1,4 +1,4 @@
-import { FC } from 'react';
+import { FC, useEffect, useRef, useState } from 'react';
 import { useTranslation } from 'react-i18next';
 import {
   DialogContent,
@@ -9,45 +9,112 @@ import {
 import { DialogContainerProps } from './Types';
 import CustomDialogTitle from './CustomDialogTitle';
 import CustomButton from '../customButton/CustomButton';
+import CodeView from '../code/CodeView';
 
 const useStyles = makeStyles((theme: Theme) => ({
+  wrapper: {
+    display: 'flex',
+  },
+  block: {
+    borderRadius: 8,
+    backgroundColor: '#fff',
+  },
+  dialog: {
+    minWidth: 480,
+  },
+  codeWrapper: {
+    width: (props: { showCode: boolean }) => (props.showCode ? 480 : 0),
+    transition: 'width 0.2s',
+  },
+  code: {
+    height: '100%',
+    // set code view padding 0 if not show
+    padding: (props: { showCode: boolean }) =>
+      props.showCode ? theme.spacing(4) : 0,
+  },
   actions: {
     paddingTop: theme.spacing(2),
+    justifyContent: 'space-between',
   },
 }));
 
 const DialogTemplate: FC<DialogContainerProps> = ({
   title,
   cancelLabel,
+  handleClose,
   handleCancel,
   confirmLabel,
   handleConfirm,
   confirmDisabled,
   children,
+  showActions = true,
+  showCancel = true,
+  showCloseIcon = true,
+  leftActions,
+  // needed for code mode
+  showCode = false,
+  codeBlocksData = [],
 }) => {
   const { t } = useTranslation('btn');
   const cancel = cancelLabel || t('cancel');
   const confirm = confirmLabel || t('confirm');
-  const classes = useStyles();
+  const classes = useStyles({ showCode });
+  const onCancel = handleCancel || handleClose;
+
+  const dialogRef = useRef(null);
+  const [dialogHeight, setDialogHeight] = useState<number>(0);
+
+  /**
+   * code mode height should not over original dialog height
+   * everytime children change, should recalculate dialog height
+   */
+  useEffect(() => {
+    if (dialogRef.current) {
+      const height = (dialogRef.current as any).offsetHeight;
+      setDialogHeight(height);
+    }
+  }, [children]);
 
   return (
-    <>
-      <CustomDialogTitle onClose={handleCancel}>{title}</CustomDialogTitle>
-      <DialogContent>{children}</DialogContent>
-      <DialogActions className={classes.actions}>
-        <CustomButton onClick={handleCancel} color="default">
-          {cancel}
-        </CustomButton>
-        <CustomButton
-          variant="contained"
-          onClick={handleConfirm}
-          color="primary"
-          disabled={confirmDisabled}
-        >
-          {confirm}
-        </CustomButton>
-      </DialogActions>
-    </>
+    <section className={classes.wrapper}>
+      <div ref={dialogRef} className={`${classes.dialog} ${classes.block}`}>
+        <CustomDialogTitle onClose={handleClose} showCloseIcon={showCloseIcon}>
+          {title}
+        </CustomDialogTitle>
+        <DialogContent>{children}</DialogContent>
+        {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>
+        )}
+      </div>
+
+      <div className={`${classes.block} ${classes.codeWrapper}`}>
+        {showCode && (
+          <CodeView
+            height={dialogHeight}
+            wrapperClass={classes.code}
+            data={codeBlocksData}
+          />
+        )}
+      </div>
+    </section>
   );
 };
 

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

@@ -1,4 +1,6 @@
+import { ReactElement } from 'react';
 import { DialogType } from '../../context/Types';
+import { CodeViewData } from '../code/Types';
 export type CustomDialogType = DialogType & {
   onClose: () => void;
   containerClass?: string;
@@ -21,9 +23,17 @@ export type DeleteDialogContentType = {
 
 export type DialogContainerProps = {
   title: string;
-  cancelLabel?: string;
-  confirmLabel?: string;
-  handleCancel: () => void;
+  cancelLabel?: string | ReactElement;
+  confirmLabel?: string | ReactElement;
+  showCloseIcon?: boolean;
+  handleClose: () => void;
+  handleCancel?: () => void;
   handleConfirm: (param: any) => void;
   confirmDisabled?: boolean;
+  showActions?: boolean;
+  showCancel?: boolean;
+  leftActions?: ReactElement;
+  // code mode requirement
+  showCode?: boolean;
+  codeBlocksData?: CodeViewData[];
 };

+ 5 - 3
client/src/components/customInput/CustomInput.tsx

@@ -65,7 +65,7 @@ const handleOnChange = (param: IChangeParam) => {
 
 const getAdornmentStyles = makeStyles(theme => ({
   icon: {
-    color: '#82838e',
+    color: theme.palette.milvusGrey.dark,
   },
 }));
 
@@ -217,6 +217,7 @@ const getTextfield = (
           ? { ...inputProps, ...defaultInputProps }
           : { ...defaultInputProps }
       }
+      error={info?.result && info.errText !== ''}
       InputProps={InputProps ? { ...InputProps } : {}}
       helperText={
         info && info.result && info.errText
@@ -247,6 +248,7 @@ const getStyles = makeStyles(theme => ({
     wordWrap: 'break-word',
     wordBreak: 'break-all',
     overflow: 'hidden',
+    marginLeft: '12px',
   },
   errBtn: {
     marginRight: `${theme.spacing(1)}`,
@@ -257,12 +259,12 @@ const createHelperTextNode = (hint: string): ReactElement => {
   const classes = getStyles();
   return (
     <span className={classes.errWrapper}>
-      {Icons.error({
+      {/* {Icons.error({
         fontSize: 'small',
         classes: {
           root: classes.errBtn,
         },
-      })}
+      })} */}
       {hint}
     </span>
   );

+ 94 - 37
client/src/components/customInput/SearchInput.tsx

@@ -1,5 +1,7 @@
 import { InputAdornment, makeStyles, TextField } from '@material-ui/core';
-import { useRef, FC, useState } from 'react';
+import { useRef, FC, useState, useEffect, useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useHistory } from 'react-router-dom';
 import Icons from '../icons/Icons';
 import { SearchType } from './Types';
 
@@ -9,10 +11,12 @@ const useSearchStyles = makeStyles(theme => ({
   },
   input: {
     backgroundColor: '#fff',
-    borderRadius: '4px 4px 0 0',
-    padding: (props: any) => `${props.showInput ? theme.spacing(0, 1) : 0}`,
-    boxSizing: 'border-box',
-    width: (props: any) => `${props.showInput ? '255px' : 0}`,
+    borderRadius: '4px',
+    padding: theme.spacing(1),
+    width: '240px',
+    border: '1px solid #e9e9ed',
+    fontSize: '14px',
+
     transition: 'all 0.2s',
 
     '& .MuiAutocomplete-endAdornment': {
@@ -26,73 +30,127 @@ const useSearchStyles = makeStyles(theme => ({
     '& .MuiInput-underline:after': {
       border: 'none',
     },
+
+    /**
+     * when input focus
+     * 1. change parent wrapper border color
+     * 2. hide input start search icon
+     */
+    '&:focus-within': {
+      border: `1px solid ${theme.palette.primary.main}`,
+
+      '& $searchIcon': {
+        width: 0,
+      },
+    },
+  },
+  textfield: {
+    padding: 0,
+    height: '16px',
+
+    '&:focus': {
+      caretColor: theme.palette.primary.main,
+    },
   },
   searchIcon: {
-    paddingLeft: theme.spacing(1),
-    color: theme.palette.primary.main,
+    color: '#aeaebb',
     cursor: 'pointer',
-    fontSize: '24px',
+    fontSize: '20px',
+    width: (props: { searched: boolean }) => `${props.searched ? 0 : '20px'}`,
+
+    transition: 'width 0.2s',
   },
   clearIcon: {
-    color: 'rgba(0, 0, 0, 0.6)',
+    color: theme.palette.primary.main,
     cursor: 'pointer',
   },
   iconWrapper: {
-    opacity: (props: any) => `${props.searched ? 1 : 0}`,
+    opacity: (props: { searched: boolean }) => `${props.searched ? 1 : 0}`,
     transition: 'opacity 0.2s',
   },
   searchWrapper: {
-    display: (props: any) => `${props.showInput ? 'flex' : 'none'}`,
+    display: 'flex',
     justifyContent: 'center',
     alignItems: 'center',
   },
 }));
 
+let timer: NodeJS.Timeout | null = null;
+
 const SearchInput: FC<SearchType> = props => {
   const { searchText = '', onClear = () => {}, onSearch = () => {} } = props;
-  const [searchValue, setSearchValue] = useState<string>(searchText);
-  const searched = searchValue !== '';
-  const [showInput, setShowInput] = useState<boolean>(searchValue !== '');
+  const [searchValue, setSearchValue] = useState<string | null>(
+    searchText || null
+  );
+
+  const [isInit, setIsInit] = useState<boolean>(true);
+
+  const searched = useMemo(
+    () => searchValue !== '' && searchValue !== null,
+    [searchValue]
+  );
+
+  const classes = useSearchStyles({ searched });
+  const { t: commonTrans } = useTranslation();
 
-  const classes = useSearchStyles({ searched, showInput });
+  const history = useHistory();
 
   const inputRef = useRef<any>(null);
 
-  const onIconClick = () => {
-    setShowInput(true);
-    if (inputRef.current) {
-      inputRef.current.focus();
+  const savedSearchFn = useRef<(value: string) => void>(() => {});
+  useEffect(() => {
+    savedSearchFn.current = onSearch;
+  }, [onSearch]);
+
+  useEffect(() => {
+    if (timer) {
+      clearTimeout(timer);
     }
+    if (searchValue !== null && !isInit) {
+      timer = setTimeout(() => {
+        // save other params data and remove last time search info
+        const location = history.location;
+        const params = new URLSearchParams(location.search);
+        params.delete('search');
+
+        if (searchValue) {
+          params.append('search', searchValue);
+        }
+        // add search value in url
+        history.push({ search: params.toString() });
 
-    if (searched) {
-      onSearch(searchValue);
+        savedSearchFn.current(searchValue);
+      }, 300);
     }
-  };
 
-  const handleInputBlur = () => {
-    setShowInput(searched);
+    return () => {
+      timer && clearTimeout(timer);
+    };
+  }, [searchValue, history, isInit]);
+
+  const handleSearch = (value: string | null) => {
+    if (value !== null) {
+      onSearch(value);
+    }
   };
 
   return (
     <div className={classes.wrapper}>
-      {!showInput && (
-        <div className={classes.searchIcon} onClick={onIconClick}>
-          {Icons.search()}
-        </div>
-      )}
       <TextField
         inputRef={inputRef}
-        autoFocus={true}
         variant="standard"
         classes={{ root: classes.input }}
         InputProps={{
           disableUnderline: true,
+          classes: { input: classes.textfield },
           endAdornment: (
             <InputAdornment position="end">
               <span
+                data-testid="clear-icon"
                 className={`flex-center ${classes.iconWrapper}`}
                 onClick={e => {
                   setSearchValue('');
+                  setIsInit(false);
                   inputRef.current.focus();
                   onClear();
                 }}
@@ -105,31 +163,30 @@ const SearchInput: FC<SearchType> = props => {
             <InputAdornment position="start">
               <span
                 className={classes.searchWrapper}
-                onClick={() => onSearch(searchValue)}
+                onClick={() => handleSearch(searchValue)}
               >
                 {Icons.search({ classes: { root: classes.searchIcon } })}
               </span>
             </InputAdornment>
           ),
         }}
-        onBlur={handleInputBlur}
         onChange={e => {
-          // console.log('change', e.target.value);
-          const value = e.target.value;
+          const value = e.target.value.trim();
           setSearchValue(value);
+          setIsInit(false);
           if (value === '') {
             onClear();
           }
         }}
         onKeyPress={e => {
-          // console.log(`Pressed keyCode ${e.key}`);
           if (e.key === 'Enter') {
             // Do code here
-            onSearch(searchValue);
+            handleSearch(searchValue);
             e.preventDefault();
           }
         }}
-        value={searchValue}
+        value={searchValue || ''}
+        placeholder={commonTrans('search')}
       />
     </div>
   );

+ 1 - 0
client/src/components/customInput/Types.ts

@@ -95,6 +95,7 @@ export interface IAdornmentConfig {
 
 export type SearchType = {
   searchText?: string;
+  placeholder?: string;
   onClear?: () => void;
   onSearch: (value: string) => void;
 };

+ 64 - 0
client/src/components/customProgress/CustomLinearProgress.tsx

@@ -0,0 +1,64 @@
+import {
+  makeStyles,
+  withStyles,
+  Theme,
+  LinearProgress,
+  Tooltip,
+  Typography,
+} from '@material-ui/core';
+import { FC } from 'react';
+import { CustomLinearProgressProps } from './Types';
+
+const getProgressStyles = makeStyles((theme: Theme) => ({
+  wrapper: {
+    display: 'flex',
+    alignItems: 'center',
+  },
+  percent: {
+    minWidth: '35px',
+    marginLeft: theme.spacing(1),
+    color: theme.palette.milvusDark.main,
+  },
+}));
+
+const BorderLinearProgress = withStyles((theme: Theme) => ({
+  root: {
+    height: 10,
+    borderRadius: 8,
+    border: '1px solid #e9e9ed',
+    minWidth: 85,
+  },
+  colorPrimary: {
+    backgroundColor: '#fff',
+  },
+  bar: {
+    borderRadius: 5,
+    backgroundColor: theme.palette.primary.main,
+  },
+}))(LinearProgress);
+
+const CustomLinearProgress: FC<CustomLinearProgressProps> = ({
+  value,
+  tooltip = '',
+  wrapperClass = '',
+}) => {
+  const classes = getProgressStyles();
+
+  return (
+    <div className={`${classes.wrapper} ${wrapperClass}`}>
+      {tooltip !== '' ? (
+        <Tooltip title={tooltip} aria-label={tooltip} arrow>
+          <BorderLinearProgress variant="determinate" value={value} />
+        </Tooltip>
+      ) : (
+        <BorderLinearProgress variant="determinate" value={value} />
+      )}
+      <Typography
+        variant="body1"
+        className={classes.percent}
+      >{`${value}%`}</Typography>
+    </div>
+  );
+};
+
+export default CustomLinearProgress;

+ 6 - 0
client/src/components/customProgress/Types.ts

@@ -0,0 +1,6 @@
+export interface CustomLinearProgressProps {
+  tooltip?: string;
+  // percentage, e.g. 50 means complete 50%
+  value: number;
+  wrapperClass?: string;
+}

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

@@ -1,39 +1,34 @@
 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 { 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.
  * */
 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 selectorClasses = useStyles();
 
   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
+        classes={classes}
         {...others}
         value={value}
         onChange={onChange}

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

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

+ 33 - 0
client/src/components/customSwitch/CustomSwitch.tsx

@@ -0,0 +1,33 @@
+import { FormControlLabel, makeStyles, Switch, Theme } from '@material-ui/core';
+import { FC } from 'react';
+import { useTranslation } from 'react-i18next';
+import { CustomSwitchProps } from './Types';
+
+const getStyles = makeStyles((theme: Theme) => ({
+  label: {
+    color: '#757575',
+  },
+
+  placement: {
+    marginLeft: 0,
+  },
+}));
+
+const CustomSwitch: FC<CustomSwitchProps> = ({ onChange }) => {
+  const classes = getStyles();
+  const { t: commonTrans } = useTranslation();
+
+  return (
+    <FormControlLabel
+      classes={{
+        label: classes.label,
+        labelPlacementStart: classes.placement,
+      }}
+      label={commonTrans('view')}
+      labelPlacement="start"
+      control={<Switch color="primary" onChange={onChange} />}
+    />
+  );
+};
+
+export default CustomSwitch;

+ 3 - 0
client/src/components/customSwitch/Types.ts

@@ -0,0 +1,3 @@
+export interface CustomSwitchProps {
+  onChange: (event: React.ChangeEvent<{ checked: boolean }>) => void;
+}

+ 5 - 2
client/src/components/customTabList/CustomTabList.tsx

@@ -22,6 +22,7 @@ const useStyles = makeStyles((theme: Theme) => ({
     marginRight: theme.spacing(3),
   },
   tabPanel: {
+    flexBasis: 0,
     flexGrow: 1,
     marginTop: theme.spacing(2),
   },
@@ -52,7 +53,7 @@ const a11yProps = (index: number) => {
 };
 
 const CustomTabList: FC<ITabListProps> = props => {
-  const { tabs, activeIndex = 0, handleTabChange } = props;
+  const { tabs, activeIndex = 0, handleTabChange, wrapperClass = '' } = props;
   const classes = useStyles();
   const [value, setValue] = useState<number>(activeIndex);
 
@@ -66,10 +67,12 @@ const CustomTabList: FC<ITabListProps> = props => {
     <>
       <Tabs
         classes={{
-          root: classes.wrapper,
+          root: `${classes.wrapper} ${wrapperClass}`,
           indicator: classes.tab,
           flexContainer: classes.tabContainer,
         }}
+        // if not provide this property, Material will add single span element by default
+        TabIndicatorProps={{ children: <div className="tab-indicator" /> }}
         value={value}
         onChange={handleChange}
         aria-label="tabs"

+ 1 - 0
client/src/components/customTabList/Types.ts

@@ -9,6 +9,7 @@ export interface ITabListProps {
   tabs: ITab[];
   activeIndex?: number;
   handleTabChange?: (index: number) => void;
+  wrapperClass?: string;
 }
 
 export interface ITabPanel {

+ 14 - 3
client/src/components/grid/index.tsx → client/src/components/grid/Grid.tsx

@@ -73,8 +73,11 @@ const userStyle = makeStyles(theme => ({
 
 const MilvusGrid: FC<MilvusGridType> = props => {
   const classes = userStyle();
-  const { t } = useTranslation();
-  const gridTrans = t('grid') as any;
+
+  // i18n
+  const { t: commonTrans } = useTranslation();
+  const gridTrans = commonTrans('grid');
+
   const {
     rowCount = 10,
     rowsPerPage = 5,
@@ -98,10 +101,14 @@ const MilvusGrid: FC<MilvusGridType> = props => {
     searchForm,
     openCheckBox = true,
     disableSelect = false,
-    noData = t('grid.noData'),
+    noData = gridTrans.noData,
     showHoverStyle = true,
+    headEditable = false,
+    editHeads = [],
     selected = [],
     setSelected = () => {},
+    setRowsPerPage = () => {},
+    tableCellMaxWidth,
   } = props;
 
   const _isSelected = (row: { [x: string]: any }) => {
@@ -206,6 +213,10 @@ const MilvusGrid: FC<MilvusGridType> = props => {
           noData={noData}
           showHoverStyle={showHoverStyle}
           isLoading={isLoading}
+          setPageSize={setRowsPerPage}
+          headEditable={headEditable}
+          editHeads={editHeads}
+          tableCellMaxWidth={tableCellMaxWidth}
         ></Table>
         {rowCount ? (
           <TablePagination

+ 73 - 27
client/src/components/grid/Table.tsx

@@ -10,17 +10,19 @@ import Checkbox from '@material-ui/core/Checkbox';
 import { TableType } from './Types';
 import { Box, Button, Typography } from '@material-ui/core';
 import EnhancedTableHead from './TableHead';
+import EditableTableHead from './TableEditableHead';
 import { stableSort, getComparator } from './Utils';
-import Copy from '../../components/copy/Copy';
 import ActionBar from './ActionBar';
 import LoadingTable from './LoadingTable';
+import CopyButton from '../advancedSearch/CopyButton';
+import { useTranslation } from 'react-i18next';
 
 const useStyles = makeStyles(theme => ({
   root: {
-    // minHeight: '29vh',
     width: '100%',
     flexGrow: 1,
-    // flexBasis: 0,
+    /* set flex basis to make child item height 100% work on Safari */
+    flexBasis: 0,
 
     // change scrollbar style
     '&::-webkit-scrollbar': {
@@ -78,22 +80,19 @@ const useStyles = makeStyles(theme => ({
     },
   },
   cell: {
+    borderBottom: '1px solid #e9e9ed',
+
     '& p': {
       display: 'inline-block',
       verticalAlign: 'middle',
       overflow: 'hidden',
       textOverflow: 'ellipsis',
       whiteSpace: 'nowrap',
-      maxWidth: '300px',
-      fontSize: '12.8px',
-    },
-    '& button': {
-      textTransform: 'inherit',
-      justifyContent: 'flex-start',
+      maxWidth: (props: { tableCellMaxWidth: string }) =>
+        props.tableCellMaxWidth,
+      fontSize: '14px',
+      lineHeight: '20px',
     },
-    // '& svg': {
-    //   color: 'rgba(0, 0, 0, 0.33)',
-    // },
   },
   noData: {
     paddingTop: theme.spacing(6),
@@ -101,6 +100,9 @@ const useStyles = makeStyles(theme => ({
     letterSpacing: '0.5px',
     color: 'rgba(0, 0, 0, 0.6)',
   },
+  copyBtn: {
+    marginLeft: theme.spacing(0.5),
+  },
 }));
 
 const EnhancedTable: FC<TableType> = props => {
@@ -112,13 +114,23 @@ const EnhancedTable: FC<TableType> = props => {
     rows = [],
     colDefinitions,
     primaryKey,
+    // whether show checkbox in the first column
+    // set true as default
     openCheckBox = true,
     disableSelect,
     noData,
-    showHoverStyle,
+    // whether change table row background color when mouse hover
+    // set true as default
+    showHoverStyle = true,
     isLoading,
+    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;
-  const classes = useStyles();
+  const classes = useStyles({ tableCellMaxWidth });
   const [order, setOrder] = React.useState('asc');
   const [orderBy, setOrderBy] = React.useState<string>('');
   const [tableMouseStatus, setTableMouseStatus] = React.useState<boolean[]>([]);
@@ -126,6 +138,9 @@ const EnhancedTable: FC<TableType> = props => {
 
   const containerRef = useRef(null);
 
+  const { t: commonTrans } = useTranslation();
+  const copyTrans = commonTrans('copy');
+
   const handleRequestSort = (event: any, property: string) => {
     const isAsc = orderBy === property && order === 'asc';
     setOrder(isAsc ? 'desc' : 'asc');
@@ -139,6 +154,25 @@ const EnhancedTable: FC<TableType> = props => {
     setLoadingRowCount(count);
   }, []);
 
+  useEffect(() => {
+    if (setPageSize) {
+      const containerHeight: number = (containerRef.current as any)!
+        .offsetHeight;
+
+      // table default row height is 54
+      // if pass component as row item, its max height should be 54 too
+      const rowHeight = 54;
+      // table header default height is 57
+      const tableHeaderHeight: number = 57;
+      if (rowHeight > 0) {
+        const pageSize = Math.floor(
+          (containerHeight - tableHeaderHeight) / rowHeight
+        );
+        setPageSize(pageSize);
+      }
+    }
+  }, [setPageSize]);
+
   return (
     <TableContainer ref={containerRef} className={classes.root}>
       <Box height="100%" className={classes.box}>
@@ -149,16 +183,20 @@ const EnhancedTable: FC<TableType> = props => {
           size="medium"
           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 && (
             <TableBody>
               {rows && rows.length ? (
@@ -237,7 +275,7 @@ const EnhancedTable: FC<TableType> = props => {
                           ) : (
                             <TableCell
                               key={'cell' + row[primaryKey] + i}
-                              padding={i === 0 ? 'none' : 'default'}
+                              // padding={i === 0 ? 'none' : 'default'}
                               align={colDef.align || 'left'}
                               className={`${classes.cell} ${classes.tableCell}`}
                               style={cellStyle}
@@ -282,7 +320,11 @@ const EnhancedTable: FC<TableType> = props => {
                               )}
 
                               {needCopy && row[colDef.id] && (
-                                <Copy data={row[colDef.id]} />
+                                <CopyButton
+                                  label={copyTrans.label}
+                                  value={row[colDef.id]}
+                                  className={classes.copyBtn}
+                                />
                               )}
                             </TableCell>
                           );
@@ -295,7 +337,11 @@ const EnhancedTable: FC<TableType> = props => {
                 <tr>
                   <td
                     className={classes.noData}
-                    colSpan={colDefinitions.length}
+                    colSpan={
+                      openCheckBox
+                        ? colDefinitions.length + 1
+                        : colDefinitions.length
+                    }
                   >
                     {noData}
                   </td>

+ 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}
             align={headCell.align || 'left'}
             padding={headCell.disablePadding ? 'none' : 'default'}
-            sortDirection={orderBy === headCell.id ? order : false}
+            sortDirection={
+              orderBy === (headCell.sortBy || headCell.id) ? order : false
+            }
             className={classes.tableCell}
           >
             {headCell.label && !headCell.notSort ? (
               <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}>
                   {headCell.label}
                 </Typography>
 
-                {orderBy === headCell.id ? (
+                {orderBy === (headCell.sortBy || headCell.id) ? (
                   <Typography className={classes.visuallyHidden}>
                     {order === 'desc'
                       ? 'sorted descending'

+ 4 - 2
client/src/components/grid/TablePaginationActions.tsx

@@ -40,8 +40,10 @@ const useStyles = makeStyles((theme: Theme) =>
 const TablePaginationActions = (props: TablePaginationActionsProps) => {
   const classes = useStyles();
   const { count, page, rowsPerPage, onChangePage } = props;
-  const { t } = useTranslation();
-  const gridTrans = t('grid') as any;
+
+  // i18n
+  const { t: commonTrans } = useTranslation();
+  const gridTrans = commonTrans('grid');
 
   const handleBackButtonClick = (
     event: React.MouseEvent<HTMLButtonElement>

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

@@ -69,6 +69,9 @@ const CustomToolBar: FC<ToolBarType> = props => {
 
             const Icon = c.icon ? Icons[c.icon!]() : '';
             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 btn = (
@@ -79,8 +82,9 @@ const CustomToolBar: FC<ToolBarType> = props => {
                 startIcon={Icon}
                 color="primary"
                 disabled={disabled}
-                variant="contained"
-                tooltip={c.tooltip}
+                // use contained variant as default
+                variant={c.btnVariant || 'contained'}
+                tooltip={tooltip}
                 className={classes.btn}
               >
                 <Typography variant="button">{c.label}</Typography>
@@ -91,7 +95,7 @@ const CustomToolBar: FC<ToolBarType> = props => {
               <CustomIconButton
                 key={i}
                 onClick={c.onClick}
-                tooltip={c.tooltip}
+                tooltip={tooltip}
                 disabled={disabled}
               >
                 {Icon}
@@ -116,6 +120,7 @@ const CustomToolBar: FC<ToolBarType> = props => {
                     onClear={c.onClear}
                     onSearch={c.onSearch}
                     searchText={c.searchText}
+                    placeholder={c.placeholder}
                     key={i}
                   />
                 );

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

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

+ 26 - 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 ArrowForwardIosIcon from '@material-ui/icons/ArrowForwardIos';
 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 { ReactComponent as MilvusIcon } from '../../assets/icons/milvus.svg';
 import { ReactComponent as OverviewIcon } from '../../assets/icons/overview.svg';
@@ -26,6 +29,11 @@ import { ReactComponent as ConsoleIcon } from '../../assets/icons/console.svg';
 import { ReactComponent as InfoIcon } from '../../assets/icons/info.svg';
 import { ReactComponent as ReleaseIcon } from '../../assets/icons/release.svg';
 import { ReactComponent as LoadIcon } from '../../assets/icons/load.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 } = {
   search: (props = {}) => <SearchIcon {...props} />,
@@ -46,6 +54,9 @@ const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
   logout: (props = {}) => <ExitToAppIcon {...props} />,
   rightArrow: (props = {}) => <ArrowForwardIosIcon {...props} />,
   remove: (props = {}) => <RemoveCircleOutlineIcon {...props} />,
+  dropdown: (props = {}) => <ArrowDropDownIcon {...props} />,
+  refresh: (props = {}) => <CachedIcon {...props} />,
+  filter: (props = {}) => <FilterListIcon {...props} />,
 
   milvus: (props = {}) => (
     <SvgIcon viewBox="0 0 44 31" component={MilvusIcon} {...props} />
@@ -59,6 +70,9 @@ const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
   navConsole: (props = {}) => (
     <SvgIcon viewBox="0 0 20 20" component={ConsoleIcon} {...props} />
   ),
+  navSearch: (props = {}) => (
+    <SvgIcon viewBox="0 0 20 20" component={VectorSearchIcon} {...props} />
+  ),
   info: (props = {}) => (
     <SvgIcon viewBox="0 0 16 16" component={InfoIcon} {...props} />
   ),
@@ -68,6 +82,18 @@ const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
   load: (props = {}) => (
     <SvgIcon viewBox="0 0 24 24" component={LoadIcon} {...props} />
   ),
+  key: (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;

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

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

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

@@ -0,0 +1,406 @@
+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,
+} 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,
+    autoIdFieldName,
+  }: { schemaOptions: Option[]; autoIdFieldName: string } = useMemo(() => {
+    /**
+     * on collection page, we get schema data from collection
+     * on partition page, we pass schema as props
+     */
+    const list =
+      schema && schema.length > 0
+        ? schema
+        : collections.find(c => c._name === collectionValue)?._fields;
+
+    const autoIdFieldName =
+      list?.find(item => item._isPrimaryKey && item._isAutoId)?._fieldName ||
+      '';
+    /**
+     * if below conditions all met, this schema shouldn't be selectable as head:
+     * 1. this field is primary key
+     * 2. this field auto id is true
+     */
+    const options = (list || [])
+      .filter(s => !s._isAutoId || !s._isPrimaryKey)
+      .map(s => ({
+        label: s._fieldName,
+        value: s._fieldId,
+      }));
+    return {
+      schemaOptions: options,
+      autoIdFieldName,
+    };
+  }, [schema, collectionValue, collections]);
+
+  const checkUploadFileValidation = (firstRowItems: string[]): boolean => {
+    const uploadFieldNamesLength = firstRowItems.length;
+    return (
+      checkIsAutoIdFieldValid(firstRowItems) ||
+      checkColumnLength(uploadFieldNamesLength)
+    );
+  };
+
+  /**
+   * when primary key field auto id is true
+   * no need to upload this field data
+   * @param firstRowItems uploaded file first row items
+   * @returns whether invalid, true means invalid
+   */
+  const checkIsAutoIdFieldValid = (firstRowItems: string[]): boolean => {
+    const isContainAutoIdField = firstRowItems.includes(autoIdFieldName);
+    isContainAutoIdField &&
+      openSnackBar(
+        insertTrans('uploadAutoIdFieldWarning', { fieldName: autoIdFieldName }),
+        'error'
+      );
+    return isContainAutoIdField;
+  };
+
+  /**
+   * uploaded file column length should be equal to schema length
+   * @param fieldNamesLength every row items length
+   * @returns whether invalid, true means invalid
+   */
+  const checkColumnLength = (fieldNamesLength: number): boolean => {
+    const isLengthEqual = schemaOptions.length === fieldNamesLength;
+    // if not equal, open warning snackbar
+    !isLengthEqual &&
+      openSnackBar(insertTrans('uploadFieldNamesLenWarning'), 'error');
+    return !isLengthEqual;
+  };
+
+  const handleUploadedData = (csv: string, uploader: HTMLFormElement) => {
+    const { data } = parse(csv);
+    // if uploaded csv contains heads, firstRowItems is the list of all heads
+    const [firstRowItems = []] = data as string[][];
+
+    const invalid = checkUploadFileValidation(firstRowItems);
+    if (invalid) {
+      // 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;

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

@@ -0,0 +1,201 @@
+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, INSERT_MAX_SIZE } 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: theme.palette.milvusDark.main,
+      },
+
+      '& .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"
+            // selected collection will affect schema, which is required for uploaded data validation check
+            // so upload file should be disabled until user select one collection
+            disabled={!selectedCollection}
+            disableTooltip={insertTrans('uploadFileDisableTooltip')}
+            setFileName={setFileName}
+            handleUploadedData={handleUploadedData}
+            maxSize={parseByte(`${INSERT_MAX_SIZE}m`)}
+            overSizeWarning={insertTrans('overSizeWarning', {
+              size: INSERT_MAX_SIZE,
+            })}
+            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').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: false,
+      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;

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

@@ -0,0 +1,77 @@
+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;
+}

+ 0 - 105
client/src/components/layout/GlobalToolbar.tsx

@@ -1,105 +0,0 @@
-import { useContext } from 'react';
-import {
-  makeStyles,
-  Theme,
-  createStyles,
-  // Divider
-} from '@material-ui/core';
-import icons from '../icons/Icons';
-// import CustomButton from '../customButton/CustomButton';
-import SimpleMenu from '../menu/SimpleMenu';
-import { useTranslation } from 'react-i18next';
-import { GlobalCreateType } from './Types';
-import { StatusEnum } from '../status/Types';
-// import { rootContext } from '../../context/Root';
-// import GlobalSearch from './GlobalSearch';
-
-const useStyles = makeStyles((theme: Theme) =>
-  createStyles({
-    root: {
-      display: 'flex',
-      background: theme.palette.common.white,
-      marginBottom: theme.spacing(3),
-      marginTop: theme.spacing(3),
-      backgroundColor: 'transparent',
-    },
-    buttonWrapper: {
-      boxSizing: 'border-box',
-      padding: theme.spacing(1, 2.5),
-      backgroundColor: theme.palette.common.white,
-      width: (props: any) => props.width,
-
-      display: 'flex',
-      flexDirection: 'column',
-    },
-    button: {
-      paddingLeft: theme.spacing(1),
-      color: theme.palette.common.black,
-      marginTop: theme.spacing(1),
-    },
-    btn: {
-      width: '100%',
-      padding: theme.spacing(1, 0),
-      fontSize: '16px',
-      lineHeight: '24px',
-    },
-    breadcrumb: {
-      padding: theme.spacing(1, 2),
-      backgroundColor: theme.palette.common.white,
-      marginLeft: '2px',
-      flex: 1,
-    },
-    divider: {
-      margin: theme.spacing(1, 0),
-    },
-    container: {},
-  })
-);
-
-const GlobalToolbar = (props: { width: String }) => {
-  const { t } = useTranslation();
-  const classes = useStyles(props);
-  const { t: btnTrans } = useTranslation('btn');
-  // const navTrans: any = t('nav');
-
-  // const SearchIcon = icons.search;
-  const AddIcon = icons.add;
-
-  // const handleGlobalSearch = () => {
-  //   openDialog({
-  //     open: true,
-  //     type: 'custom',
-  //     params: {
-  //       component: <GlobalSearch options={top100Films} />,
-  //       containerClass: classes.container,
-  //     },
-  //   });
-  // };
-
-  return (
-    <div className={classes.root}>
-      <div className={classes.buttonWrapper}>
-        <SimpleMenu
-          label={btnTrans('create')}
-          menuItems={[]}
-          buttonProps={{
-            startIcon: <AddIcon />,
-            variant: 'contained',
-            className: classes.btn,
-          }}
-        ></SimpleMenu>
-
-        {/* <CustomButton
-          startIcon={<SearchIcon />}
-          variant="outlined"
-          className={classes.button}
-          onClick={handleGlobalSearch}
-        >
-          {btnTrans('search')}
-        </CustomButton> */}
-      </div>
-    </div>
-  );
-};
-
-export default GlobalToolbar;

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

@@ -61,8 +61,8 @@ const Header: FC<HeaderType> = props => {
   const { navInfo } = useContext(navContext);
   const { address, setAddress } = useContext(authContext);
   const history = useHistory();
-  const { t } = useTranslation();
-  const statusTrans: { [key in string]: string } = t('status');
+  const { t: commonTrans } = useTranslation();
+  const statusTrans = commonTrans('status');
   const BackIcon = icons.back;
   const LogoutIcon = icons.logout;
 

+ 38 - 17
client/src/components/layout/Layout.tsx

@@ -3,10 +3,10 @@ import Header from './Header';
 import { makeStyles, Theme, createStyles } from '@material-ui/core';
 import NavMenu from '../menu/NavMenu';
 import { NavMenuItem } from '../menu/Types';
-import { useContext } from 'react';
+import { useContext, useMemo } from 'react';
 import icons from '../icons/Icons';
 import { useTranslation } from 'react-i18next';
-import { useHistory } from 'react-router-dom';
+import { useHistory, useLocation } from 'react-router-dom';
 import { authContext } from '../../context/Auth';
 
 const useStyles = makeStyles((theme: Theme) =>
@@ -17,6 +17,18 @@ const useStyles = makeStyles((theme: Theme) =>
     },
     content: {
       display: 'flex',
+
+      '& .normalSearchIcon': {
+        '& path': {
+          fill: theme.palette.milvusGrey.dark,
+        },
+      },
+
+      '& .activeSearchIcon': {
+        '& path': {
+          fill: theme.palette.primary.main,
+        },
+      },
     },
     body: {
       flex: 1,
@@ -25,36 +37,45 @@ const useStyles = makeStyles((theme: Theme) =>
       height: '100vh',
       overflowY: 'scroll',
     },
-    activeConsole: {
-      '& path': {
-        fill: theme.palette.primary.main,
-      },
-    },
-    normalConsole: {
-      '& path': {
-        fill: '#82838e',
-      },
-    },
   })
 );
 
 const Layout = (props: any) => {
   const history = useHistory();
   const { isAuth } = useContext(authContext);
-  const { t } = useTranslation('nav');
+  const { t: navTrans } = useTranslation('nav');
   const classes = useStyles();
+  const location = useLocation();
+  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[] = [
     {
       icon: icons.navOverview,
-      label: t('overview'),
+      label: navTrans('overview'),
       onClick: () => history.push('/'),
     },
     {
       icon: icons.navCollection,
-      label: t('collection'),
+      label: navTrans('collection'),
       onClick: () => history.push('/collections'),
     },
+    {
+      icon: icons.navSearch,
+      label: navTrans('search'),
+      onClick: () => history.push('/search'),
+      iconActiveClass: 'activeSearchIcon',
+      iconNormalClass: 'normalSearchIcon',
+    },
   ];
 
   return (
@@ -65,9 +86,9 @@ const Layout = (props: any) => {
             <NavMenu
               width="200px"
               data={menuItems}
-              defaultActive={t('overview')}
+              defaultActive={defaultActive}
               // used for nested child menu
-              defaultOpen={{ [t('overview')]: true }}
+              defaultOpen={{ [navTrans('overview')]: true }}
             />
           )}
 

+ 126 - 57
client/src/components/menu/NavMenu.tsx

@@ -1,35 +1,51 @@
-import { useState, FC } from 'react';
+import { useState, FC, useEffect } from 'react';
+import clsx from 'clsx';
 import { makeStyles, Theme, createStyles } from '@material-ui/core/styles';
+import Fade from '@material-ui/core/Fade';
+import Button from '@material-ui/core/Button';
 import List from '@material-ui/core/List';
 import ListItem from '@material-ui/core/ListItem';
 import ListItemIcon from '@material-ui/core/ListItemIcon';
 import ListItemText from '@material-ui/core/ListItemText';
-import Collapse from '@material-ui/core/Collapse';
 import { NavMenuItem, NavMenuType } from './Types';
 import icons from '../icons/Icons';
 import { useTranslation } from 'react-i18next';
 import Typography from '@material-ui/core/Typography';
+import ChevronRightIcon from '@material-ui/icons/ChevronRight';
+
+const timeout = 150;
+const duration = `${timeout}ms`;
 
 const useStyles = makeStyles((theme: Theme) =>
   createStyles({
     root: {
-      width: (props: any) => props.width || '100%',
       background: theme.palette.common.white,
-
+      paddingTop: 0,
       paddingBottom: theme.spacing(5),
       display: 'flex',
       flexDirection: 'column',
       justifyContent: 'space-between',
+      transition: theme.transitions.create('width', {
+        duration,
+      }),
+      overflow: 'hidden',
+    },
+    rootCollapse: {
+      width: '86px',
+    },
+    rootExpand: {
+      width: (props: any) => props.width || '100%',
     },
     nested: {
       paddingLeft: theme.spacing(4),
     },
     item: {
       marginBottom: theme.spacing(2),
-      marginLeft: theme.spacing(3),
-
+      paddingLeft: theme.spacing(4),
+      boxSizing: 'content-box',
+      height: theme.spacing(3),
       width: 'initial',
-      color: '#82838e',
+      color: theme.palette.milvusGrey.dark,
     },
     itemIcon: {
       minWidth: '20px',
@@ -39,10 +55,13 @@ const useStyles = makeStyles((theme: Theme) =>
         fill: 'transparent',
 
         '& path': {
-          stroke: '#82838e',
+          stroke: theme.palette.milvusGrey.dark,
         },
       },
     },
+    itemText: {
+      whiteSpace: 'nowrap',
+    },
     active: {
       color: theme.palette.primary.main,
       borderRight: `2px solid ${theme.palette.primary.main}`,
@@ -57,48 +76,89 @@ const useStyles = makeStyles((theme: Theme) =>
     logoWrapper: {
       width: '100%',
       display: 'flex',
-      justifyContent: 'center',
       alignItems: 'center',
-
-      marginTop: theme.spacing(3),
-
+      height: '86px',
       marginBottom: theme.spacing(8),
+      backgroundColor: theme.palette.primary.main,
+      paddingLeft: theme.spacing(4),
 
       '& .title': {
         margin: 0,
         fontSize: '16px',
-        lineHeight: '19px',
         letterSpacing: '0.15px',
-        color: '#323232',
+        color: 'white',
+        whiteSpace: 'nowrap',
+        lineHeight: '86px',
       },
     },
     logo: {
+      transition: theme.transitions.create('all', {
+        duration,
+      }),
+    },
+    logoExpand: {
       marginRight: theme.spacing(1),
+      '& path': {
+        fill: 'white',
+      },
+    },
+    logoCollapse: {
+      backgroundColor: theme.palette.primary.main,
+      '& path': {
+        fill: 'white',
+      },
+      transform: 'scale(1.5)',
+    },
+    actionIcon: {
+      position: 'fixed',
+      borderRadius: '50%',
+      backgroundColor: 'white',
+      top: '74px',
+      transition: theme.transitions.create('all', {
+        duration,
+      }),
+      minWidth: '24px',
+      padding: 0,
+
+      '& svg path': {
+        fill: theme.palette.milvusGrey.dark,
+      },
+
+      '&:hover': {
+        backgroundColor: theme.palette.primary.main,
+
+        '& svg path': {
+          fill: 'white',
+        },
+      },
+    },
+    expandIcon: {
+      left: '187px',
+      transform: 'rotateZ(180deg)',
+    },
+    collapseIcon: {
+      left: '73px',
     },
   })
 );
 
 const NavMenu: FC<NavMenuType> = props => {
-  const { width, data, defaultActive = '', defaultOpen = {} } = props;
+  const { width, data, defaultActive = '' } = props;
   const classes = useStyles({ width });
-  const [open, setOpen] = useState<{ [x: string]: boolean }>(defaultOpen);
+  const [expanded, setExpanded] = useState<boolean>(false);
   const [active, setActive] = useState<string>(defaultActive);
 
-  const { t } = useTranslation();
-  const milvusTrans: { [key in string]: string } = t('milvus');
+  const { t: commonTrans } = useTranslation();
+  const milvusTrans = commonTrans('milvus');
 
-  const ExpandLess = icons.expandLess;
-  const ExpandMore = icons.expandMore;
-
-  const handleClick = (label: string) => {
-    setOpen(v => ({
-      ...v,
-      [label]: !v[label],
-    }));
-  };
+  useEffect(() => {
+    if (defaultActive) {
+      setActive(defaultActive);
+    }
+  }, [defaultActive]);
 
   const NestList = (props: { data: NavMenuItem[]; className?: string }) => {
-    const { className, data } = props;
+    const { className = '', data } = props;
     return (
       <>
         {data.map((v: NavMenuItem) => {
@@ -110,32 +170,15 @@ const NavMenu: FC<NavMenuType> = props => {
                 ? v.iconActiveClass
                 : v.iconNormalClass
               : 'icon';
-          if (v.children) {
-            return (
-              <div key={v.label}>
-                <ListItem button onClick={() => handleClick(v.label)}>
-                  <ListItemIcon>
-                    <IconComponent classes={{ root: iconClass }} />
-                  </ListItemIcon>
-
-                  <ListItemText primary={v.label} />
-                  {open[v.label] ? <ExpandLess /> : <ExpandMore />}
-                </ListItem>
-                <Collapse in={open[v.label]} timeout="auto" unmountOnExit>
-                  <List component="div" disablePadding>
-                    <NestList data={v.children} className={classes.nested} />
-                  </List>
-                </Collapse>
-              </div>
-            );
-          }
           return (
             <ListItem
               button
               key={v.label}
-              className={`${className || ''} ${classes.item} ${
-                isActive ? classes.active : ''
-              }`}
+              title={v.label}
+              className={clsx(classes.item, {
+                [className]: className,
+                [classes.active]: isActive,
+              })}
               onClick={() => {
                 setActive(v.label);
                 v.onClick && v.onClick();
@@ -145,7 +188,9 @@ const NavMenu: FC<NavMenuType> = props => {
                 <IconComponent classes={{ root: iconClass }} />
               </ListItemIcon>
 
-              <ListItemText primary={v.label} />
+              <Fade in={expanded} timeout={timeout}>
+                <ListItemText className={classes.itemText} primary={v.label} />
+              </Fade>
             </ListItem>
           );
         })}
@@ -156,15 +201,39 @@ const NavMenu: FC<NavMenuType> = props => {
   const Logo = icons.milvus;
 
   return (
-    <List component="nav" className={classes.root}>
+    <List
+      component="nav"
+      className={clsx(classes.root, {
+        [classes.rootExpand]: expanded,
+        [classes.rootCollapse]: !expanded,
+      })}
+    >
       <div>
         <div className={classes.logoWrapper}>
-          <Logo classes={{ root: classes.logo }} />
-          <Typography variant="h3" className="title">
-            {milvusTrans.admin}
-          </Typography>
+          <Logo
+            classes={{ root: classes.logo }}
+            className={clsx({
+              [classes.logoExpand]: expanded,
+              [classes.logoCollapse]: !expanded,
+            })}
+          />
+          <Fade in={expanded} timeout={timeout}>
+            <Typography variant="h3" className="title">
+              {milvusTrans.admin}
+            </Typography>
+          </Fade>
         </div>
-
+        <Button
+          onClick={() => {
+            setExpanded(!expanded);
+          }}
+          className={clsx(classes.actionIcon, {
+            [classes.expandIcon]: expanded,
+            [classes.collapseIcon]: !expanded,
+          })}
+        >
+          <ChevronRightIcon />
+        </Button>
         <NestList data={data} />
       </div>
     </List>

+ 43 - 21
client/src/components/menu/SimpleMenu.tsx

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

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

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

+ 2 - 2
client/src/components/status/Status.tsx

@@ -41,8 +41,8 @@ const useStyles = makeStyles((theme: Theme) =>
 
 const Status: FC<StatusType> = props => {
   const { status } = props;
-  const { t } = useTranslation();
-  const statusTrans: { [key in string]: string } = t('status');
+  const { t: commonTrans } = useTranslation();
+  const statusTrans = commonTrans('status');
   const { label, color } = useMemo(() => {
     switch (status) {
       case StatusEnum.unloaded:

+ 1 - 1
client/src/components/status/StatusIcon.tsx

@@ -23,7 +23,7 @@ const StatusIcon: FC<StatusIconType> = props => {
       case 'creating':
         return (
           <CircularProgress
-            size={24}
+            size={20}
             thickness={8}
             classes={{ svg: classes.svg }}
           />

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

@@ -0,0 +1,17 @@
+export interface UploaderProps {
+  label: string;
+  accept: string;
+  btnClass?: string;
+  disabled?: boolean;
+  disableTooltip?: 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;
+}

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

@@ -0,0 +1,90 @@
+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 = '',
+  disabled = false,
+  disableTooltip = '',
+  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}
+        disabled={disabled}
+        tooltip={disabled ? disableTooltip : ''}
+      >
+        {label}
+      </CustomButton>
+      <input
+        ref={inputRef}
+        id="fileId"
+        type="file"
+        accept={accept}
+        style={{ display: 'none' }}
+      />
+    </form>
+  );
+};
+
+export default Uploader;

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

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

+ 120 - 65
client/src/consts/Milvus.tsx

@@ -1,33 +1,13 @@
-export const VECTOR_TYPE_OPTIONS = [
-  {
-    label: 'Vector float',
-    value: 'VECTOR_FLOAT',
-  },
-  {
-    label: 'Vector binary',
-    value: 'VECTOR_BINARY',
-  },
-];
-
-export const NON_VECTOR_TYPE_OPTIONS = [
-  {
-    label: 'Number',
-    value: 'number',
-  },
-  {
-    label: 'Float',
-    value: 'float',
-  },
-];
+import { DataTypeEnum } from '../pages/collections/Types';
 
 export enum METRIC_TYPES_VALUES {
-  L2 = 1,
-  IP,
-  HAMMING,
-  JACCARD,
-  TANIMOTO,
-  SUBSTRUCTURE,
-  SUPERSTRUCTURE,
+  L2 = 'L2',
+  IP = 'IP',
+  HAMMING = 'HAMMING',
+  JACCARD = 'JACCARD',
+  TANIMOTO = 'TANIMOTO',
+  SUBSTRUCTURE = 'SUBSTRUCTURE',
+  SUPERSTRUCTURE = 'SUPERSTRUCTURE',
 }
 
 export const METRIC_TYPES = [
@@ -39,45 +19,48 @@ export const METRIC_TYPES = [
     value: METRIC_TYPES_VALUES.IP,
     label: 'IP',
   },
-  {
-    value: METRIC_TYPES_VALUES.HAMMING,
-    label: 'Hamming',
-  },
   {
     value: METRIC_TYPES_VALUES.SUBSTRUCTURE,
-    label: 'Substructure',
+    label: 'SUBSTRUCTURE',
   },
   {
     value: METRIC_TYPES_VALUES.SUPERSTRUCTURE,
-    label: 'Superstructure',
+    label: 'SUPERSTRUCTURE',
+  },
+  {
+    value: METRIC_TYPES_VALUES.HAMMING,
+    label: 'HAMMING',
   },
   {
     value: METRIC_TYPES_VALUES.JACCARD,
-    label: 'Jaccard',
+    label: 'JACCARD',
   },
   {
     value: METRIC_TYPES_VALUES.TANIMOTO,
-    label: 'Tanimoto',
+    label: 'TANIMOTO',
   },
 ];
 
-export const BINARY_METRIC_TYPES = [
-  'HAMMING',
-  'JACCARD',
-  'TANIMOTO',
-  'SUBSTRUCTURE',
-  'SUPERSTRUCTURE',
-];
+export type MetricType =
+  | 'L2'
+  | 'IP'
+  | 'HAMMING'
+  | 'SUBSTRUCTURE'
+  | 'SUPERSTRUCTURE'
+  | 'JACCARD'
+  | 'TANIMOTO';
 
 export type searchKeywordsType = 'nprobe' | 'ef' | 'search_k' | 'search_length';
 
-// index
-export const INDEX_CONFIG: {
+export type indexConfigType = {
   [x: string]: {
     create: string[];
     search: searchKeywordsType[];
   };
-} = {
+};
+
+// index
+export const FLOAT_INDEX_CONFIG: indexConfigType = {
   IVF_FLAT: {
     create: ['nlist'],
     search: ['nprobe'],
@@ -90,10 +73,10 @@ export const INDEX_CONFIG: {
     create: ['nlist'],
     search: ['nprobe'],
   },
-  IVF_SQ8_HYBRID: {
-    create: ['nlist'],
-    search: ['nprobe'],
-  },
+  // IVF_SQ8_HYBRID: {
+  //   create: ['nlist'],
+  //   search: ['nprobe'],
+  // },
   FLAT: {
     create: ['nlist'],
     search: ['nprobe'],
@@ -106,12 +89,29 @@ export const INDEX_CONFIG: {
     create: ['n_trees'],
     search: ['search_k'],
   },
-  RNSG: {
-    create: ['out_degree', 'candidate_pool_size', 'search_length', 'knng'],
-    search: ['search_length'],
+  // RNSG: {
+  //   create: ['out_degree', 'candidate_pool_size', 'search_length', 'knng'],
+  //   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 m_OPTIONS = [
@@ -123,19 +123,74 @@ export const m_OPTIONS = [
 ];
 
 export const INDEX_OPTIONS_MAP = {
-  FLOAT_POINT: Object.keys(INDEX_CONFIG).map(v => ({ label: v, value: v })),
-  BINARY_ONE: [{ label: 'FLAT', value: 'FLAT' }],
-  BINARY_TWO: [
-    { 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 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,
+    },
   ],
 };
 
-export const FIELD_TYPES = {
-  VECTOR_FLOAT: 'vector_float',
-  VECTOR_BINARY: 'vector_binary',
-  Float: 'float',
-  Double: 'double',
-  INT32: 'int32',
-  INT64: 'int64',
+/**
+ * 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,
+};
+
+export const DEFAULT_NLIST_VALUE = 1024;

+ 11 - 2
client/src/context/Auth.tsx

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

+ 1 - 1
client/src/context/Root.tsx

@@ -85,8 +85,8 @@ export const RootProvider = (props: { children: React.ReactNode }) => {
     },
     []
   );
+
   const handleCloseDialog = () => {
-    // setDialog(DefaultDialogConfigs);
     setDialog({
       ...dialog,
       open: false,

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

@@ -25,7 +25,7 @@ export type DialogType = {
      * Usually we control open status in root context,
      * if we need a hoc component depend on setDialog in context,
      * we may need control open status by ourself
-     *  */
+     **/
     handleClose?: () => void;
 
     // used for dialog position

+ 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 { Typography } from '@material-ui/core';
 import { rootContext } from '../context/Root';
 import { CollectionView } from '../pages/collections/Types';
 import { PartitionView } from '../pages/partitions/Types';
 import { StatusEnum } from '../components/status/Types';
+import { CollectionData } from '../pages/overview/collectionCard/Types';
 
 // handle release and load dialog
-export interface DialogHookProps {
+export interface LoadAndReleaseDialogHookProps {
   type: 'partition' | 'collection';
 }
 
-export const useDialogHook = (props: DialogHookProps) => {
+export const useLoadAndReleaseDialogHook = (
+  props: LoadAndReleaseDialogHookProps
+) => {
   const { type } = props;
   const { setDialog } = useContext(rootContext);
   const { t: dialogTrans } = useTranslation('dialog');
@@ -46,7 +49,7 @@ export const useDialogHook = (props: DialogHookProps) => {
   };
 
   const handleAction = (
-    data: PartitionView | CollectionView,
+    data: PartitionView | CollectionView | CollectionData,
     cb: (data: any) => Promise<any>
   ) => {
     const actionType: 'release' | 'load' =
@@ -69,3 +72,24 @@ export const useDialogHook = (props: DialogHookProps) => {
     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,
+  };
+};

+ 6 - 3
client/src/hooks/Form.ts

@@ -1,6 +1,6 @@
 import { useState } from 'react';
 import { IValidation } from '../components/customInput/Types';
-import { checkIsEmpty, getCheckResult } from '../utils/Validation';
+import { checkEmptyValid, getCheckResult } from '../utils/Validation';
 
 export interface IForm {
   key: string;
@@ -51,7 +51,10 @@ export const useFormValidation = (form: IForm[]): IValidationInfo => {
   // validation detail about form item
   const [validation, setValidation] = useState(initValidation);
   // 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 { value, key, rules } = param;
@@ -95,7 +98,7 @@ export const useFormValidation = (form: IForm[]): IValidationInfo => {
 
   const checkFormValid = (form: IForm[]): boolean => {
     const requireCheckItems = form.filter(f => f.needCheck);
-    if (requireCheckItems.some(item => !checkIsEmpty(item.value))) {
+    if (requireCheckItems.some(item => !checkEmptyValid(item.value))) {
       return false;
     }
 

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

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

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott