Bladeren bron

Merge branch 'main' into readme

ruiyi.jiang 4 jaren geleden
bovenliggende
commit
d722014c2f
57 gewijzigde bestanden met toevoegingen van 1747 en 267 verwijderingen
  1. 19 0
      .github/workflows/merge.yml
  2. 31 0
      .github/workflows/release.yml
  3. 1 0
      .gitignore
  4. 201 0
      LICENSE
  5. 1 1
      client/public/index.html
  6. 3 0
      client/src/assets/icons/key.svg
  7. 2 2
      client/src/components/customInput/CustomInput.tsx
  8. 18 0
      client/src/components/grid/Table.tsx
  9. 3 0
      client/src/components/grid/Types.ts
  10. 3 1
      client/src/components/grid/index.tsx
  11. 4 0
      client/src/components/icons/Icons.tsx
  12. 2 1
      client/src/components/icons/Types.ts
  13. 26 50
      client/src/consts/Milvus.tsx
  14. 2 2
      client/src/hooks/Form.ts
  15. 10 5
      client/src/hooks/Pagination.ts
  16. 0 2
      client/src/http/BaseModel.ts
  17. 2 1
      client/src/http/Collection.ts
  18. 47 0
      client/src/http/Field.ts
  19. 71 0
      client/src/http/Index.ts
  20. 12 0
      client/src/http/Partition.ts
  21. 3 0
      client/src/i18n/cn/collection.ts
  22. 1 0
      client/src/i18n/cn/common.ts
  23. 2 0
      client/src/i18n/cn/dialog.ts
  24. 14 0
      client/src/i18n/cn/index.ts
  25. 1 0
      client/src/i18n/cn/partition.ts
  26. 0 4
      client/src/i18n/cn/warning.ts
  27. 3 0
      client/src/i18n/en/collection.ts
  28. 1 0
      client/src/i18n/en/common.ts
  29. 2 0
      client/src/i18n/en/dialog.ts
  30. 14 0
      client/src/i18n/en/index.ts
  31. 1 0
      client/src/i18n/en/partition.ts
  32. 0 4
      client/src/i18n/en/warning.ts
  33. 4 0
      client/src/i18n/index.ts
  34. 5 4
      client/src/pages/collections/Collection.tsx
  35. 37 21
      client/src/pages/collections/Collections.tsx
  36. 42 18
      client/src/pages/collections/Create.tsx
  37. 194 111
      client/src/pages/collections/CreateFields.tsx
  38. 15 3
      client/src/pages/collections/Types.ts
  39. 2 2
      client/src/pages/connect/Connect.tsx
  40. 10 6
      client/src/pages/overview/Overview.tsx
  41. 3 3
      client/src/pages/overview/collectionCard/CollectionCard.tsx
  42. 5 5
      client/src/pages/partitions/Create.tsx
  43. 13 3
      client/src/pages/partitions/partitions.tsx
  44. 160 0
      client/src/pages/structure/Create.tsx
  45. 205 0
      client/src/pages/structure/CreateForm.tsx
  46. 167 0
      client/src/pages/structure/IndexTypeElement.tsx
  47. 218 0
      client/src/pages/structure/Structure.tsx
  48. 63 0
      client/src/pages/structure/Types.ts
  49. 9 0
      client/src/types/Common.ts
  50. 62 3
      client/src/utils/Form.ts
  51. 5 5
      client/src/utils/Validation.ts
  52. 4 4
      client/src/utils/__test__/Validation.spec.ts
  53. 15 0
      package.json
  54. 1 1
      server/package.json
  55. 1 1
      server/src/collections/collections.service.ts
  56. 3 0
      server/src/schema/schema.service.ts
  57. 4 4
      server/yarn.lock

+ 19 - 0
.github/workflows/merge.yml

@@ -0,0 +1,19 @@
+on:
+  push:
+    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: Semantic release
+        env:
+          GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
+        run: npx semantic-release

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

@@ -0,0 +1,31 @@
+on:
+  release:
+    types: [published]
+
+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

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

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

+ 1 - 1
client/public/index.html

@@ -30,7 +30,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>

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

+ 2 - 2
client/src/components/customInput/CustomInput.tsx

@@ -257,12 +257,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>
   );

+ 18 - 0
client/src/components/grid/Table.tsx

@@ -117,6 +117,7 @@ const EnhancedTable: FC<TableType> = props => {
     noData,
     showHoverStyle,
     isLoading,
+    setPageSize,
   } = props;
   const classes = useStyles();
   const [order, setOrder] = React.useState('asc');
@@ -125,6 +126,7 @@ const EnhancedTable: FC<TableType> = props => {
   const [loadingRowCount, setLoadingRowCount] = useState<number>(0);
 
   const containerRef = useRef(null);
+  const rowRef = useRef(null);
 
   const handleRequestSort = (event: any, property: string) => {
     const isAsc = orderBy === property && order === 'asc';
@@ -139,6 +141,21 @@ const EnhancedTable: FC<TableType> = props => {
     setLoadingRowCount(count);
   }, []);
 
+  useEffect(() => {
+    if (setPageSize) {
+      const containerHeight: number = (containerRef.current as any)!
+        .offsetHeight;
+      const rowHeight: number = (rowRef.current as any)?.offsetHeight || 0;
+      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}>
@@ -183,6 +200,7 @@ const EnhancedTable: FC<TableType> = props => {
 
                     return (
                       <TableRow
+                        ref={rowRef}
                         hover={showHoverStyle}
                         key={'row' + row[primaryKey] + index}
                         onClick={event => onSelected(event, row)}

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

@@ -66,6 +66,7 @@ export type TableType = {
   noData?: string;
   showHoverStyle?: boolean;
   isLoading?: boolean;
+  setPageSize?: (size: number) => void;
 };
 
 export type ColDefinitionsType = {
@@ -93,6 +94,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;

+ 3 - 1
client/src/components/grid/index.tsx

@@ -98,10 +98,11 @@ const MilvusGrid: FC<MilvusGridType> = props => {
     searchForm,
     openCheckBox = true,
     disableSelect = false,
-    noData = t('grid.noData'),
+    noData = gridTrans.noData,
     showHoverStyle = true,
     selected = [],
     setSelected = () => {},
+    setRowsPerPage = () => {},
   } = props;
 
   const _isSelected = (row: { [x: string]: any }) => {
@@ -206,6 +207,7 @@ const MilvusGrid: FC<MilvusGridType> = props => {
           noData={noData}
           showHoverStyle={showHoverStyle}
           isLoading={isLoading}
+          setPageSize={setRowsPerPage}
         ></Table>
         {rowCount ? (
           <TablePagination

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

@@ -26,6 +26,7 @@ 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';
 
 const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
   search: (props = {}) => <SearchIcon {...props} />,
@@ -68,6 +69,9 @@ 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} />
+  ),
 };
 
 export default icons;

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

@@ -23,4 +23,5 @@ export type IconsType =
   | 'info'
   | 'release'
   | 'load'
-  | 'remove';
+  | 'remove'
+  | 'key';

+ 26 - 50
client/src/consts/Milvus.tsx

@@ -1,33 +1,11 @@
-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',
-  },
-];
-
 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,10 +17,6 @@ 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',
@@ -51,6 +25,10 @@ export const METRIC_TYPES = [
     value: METRIC_TYPES_VALUES.SUPERSTRUCTURE,
     label: 'Superstructure',
   },
+  {
+    value: METRIC_TYPES_VALUES.HAMMING,
+    label: 'Hamming',
+  },
   {
     value: METRIC_TYPES_VALUES.JACCARD,
     label: 'Jaccard',
@@ -61,13 +39,14 @@ export const METRIC_TYPES = [
   },
 ];
 
-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';
 
@@ -124,18 +103,15 @@ 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: [
+  BINARY: [
     { label: 'FLAT', value: 'FLAT' },
     { label: 'IVF_FLAT', value: 'IVF_FLAT' },
   ],
 };
 
-export const FIELD_TYPES = {
-  VECTOR_FLOAT: 'vector_float',
-  VECTOR_BINARY: 'vector_binary',
-  Float: 'float',
-  Double: 'double',
-  INT32: 'int32',
-  INT64: 'int64',
-};
+export const PRIMARY_KEY_FIELD = 'INT64 (Primary key)';
+
+export enum EmbeddingTypeEnum {
+  float = 'FLOAT_POINT',
+  binary = 'BINARY',
+}

+ 2 - 2
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;
@@ -95,7 +95,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;
     }
 

+ 10 - 5
client/src/hooks/Pagination.ts

@@ -1,26 +1,31 @@
 import { useMemo, useState } from 'react';
 
-const PAGE_SIZE = 10;
 export const usePaginationHook = (list: any[]) => {
   const [currentPage, setCurrentPage] = useState(0);
+  const [pageSize, setPageSize] = useState(10);
 
   const total = list.length;
   const { data, offset } = useMemo(() => {
-    const offset = PAGE_SIZE * currentPage;
+    const offset = pageSize * currentPage;
     return {
       offset,
-      data: list.slice(offset, offset + PAGE_SIZE),
+      data: list.slice(offset, offset + pageSize),
     };
-  }, [list, currentPage]);
+  }, [list, currentPage, pageSize]);
 
   const handleCurrentPage = (page: number) => {
     setCurrentPage(page);
   };
 
+  const handlePageSize = (size: number) => {
+    setPageSize(size);
+  };
+
   return {
     offset,
     currentPage,
-    pageSize: PAGE_SIZE,
+    pageSize,
+    handlePageSize,
     handleCurrentPage,
     total,
     data,

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

@@ -55,9 +55,7 @@ export default class BaseModel {
    */
   static async create(options: updateParamsType) {
     const { path, data } = options;
-
     const res = await http.post(path, data);
-
     return new this(res.data.data || {});
   }
 

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

@@ -9,6 +9,7 @@ export class CollectionHttp extends BaseModel implements CollectionView {
   private description!: string;
   private rowCount!: string;
   private index_status!: string;
+  private id!: string;
 
   static COLLECTIONS_URL = '/collections';
   static COLLECTIONS_INDEX_STATUS_URL = '/collections/indexes/status';
@@ -60,7 +61,7 @@ export class CollectionHttp extends BaseModel implements CollectionView {
   }
 
   get _id() {
-    return '12';
+    return this.id;
   }
 
   get _name() {

+ 47 - 0
client/src/http/Field.ts

@@ -0,0 +1,47 @@
+import { DataType } from '../pages/collections/Types';
+import { FieldData } from '../pages/structure/Types';
+import BaseModel from './BaseModel';
+
+export class FieldHttp extends BaseModel implements FieldData {
+  data_type!: DataType;
+  fieldID!: string;
+  type_params!: { key: string; value: string }[];
+  is_primary_key!: true;
+  name!: string;
+
+  constructor(props: {}) {
+    super(props);
+    Object.assign(this, props);
+  }
+
+  static async getFields(collectionName: string): Promise<FieldHttp[]> {
+    const path = `/collections/${collectionName}`;
+
+    const res = await super.findAll({
+      path,
+      params: {},
+    });
+
+    return res.schema.fields.map((f: any) => new this(f));
+  }
+
+  get _fieldId() {
+    return this.fieldID;
+  }
+
+  get _isPrimaryKey() {
+    return this.is_primary_key;
+  }
+
+  get _fieldName() {
+    return this.name;
+  }
+
+  get _fieldType() {
+    return this.data_type;
+  }
+
+  get _dimension() {
+    return this.type_params.find(item => item.key === 'dim')?.value || '--';
+  }
+}

+ 71 - 0
client/src/http/Index.ts

@@ -0,0 +1,71 @@
+import {
+  IndexCreateParam,
+  IndexManageParam,
+  IndexView,
+  ParamPair,
+} from '../pages/structure/Types';
+import { ManageRequestMethods } from '../types/Common';
+import { IndexState } from '../types/Milvus';
+import BaseModel from './BaseModel';
+
+export class IndexHttp extends BaseModel implements IndexView {
+  params!: ParamPair[];
+  field_name!: string;
+
+  constructor(props: {}) {
+    super(props);
+    Object.assign(this, props);
+  }
+
+  static BASE_URL = `/schema/index`;
+
+  static async getIndexStatus(
+    collectionName: string,
+    fieldName: string
+  ): Promise<IndexState> {
+    const path = `${this.BASE_URL}/state`;
+    return super.findAll({
+      path,
+      params: { collection_name: collectionName, field_name: fieldName },
+    });
+  }
+
+  static async getIndexInfo(collectionName: string): Promise<IndexHttp[]> {
+    const path = this.BASE_URL;
+
+    const res = await super.findAll({
+      path,
+      params: { collection_name: collectionName },
+    });
+    return res.index_descriptions.map((index: any) => new this(index));
+  }
+
+  static async createIndex(param: IndexCreateParam) {
+    const path = this.BASE_URL;
+    const type: ManageRequestMethods = ManageRequestMethods.CREATE;
+
+    return super.create({
+      path,
+      data: { ...param, type },
+    });
+  }
+
+  static async deleteIndex(param: IndexManageParam) {
+    const path = this.BASE_URL;
+    const type: ManageRequestMethods = ManageRequestMethods.DELETE;
+
+    return super.batchDelete({ path, data: { ...param, type } });
+  }
+
+  get _indexType() {
+    return this.params.find(p => p.key === 'index_type')?.value || '';
+  }
+
+  get _indexParameterPairs() {
+    return this.params.filter(p => p.key !== 'index_type');
+  }
+
+  get _fieldName() {
+    return this.field_name;
+  }
+}

+ 12 - 0
client/src/http/Partition.ts

@@ -51,6 +51,18 @@ export class PartitionHttp extends BaseModel implements PartitionView {
     });
   }
 
+  static releasePartition(param: PartitionParam) {
+    const { collectionName, partitionNames } = param;
+    const path = `${this.URL_BASE}/release`;
+    return super.update({
+      path,
+      data: {
+        collection_name: collectionName,
+        partition_names: partitionNames,
+      },
+    });
+  }
+
   get _id() {
     return this.id;
   }

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

@@ -26,6 +26,9 @@ const collectionTrans = {
   fieldName: 'Field Name',
   autoId: 'Auto ID',
   dimension: 'Dimension',
+  dimensionTooltip: 'Only vector type has dimension',
+  dimensionMutipleWarning: 'Dimension should be 8 multiple',
+  dimensionPositiveWarning: 'Dimension should be positive number',
   newBtn: 'add new field',
 
   // load dialog

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

@@ -21,6 +21,7 @@ const commonTrans = {
     copy: 'Copy',
     copied: 'Copied',
   },
+  param: 'Parameter',
 };
 
 export default commonTrans;

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

@@ -8,6 +8,8 @@ const dialogTrans = {
 
   loadContent: `You are trying to load a {{type}} with data. Only loaded {{type}} can be searched.`,
   releaseContent: `You are trying to release a {{type}} with data. Please be aware that the data will no longer be available for search.`,
+
+  createTitle: `Create {{type}} in "{{name}}"`,
 };
 
 export default dialogTrans;

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

@@ -0,0 +1,14 @@
+const indexTrans = {
+  type: 'Index Type',
+  param: 'Index Parameters',
+  create: 'Create Index',
+
+  index: 'Index',
+  metric: 'Metric Type',
+
+  createSuccess: 'Creating index successfully',
+  deleteWarning:
+    'You are trying to delete an index. This action cannot be undone.',
+};
+
+export default indexTrans;

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

@@ -15,6 +15,7 @@ const partitionTrans = {
 
   deleteWarning:
     'You are trying to delete partition. This action cannot be undone.',
+  deletePartitionError: 'default partition cannot be deleted',
 };
 
 export default partitionTrans;

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

@@ -1,12 +1,8 @@
 const warningTrans = {
-  closeDialog: '已填写的表单会丢失,确认离开?',
-
   required: '{{name}} is required',
   positive: '{{name}} should be positive',
   integer: '{{name}} should be integers',
   range: 'range is {{min}} ~ {{max}}',
-
-  deletePartition: 'default partition cannot be deleted',
 };
 
 export default warningTrans;

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

@@ -26,6 +26,9 @@ const collectionTrans = {
   fieldName: 'Field Name',
   autoId: 'Auto ID',
   dimension: 'Dimension',
+  dimensionTooltip: 'Only vector type has dimension',
+  dimensionMutipleWarning: 'Dimension should be 8 multiple',
+  dimensionPositiveWarning: 'Dimension should be positive number',
   newBtn: 'add new field',
 
   // load dialog

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

@@ -21,6 +21,7 @@ const commonTrans = {
     copy: 'Copy',
     copied: 'Copied',
   },
+  param: 'Parameter',
 };
 
 export default commonTrans;

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

@@ -7,6 +7,8 @@ const dialogTrans = {
 
   loadContent: `You are trying to load a {{type}} with data. Only loaded {{type}} can be searched.`,
   releaseContent: `You are trying to release a {{type}} with data. Please be aware that the data will no longer be available for search.`,
+
+  createTitle: `Create {{type}} in "{{name}}"`,
 };
 
 export default dialogTrans;

+ 14 - 0
client/src/i18n/en/index.ts

@@ -0,0 +1,14 @@
+const indexTrans = {
+  type: 'Index Type',
+  param: 'Index Parameters',
+
+  create: 'Create Index',
+  index: 'Index',
+
+  metric: 'Metric Type',
+  createSuccess: 'Creating index successfully',
+  deleteWarning:
+    'You are trying to delete an index. This action cannot be undone.',
+};
+
+export default indexTrans;

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

@@ -15,6 +15,7 @@ const partitionTrans = {
 
   deleteWarning:
     'You are trying to delete partition. This action cannot be undone.',
+  deletePartitionError: 'default partition cannot be deleted',
 };
 
 export default partitionTrans;

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

@@ -1,12 +1,8 @@
 const warningTrans = {
-  closeDialog: 'Will lose your data, are you sure to leave?',
-
   required: '{{name}} is required',
   positive: '{{name}} should be positive',
   integer: '{{name}} should be integers',
   range: 'range is {{min}} ~ {{max}}',
-
-  deletePartition: 'default partition cannot be deleted',
 };
 
 export default warningTrans;

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

@@ -19,6 +19,8 @@ import partitionCn from './cn/partition';
 import partitionEn from './en/partition';
 import successEn from './en/success';
 import successCn from './cn/success';
+import indexEn from './en/index';
+import indexCn from './cn/index';
 
 export const resources = {
   cn: {
@@ -31,6 +33,7 @@ export const resources = {
     dialog: dialogCn,
     partition: partitionCn,
     success: successCn,
+    index: indexCn,
   },
   en: {
     translation: commonEn,
@@ -42,6 +45,7 @@ export const resources = {
     dialog: dialogEn,
     partition: partitionEn,
     success: successEn,
+    index: indexEn,
   },
 };
 

+ 5 - 4
client/src/pages/collections/Collection.tsx

@@ -7,6 +7,7 @@ import Partitions from '../partitions/partitions';
 import { useHistory, useLocation, useParams } from 'react-router-dom';
 import { useMemo } from 'react';
 import { parseLocationSearch } from '../../utils/Format';
+import Structure from '../structure/Structure';
 
 enum TAB_EMUM {
   'partition',
@@ -24,7 +25,7 @@ const Collection = () => {
   const history = useHistory();
   const location = useLocation();
 
-  const { t } = useTranslation('collection');
+  const { t: collectionTrans } = useTranslation('collection');
 
   const activeTabIndex = useMemo(() => {
     const { activeIndex } = location.search
@@ -40,12 +41,12 @@ const Collection = () => {
 
   const tabs: ITab[] = [
     {
-      label: t('partitionTab'),
+      label: collectionTrans('partitionTab'),
       component: <Partitions collectionName={collectionName} />,
     },
     {
-      label: t('structureTab'),
-      component: <section>structure section</section>,
+      label: collectionTrans('structureTab'),
+      component: <Structure collectionName={collectionName} />,
     },
   ];
 

+ 37 - 21
client/src/pages/collections/Collections.tsx

@@ -4,7 +4,7 @@ import { useNavigationHook } from '../../hooks/Navigation';
 import { ALL_ROUTER_TYPES } from '../../router/Types';
 import MilvusGrid from '../../components/grid';
 import CustomToolBar from '../../components/grid/ToolBar';
-import { CollectionCreateParam, CollectionView } from './Types';
+import { CollectionCreateParam, CollectionView, DataTypeEnum } from './Types';
 import { ColDefinitionsType, ToolBarConfig } from '../../components/grid/Types';
 import { usePaginationHook } from '../../hooks/Pagination';
 import icons from '../../components/icons/Icons';
@@ -46,6 +46,7 @@ const Collections = () => {
   const [collections, setCollections] = useState<CollectionView[]>([]);
   const {
     pageSize,
+    handlePageSize,
     currentPage,
     handleCurrentPage,
     total,
@@ -58,7 +59,7 @@ const Collections = () => {
 
   const { setDialog, handleCloseDialog, openSnackBar } =
     useContext(rootContext);
-  const { t } = useTranslation('collection');
+  const { t: collectionTrans } = useTranslation('collection');
   const { t: btnTrans } = useTranslation('btn');
   const { t: dialogTrans } = useTranslation('dialog');
   const { t: successTrans } = useTranslation('success');
@@ -106,26 +107,36 @@ const Collections = () => {
 
   const handleCreateCollection = async (param: CollectionCreateParam) => {
     const data: CollectionCreateParam = JSON.parse(JSON.stringify(param));
-    data.fields = data.fields.map(v => ({
-      ...v,
-      type_params: [{ key: 'dim', value: v.dimension }],
-    }));
+    const vectorType = [DataTypeEnum.BinaryVector, DataTypeEnum.FloatVector];
+
+    data.fields = data.fields.map(v =>
+      vectorType.includes(v.data_type)
+        ? {
+            ...v,
+            type_params: [{ key: 'dim', value: v.dimension }],
+          }
+        : v
+    );
     await CollectionHttp.createCollection(data);
     handleCloseDialog();
-    openSnackBar(successTrans('create', { name: t('collection') }));
+    openSnackBar(
+      successTrans('create', { name: collectionTrans('collection') })
+    );
     fetchData();
   };
 
   const handleRelease = async (data: CollectionView) => {
     const res = await CollectionHttp.releaseCollection(data._name);
-    openSnackBar(successTrans('release', { name: t('collection') }));
+    openSnackBar(
+      successTrans('release', { name: collectionTrans('collection') })
+    );
     fetchData();
     return res;
   };
 
   const handleLoad = async (data: CollectionView) => {
     const res = await CollectionHttp.loadCollection(data._name);
-    openSnackBar(successTrans('load', { name: t('collection') }));
+    openSnackBar(successTrans('load', { name: collectionTrans('collection') }));
     fetchData();
     return res;
   };
@@ -134,7 +145,9 @@ const Collections = () => {
     for (const item of selectedCollections) {
       await CollectionHttp.deleteCollection(item._name);
     }
-    openSnackBar(successTrans('delete', { name: t('collection') }));
+    openSnackBar(
+      successTrans('delete', { name: collectionTrans('collection') })
+    );
     fetchData();
     handleCloseDialog();
     setSelectedCollections([]);
@@ -142,7 +155,7 @@ const Collections = () => {
 
   const toolbarConfigs: ToolBarConfig[] = [
     {
-      label: t('create'),
+      label: collectionTrans('create'),
       onClick: () => {
         setDialog({
           open: true,
@@ -166,15 +179,17 @@ const Collections = () => {
             component: (
               <DeleteTemplate
                 label={btnTrans('delete')}
-                title={dialogTrans('deleteTitle', { type: t('collection') })}
-                text={t('deleteWarning')}
+                title={dialogTrans('deleteTitle', {
+                  type: collectionTrans('collection'),
+                })}
+                text={collectionTrans('deleteWarning')}
                 handleDelete={handleDelete}
               />
             ),
           },
         });
       },
-      label: t('delete'),
+      label: collectionTrans('delete'),
       icon: 'delete',
       disabled: data => data.length === 0,
     },
@@ -185,19 +200,19 @@ const Collections = () => {
       id: '_id',
       align: 'left',
       disablePadding: true,
-      label: t('id'),
+      label: collectionTrans('id'),
     },
     {
       id: 'nameElement',
       align: 'left',
       disablePadding: true,
-      label: t('name'),
+      label: collectionTrans('name'),
     },
     {
       id: 'statusElement',
       align: 'left',
       disablePadding: false,
-      label: t('status'),
+      label: collectionTrans('status'),
     },
     {
       id: '_rowCount',
@@ -205,8 +220,8 @@ const Collections = () => {
       disablePadding: false,
       label: (
         <span className="flex-center">
-          {t('rowCount')}
-          <CustomToolTip title={t('tooltip')}>
+          {collectionTrans('rowCount')}
+          <CustomToolTip title={collectionTrans('tooltip')}>
             <InfoIcon classes={{ root: classes.icon }} />
           </CustomToolTip>
         </span>
@@ -216,7 +231,7 @@ const Collections = () => {
       id: '_desc',
       align: 'left',
       disablePadding: false,
-      label: t('desc'),
+      label: collectionTrans('desc'),
     },
     {
       id: 'indexCreatingElement',
@@ -277,6 +292,7 @@ const Collections = () => {
           page={currentPage}
           onChangePage={handlePageChange}
           rowsPerPage={pageSize}
+          setRowsPerPage={handlePageSize}
           isLoading={loading}
         />
       ) : (
@@ -285,7 +301,7 @@ const Collections = () => {
           <EmptyCard
             wrapperClass={`page-empty-card ${classes.emptyWrapper}`}
             icon={<CollectionIcon />}
-            text={t('noData')}
+            text={collectionTrans('noData')}
           />
         </>
       )}

+ 42 - 18
client/src/pages/collections/Create.tsx

@@ -6,7 +6,6 @@ import CustomInput from '../../components/customInput/CustomInput';
 import { ITextfieldConfig } from '../../components/customInput/Types';
 import { rootContext } from '../../context/Root';
 import { useFormValidation } from '../../hooks/Form';
-import { generateId } from '../../utils/Common';
 import { formatForm } from '../../utils/Form';
 import CreateFields from './CreateFields';
 import {
@@ -43,7 +42,7 @@ const useStyles = makeStyles((theme: Theme) => ({
 const CreateCollection: FC<CollectionCreateProps> = ({ handleCreate }) => {
   const classes = useStyles();
   const { handleCloseDialog } = useContext(rootContext);
-  const { t } = useTranslation('collection');
+  const { t: collectionTrans } = useTranslation('collection');
   const { t: btnTrans } = useTranslation('btn');
   const { t: warningTrans } = useTranslation('warning');
 
@@ -52,31 +51,45 @@ const CreateCollection: FC<CollectionCreateProps> = ({ handleCreate }) => {
     description: '',
     autoID: true,
   });
+
   const [fields, setFields] = useState<Field[]>([
     {
       data_type: DataTypeEnum.Int64,
       is_primary_key: true,
-      name: '',
+      name: null, // we need hide helpertext at first time, so we use null to detect user enter input or not.
       description: '',
       isDefault: true,
-      id: generateId(),
+      id: '1',
     },
     {
       data_type: DataTypeEnum.FloatVector,
       is_primary_key: false,
-      name: '',
-      dimension: '',
+      name: null,
+      dimension: '128',
       description: '',
       isDefault: true,
-      id: generateId(),
+      id: '2',
     },
   ]);
-  const [fieldsAllValid, setFieldsAllValid] = useState<boolean>(true);
+
+  const [fieldsValidation, setFieldsValidation] = useState<
+    {
+      [x: string]: string | boolean;
+    }[]
+  >([
+    { id: '1', name: false },
+    { id: '2', name: false, dimension: true },
+  ]);
+
+  const allFieldsValid = useMemo(() => {
+    return fieldsValidation.every(v => Object.keys(v).every(key => !!v[key]));
+  }, [fieldsValidation]);
 
   const checkedForm = useMemo(() => {
     const { collection_name } = form;
     return formatForm({ collection_name });
   }, [form]);
+
   const { validation, checkIsValid, disabled } = useFormValidation(checkedForm);
 
   const changeIsAutoID = (value: boolean) => {
@@ -92,21 +105,23 @@ const CreateCollection: FC<CollectionCreateProps> = ({ handleCreate }) => {
 
   const generalInfoConfigs: ITextfieldConfig[] = [
     {
-      label: t('name'),
-      key: 'name',
+      label: collectionTrans('name'),
+      key: 'collection_name',
       value: form.collection_name,
       onChange: (value: string) => handleInputChange('collection_name', value),
       variant: 'filled',
       validations: [
         {
           rule: 'require',
-          errorText: warningTrans('required', { name: t('name') }),
+          errorText: warningTrans('required', {
+            name: collectionTrans('name'),
+          }),
         },
       ],
       className: classes.input,
     },
     {
-      label: t('description'),
+      label: collectionTrans('description'),
       key: 'description',
       value: form.description,
       onChange: (value: string) => handleInputChange('description', value),
@@ -117,24 +132,33 @@ const CreateCollection: FC<CollectionCreateProps> = ({ handleCreate }) => {
   ];
 
   const handleCreateCollection = () => {
+    const vectorType = [DataTypeEnum.BinaryVector, DataTypeEnum.FloatVector];
     const param: CollectionCreateParam = {
       ...form,
-      fields,
+      fields: fields.map(v => {
+        return {
+          name: v.name,
+          description: v.description,
+          is_primary_key: v.is_primary_key,
+          data_type: v.data_type,
+          dimension: vectorType.includes(v.data_type) ? v.dimension : undefined,
+        };
+      }),
     };
     handleCreate(param);
   };
 
   return (
     <DialogTemplate
-      title={t('createTitle')}
+      title={collectionTrans('createTitle')}
       handleCancel={handleCloseDialog}
       confirmLabel={btnTrans('create')}
       handleConfirm={handleCreateCollection}
-      confirmDisabled={disabled || !fieldsAllValid}
+      confirmDisabled={disabled || !allFieldsValid}
     >
       <form>
         <fieldset className={classes.fieldset}>
-          <legend>{t('general')}</legend>
+          <legend>{collectionTrans('general')}</legend>
           {generalInfoConfigs.map(config => (
             <CustomInput
               key={config.key}
@@ -147,11 +171,11 @@ const CreateCollection: FC<CollectionCreateProps> = ({ handleCreate }) => {
         </fieldset>
 
         <fieldset className={classes.fieldset}>
-          <legend>{t('structure')}</legend>
+          <legend>{collectionTrans('structure')}</legend>
           <CreateFields
             fields={fields}
             setFields={setFields}
-            setfieldsAllValid={setFieldsAllValid}
+            setFieldsValidation={setFieldsValidation}
             autoID={form.autoID}
             setAutoID={changeIsAutoID}
           />

+ 194 - 111
client/src/pages/collections/CreateFields.tsx

@@ -4,8 +4,10 @@ import { useTranslation } from 'react-i18next';
 import CustomButton from '../../components/customButton/CustomButton';
 import CustomSelector from '../../components/customSelector/CustomSelector';
 import icons from '../../components/icons/Icons';
+import { PRIMARY_KEY_FIELD } from '../../consts/Milvus';
 import { generateId } from '../../utils/Common';
 import { getCreateFieldType } from '../../utils/Format';
+import { checkEmptyValid, getCheckResult } from '../../utils/Validation';
 import {
   ALL_OPTIONS,
   AUTO_ID_OPTIONS,
@@ -38,6 +40,7 @@ const useStyles = makeStyles((theme: Theme) => ({
   },
   select: {
     width: '160px',
+    marginBottom: '22px',
   },
   descInput: {
     minWidth: '270px',
@@ -57,20 +60,34 @@ const useStyles = makeStyles((theme: Theme) => ({
   mb2: {
     marginBottom: theme.spacing(2),
   },
+  helperText: {
+    color: theme.palette.error.main,
+  },
 }));
 
+type inputType = {
+  label: string;
+  value: string | number | null;
+  handleChange?: (value: string) => void;
+  className?: string;
+  inputClassName?: string;
+  isReadOnly?: boolean;
+  validate?: (value: string | number | null) => string;
+  type?: 'number' | 'text';
+};
+
 const CreateFields: FC<CreateFieldsProps> = ({
   fields,
   setFields,
-  // @TODO validation
-  setfieldsAllValid,
   setAutoID,
   autoID,
+  setFieldsValidation,
 }) => {
-  const { t } = useTranslation('collection');
+  const { t: collectionTrans } = useTranslation('collection');
+  const { t: warningTrans } = useTranslation('warning');
+
   const classes = useStyles();
 
-  const primaryInt64Value = 'INT64 (Primary key)';
   const AddIcon = icons.add;
   const RemoveIcon = icons.remove;
 
@@ -79,49 +96,146 @@ const CreateFields: FC<CreateFieldsProps> = ({
     label: string,
     value: number,
     onChange: (value: DataTypeEnum) => void
-  ) => (
-    <CustomSelector
-      options={type === 'all' ? ALL_OPTIONS : VECTOR_FIELDS_OPTIONS}
-      onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
-        onChange(e.target.value as DataTypeEnum);
-      }}
-      value={value}
-      variant="filled"
-      label={label}
-      classes={{ root: classes.select }}
-    />
-  );
+  ) => {
+    return (
+      <CustomSelector
+        options={type === 'all' ? ALL_OPTIONS : VECTOR_FIELDS_OPTIONS}
+        onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
+          onChange(e.target.value as DataTypeEnum);
+        }}
+        value={value}
+        variant="filled"
+        label={label}
+        classes={{ root: classes.select }}
+      />
+    );
+  };
 
-  const getInput = (
-    label: string,
-    value: string | number,
-    handleChange: (value: string) => void,
-    className = '',
-    inputClassName = '',
-    isReadOnly = false
-  ) => (
-    <TextField
-      label={label}
-      value={value}
-      onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
-        handleChange(e.target.value as string);
-      }}
-      variant="filled"
-      className={className}
-      InputProps={{
-        classes: {
-          input: inputClassName,
+  const getInput = (data: inputType) => {
+    const {
+      label,
+      value,
+      handleChange = () => {},
+      className = '',
+      inputClassName = '',
+      isReadOnly = false,
+      validate = (value: string | number | null) => ' ',
+      type = 'text',
+    } = data;
+    return (
+      <TextField
+        label={label}
+        // value={value}
+        onBlur={(e: React.ChangeEvent<{ value: unknown }>) => {
+          handleChange(e.target.value as string);
+        }}
+        variant="filled"
+        className={className}
+        InputProps={{
+          classes: {
+            input: inputClassName,
+          },
+        }}
+        disabled={isReadOnly}
+        helperText={validate(value)}
+        FormHelperTextProps={{
+          className: classes.helperText,
+        }}
+        defaultValue={value}
+        type={type}
+      />
+    );
+  };
+
+  const generateFieldName = (field: Field) => {
+    return getInput({
+      label: collectionTrans('fieldName'),
+      value: field.name,
+      handleChange: (value: string) => {
+        const isValid = checkEmptyValid(value);
+        setFieldsValidation(v =>
+          v.map(item =>
+            item.id === field.id! ? { ...item, name: isValid } : item
+          )
+        );
+
+        changeFields(field.id!, 'name', value);
+      },
+      validate: (value: any) => {
+        if (value === null) return ' ';
+        const isValid = checkEmptyValid(value);
+
+        return isValid
+          ? ' '
+          : warningTrans('required', { name: collectionTrans('fieldName') });
+      },
+    });
+  };
+
+  const generateDesc = (field: Field) => {
+    return getInput({
+      label: collectionTrans('description'),
+      value: field.description,
+      handleChange: (value: string) =>
+        changeFields(field.id!, 'description', value),
+      className: classes.descInput,
+    });
+  };
+
+  const generateDimension = (field: Field) => {
+    const validateDimension = (value: string) => {
+      const isPositive = getCheckResult({
+        value,
+        rule: 'positiveNumber',
+      });
+      const isMutiple = getCheckResult({
+        value,
+        rule: 'multiple',
+        extraParam: {
+          multipleNumber: 8,
         },
-      }}
-      disabled={isReadOnly}
-    />
-  );
+      });
+      if (field.data_type === DataTypeEnum.BinaryVector) {
+        return {
+          isMutiple,
+          isPositive,
+        };
+      }
+      return {
+        isPositive,
+      };
+    };
+    return getInput({
+      label: collectionTrans('dimension'),
+      value: field.dimension as number,
+      handleChange: (value: string) => {
+        const { isPositive, isMutiple } = validateDimension(value);
+        const isValid =
+          field.data_type === DataTypeEnum.BinaryVector
+            ? !!isMutiple && isPositive
+            : isPositive;
 
-  const changeFields = (
-    id: string,
-    key: string,
-    value: string | DataTypeEnum
-  ) => {
+        changeFields(field.id!, 'dimension', `${value}`);
+
+        setFieldsValidation(v =>
+          v.map(item =>
+            item.id === field.id! ? { ...item, dimension: isValid } : item
+          )
+        );
+      },
+      type: 'number',
+      validate: (value: any) => {
+        const { isPositive, isMutiple } = validateDimension(value);
+        if (isMutiple === false) {
+          return collectionTrans('dimensionMutipleWarning');
+        }
+
+        return isPositive ? ' ' : collectionTrans('dimensionPositiveWarning');
+      },
+    });
+  };
+
+  const changeFields = (id: string, key: string, value: any) => {
     const newFields = fields.map(f => {
       if (f.id !== id) {
         return f;
@@ -131,25 +245,33 @@ const CreateFields: FC<CreateFieldsProps> = ({
         [key]: value,
       };
     });
-
     setFields(newFields);
   };
 
   const handleAddNewField = () => {
+    const id = generateId();
     const newDefaultItem: Field = {
-      name: '',
+      name: null,
       data_type: DataTypeEnum.Int16,
       is_primary_key: false,
       description: '',
       isDefault: false,
-      id: generateId(),
+      dimension: '128',
+      id,
+    };
+    const newValidation = {
+      id,
+      name: false,
+      dimension: true,
     };
     setFields([...fields, newDefaultItem]);
+    setFieldsValidation(v => [...v, newValidation]);
   };
 
   const handleRemoveField = (field: Field) => {
     const newFields = fields.filter(f => f.id !== field.id);
     setFields(newFields);
+    setFieldsValidation(v => v.filter(item => item.id !== field.id));
   };
 
   const generatePrimaryKeyRow = (
@@ -158,21 +280,18 @@ const CreateFields: FC<CreateFieldsProps> = ({
   ): ReactElement => {
     return (
       <div className={`${classes.rowWrapper} ${classes.mb3}`}>
-        {getInput(
-          t('fieldType'),
-          primaryInt64Value,
-          () => {},
-          classes.primaryInput,
-          classes.input,
-          true
-        )}
+        {getInput({
+          label: collectionTrans('fieldType'),
+          value: PRIMARY_KEY_FIELD,
+          className: classes.primaryInput,
+          inputClassName: classes.input,
+          isReadOnly: true,
+        })}
 
-        {getInput(t('fieldName'), field.name, (value: string) =>
-          changeFields(field.id, 'name', value)
-        )}
+        {generateFieldName(field)}
 
         <CustomSelector
-          label={t('autoId')}
+          label={collectionTrans('autoId')}
           options={AUTO_ID_OPTIONS}
           value={autoID ? 'true' : 'false'}
           onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
@@ -183,12 +302,7 @@ const CreateFields: FC<CreateFieldsProps> = ({
           classes={{ root: classes.select }}
         />
 
-        {getInput(
-          t('description'),
-          field.description,
-          (value: string) => changeFields(field.id, 'description', value),
-          classes.descInput
-        )}
+        {generateDesc(field)}
       </div>
     );
   };
@@ -199,33 +313,21 @@ const CreateFields: FC<CreateFieldsProps> = ({
         <div className={`${classes.rowWrapper} ${classes.mb2}`}>
           {getSelector(
             'vector',
-            t('fieldType'),
+            collectionTrans('fieldType'),
             field.data_type,
-            (value: DataTypeEnum) => changeFields(field.id, 'data_type', value)
+            (value: DataTypeEnum) => changeFields(field.id!, 'data_type', value)
           )}
 
-          {getInput(t('fieldName'), field.name, (value: string) =>
-            changeFields(field.id, 'name', value)
-          )}
+          {generateFieldName(field)}
 
-          {getInput(
-            t('dimension'),
-            field.dimension as number,
-            (value: string) => changeFields(field.id, 'dimension', value),
-            'dimension'
-          )}
+          {generateDimension(field)}
 
-          {getInput(
-            t('description'),
-            field.description,
-            (value: string) => changeFields(field.id, 'description', value),
-            classes.descInput
-          )}
+          {generateDesc(field)}
         </div>
 
         <CustomButton onClick={handleAddNewField} className={classes.mb2}>
           <AddIcon />
-          <span className={classes.btnTxt}>{t('newBtn')}</span>
+          <span className={classes.btnTxt}>{collectionTrans('newBtn')}</span>
         </CustomButton>
       </>
     );
@@ -241,22 +343,15 @@ const CreateFields: FC<CreateFieldsProps> = ({
         >
           <RemoveIcon />
         </IconButton>
-        {getInput(t('fieldName'), field.name, (value: string) =>
-          changeFields(field.id, 'name', value)
-        )}
+        {generateFieldName(field)}
         {getSelector(
           'all',
-          t('fieldType'),
+          collectionTrans('fieldType'),
           field.data_type,
-          (value: DataTypeEnum) => changeFields(field.id, 'type', value)
+          (value: DataTypeEnum) => changeFields(field.id!, 'data_type', value)
         )}
 
-        {getInput(
-          t('description'),
-          field.description,
-          (value: string) => changeFields(field.id, 'desc', value),
-          classes.descInput
-        )}
+        {generateDesc(field)}
       </div>
     );
   };
@@ -267,28 +362,16 @@ const CreateFields: FC<CreateFieldsProps> = ({
         <IconButton classes={{ root: classes.iconBtn }} aria-label="delete">
           <RemoveIcon />
         </IconButton>
-        {getInput(t('fieldName'), field.name, (value: string) =>
-          changeFields(field.id, 'name', value)
-        )}
+        {generateFieldName(field)}
         {getSelector(
           'all',
-          t('fieldType'),
+          collectionTrans('fieldType'),
           field.data_type,
-          (value: DataTypeEnum) => changeFields(field.id, 'data_type', value)
-        )}
-        {getInput(
-          t('dimension'),
-          field.dimension as number,
-          (value: string) => changeFields(field.id, 'dimension', value),
-          'dimension'
+          (value: DataTypeEnum) => changeFields(field.id!, 'data_type', value)
         )}
+        {generateDimension(field)}
 
-        {getInput(
-          t('description'),
-          field.description,
-          (value: string) => changeFields(field.id, 'description', value),
-          classes.descInput
-        )}
+        {generateDesc(field)}
       </div>
     );
   };

+ 15 - 3
client/src/pages/collections/Types.ts

@@ -38,14 +38,24 @@ export enum DataTypeEnum {
   FloatVector = 101,
 }
 
+export type DataType =
+  | 'Int8'
+  | 'Int16'
+  | 'Int32'
+  | 'Int64'
+  | 'Float'
+  | 'Double'
+  | 'BinaryVector'
+  | 'FloatVector';
+
 export interface Field {
-  name: string;
+  name: string | null;
   data_type: DataTypeEnum;
   is_primary_key: boolean;
   description: string;
   dimension?: number | string;
   isDefault?: boolean;
-  id: string;
+  id?: string;
   type_params?: { key: string; value: any }[];
 }
 
@@ -58,7 +68,9 @@ export type CreateFieldType =
 export interface CreateFieldsProps {
   fields: Field[];
   setFields: Dispatch<SetStateAction<Field[]>>;
-  setfieldsAllValid: Dispatch<SetStateAction<boolean>>;
+  setFieldsValidation: Dispatch<
+    SetStateAction<{ [x: string]: string | boolean }[]>
+  >;
   autoID: boolean;
   setAutoID: (value: boolean) => void;
 }

+ 2 - 2
client/src/pages/connect/Connect.tsx

@@ -48,9 +48,9 @@ const Connect = () => {
   const { setAddress } = useContext(authContext);
   const { openSnackBar } = useContext(rootContext);
   const classes = useStyles();
-  const { t } = useTranslation();
+  const { t: commonTrans } = useTranslation();
   const { t: warningTrans } = useTranslation('warning');
-  const milvusTrans: { [key in string]: string } = t('milvus');
+  const milvusTrans: { [key in string]: string } = commonTrans('milvus');
   const { t: btnTrans } = useTranslation('btn');
   const { t: successTrans } = useTranslation('success');
 

+ 10 - 6
client/src/pages/overview/Overview.tsx

@@ -28,24 +28,26 @@ const useStyles = makeStyles((theme: Theme) => ({
 const Overview = () => {
   useNavigationHook(ALL_ROUTER_TYPES.OVERVIEW);
   const classes = useStyles();
-  const { t } = useTranslation('overview');
+  const { t: overviewTrans } = useTranslation('overview');
   const { t: collectionTrans } = useTranslation('collection');
 
   const mockStatistics: StatisticsCardProps = {
     data: [
       {
-        label: t('load'),
+        label: overviewTrans('load'),
         value: formatNumber(4337),
         valueColor: '#07d197',
       },
       {
-        label: t('all'),
+        label: overviewTrans('all'),
         value: formatNumber(30000),
         valueColor: '#06aff2',
       },
       {
-        label: t('data'),
-        value: t('rows', { number: formatNumber(209379100) }) as string,
+        label: overviewTrans('data'),
+        value: overviewTrans('rows', {
+          number: formatNumber(209379100),
+        }) as string,
         valueColor: '#0689d2',
       },
     ],
@@ -94,7 +96,9 @@ const Overview = () => {
   return (
     <section className="page-wrapper">
       <StatisticsCard data={mockStatistics.data} />
-      <Typography className={classes.collectionTitle}>{t('load')}</Typography>
+      <Typography className={classes.collectionTitle}>
+        {overviewTrans('load')}
+      </Typography>
       {mockCollections.length > 0 ? (
         <div className={classes.cardsWrapper}>
           {mockCollections.map(collection => (

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

@@ -60,7 +60,7 @@ const CollectionCard: FC<CollectionCardProps> = ({
   const RightArrowIcon = icons.rightArrow;
   const InfoIcon = icons.info;
   const ReleaseIcon = icons.release;
-  const { t } = useTranslation('collection');
+  const { t: collectionTrans } = useTranslation('collection');
   const { t: btnTrans } = useTranslation('btn');
 
   const handleRelease = () => {};
@@ -79,8 +79,8 @@ const CollectionCard: FC<CollectionCardProps> = ({
         <RightArrowIcon classes={{ root: classes.icon }} />
       </Link>
       <div className={classes.content}>
-        <Typography>{t('rowCount')}</Typography>
-        <CustomToolTip title={t('tooltip')} placement="bottom">
+        <Typography>{collectionTrans('rowCount')}</Typography>
+        <CustomToolTip title={collectionTrans('tooltip')} placement="bottom">
           <InfoIcon classes={{ root: classes.icon }} />
         </CustomToolTip>
         <Typography className={classes.rowCount}>{rowCount}</Typography>

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

@@ -18,7 +18,7 @@ const CreatePartition: FC<PartitionCreateProps> = ({
   handleCreate,
   handleClose,
 }) => {
-  const { t } = useTranslation('partition');
+  const { t: partitionTrans } = useTranslation('partition');
   const { t: btnTrans } = useTranslation('btn');
   const { t: warningTrans } = useTranslation('warning');
 
@@ -38,7 +38,7 @@ const CreatePartition: FC<PartitionCreateProps> = ({
   };
 
   const nameInputConfig: ITextfieldConfig = {
-    label: t('name'),
+    label: partitionTrans('name'),
     variant: 'filled',
     key: 'name',
     fullWidth: true,
@@ -47,11 +47,11 @@ const CreatePartition: FC<PartitionCreateProps> = ({
     validations: [
       {
         rule: 'require',
-        errorText: warningTrans('required', { name: t('name') }),
+        errorText: warningTrans('required', { name: partitionTrans('name') }),
       },
       {
         rule: 'partitionName',
-        errorText: t('nameWarning'),
+        errorText: partitionTrans('nameWarning'),
       },
     ],
   };
@@ -62,7 +62,7 @@ const CreatePartition: FC<PartitionCreateProps> = ({
 
   return (
     <DialogTemplate
-      title={t('createTitle')}
+      title={partitionTrans('createTitle')}
       handleCancel={handleClose}
       confirmLabel={btnTrans('create')}
       handleConfirm={handleCreatePartition}

+ 13 - 3
client/src/pages/partitions/partitions.tsx

@@ -32,7 +32,6 @@ const Partitions: FC<{
   const classes = useStyles();
   const { t } = useTranslation('partition');
   const { t: successTrans } = useTranslation('success');
-  const { t: warningTrans } = useTranslation('warning');
   const { t: btnTrans } = useTranslation('btn');
   const { t: dialogTrans } = useTranslation('dialog');
   const InfoIcon = icons.info;
@@ -46,6 +45,7 @@ const Partitions: FC<{
   const [partitions, setPartitions] = useState<PartitionView[]>([]);
   const {
     pageSize,
+    handlePageSize,
     currentPage,
     handleCurrentPage,
     total,
@@ -88,7 +88,16 @@ const Partitions: FC<{
     handleCloseDialog();
   };
 
-  const handleRelease = async (data: PartitionView) => {};
+  const handleRelease = async (data: PartitionView) => {
+    const param: PartitionParam = {
+      collectionName,
+      partitionNames: [data._name],
+    };
+    const res = await PartitionHttp.releasePartition(param);
+    openSnackBar(successTrans('release', { name: t('partition') }));
+    fetchPartitions(collectionName);
+    return res;
+  };
 
   const handleLoad = async (data: PartitionView) => {
     const param: PartitionParam = {
@@ -145,7 +154,7 @@ const Partitions: FC<{
         selectedPartitions.length === 0 ||
         selectedPartitions.some(p => p._name === '_default'),
       tooltip: selectedPartitions.some(p => p._name === '_default')
-        ? warningTrans('deletePartition')
+        ? t('deletePartitionError')
         : '',
     },
   ];
@@ -247,6 +256,7 @@ const Partitions: FC<{
         page={currentPage}
         onChangePage={handlePageChange}
         rowsPerPage={pageSize}
+        setRowsPerPage={handlePageSize}
         isLoading={loading}
       />
     </section>

+ 160 - 0
client/src/pages/structure/Create.tsx

@@ -0,0 +1,160 @@
+import { useEffect, useMemo, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import DialogTemplate from '../../components/customDialog/DialogTemplate';
+import {
+  EmbeddingTypeEnum,
+  INDEX_CONFIG,
+  INDEX_OPTIONS_MAP,
+  MetricType,
+} from '../../consts/Milvus';
+import { useFormValidation } from '../../hooks/Form';
+import { formatForm, getMetricOptions } from '../../utils/Form';
+import { DataType } from '../collections/Types';
+import CreateForm from './CreateForm';
+import { IndexType, ParamPair } from './Types';
+
+const CreateIndex = (props: {
+  collectionName: string;
+  fieldType: DataType;
+  handleCreate: (params: ParamPair[]) => void;
+  handleCancel: () => void;
+}) => {
+  const { collectionName, fieldType, handleCreate, handleCancel } = props;
+
+  const { t: indexTrans } = useTranslation('index');
+  const { t: dialogTrans } = useTranslation('dialog');
+  const { t: btnTrans } = useTranslation('btn');
+
+  const defaultMetricType = fieldType === 'BinaryVector' ? 'Hamming' : 'L2';
+
+  const [indexSetting, setIndexSetting] = useState<{
+    index_type: IndexType;
+    metric_type: MetricType;
+    [x: string]: string;
+  }>({
+    index_type: 'IVF_FLAT',
+    metric_type: defaultMetricType,
+    M: '',
+    m: '4',
+    efConstruction: '',
+    nlist: '',
+    n_trees: '',
+    outDegree: '',
+    candidatePoolSize: '',
+    searchLength: '',
+    knng: '',
+  });
+
+  const indexCreateParams = useMemo(() => {
+    if (!INDEX_CONFIG[indexSetting.index_type]) {
+      return [];
+    }
+    return INDEX_CONFIG[indexSetting.index_type].create;
+  }, [indexSetting.index_type]);
+
+  const metricOptions = useMemo(
+    () => getMetricOptions(indexSetting.index_type, fieldType),
+    [indexSetting.index_type, fieldType]
+  );
+
+  const indexOptions = useMemo(() => {
+    const type =
+      fieldType === 'BinaryVector'
+        ? EmbeddingTypeEnum.binary
+        : EmbeddingTypeEnum.float;
+    return INDEX_OPTIONS_MAP[type];
+  }, [fieldType]);
+
+  const checkedForm = useMemo(() => {
+    const paramsForm: any = { metric_type: indexSetting.metric_type };
+    indexCreateParams.forEach(v => {
+      paramsForm[v] = indexSetting[v];
+    });
+    const form = formatForm(paramsForm);
+    return form;
+  }, [indexSetting, indexCreateParams]);
+
+  const { validation, checkIsValid, disabled, setDisabled, resetValidation } =
+    useFormValidation(checkedForm);
+
+  useEffect(() => {
+    setDisabled(true);
+    setIndexSetting(v => ({
+      ...v,
+      metric_type: defaultMetricType,
+      M: '',
+      m: '4',
+      efConstruction: '',
+      nlist: '',
+      n_trees: '',
+      out_degree: '',
+      candidate_pool_size: '',
+      search_length: '',
+      knng: '',
+    }));
+  }, [indexCreateParams, setDisabled, defaultMetricType]);
+
+  const updateStepTwoForm = (type: string, value: string) => {
+    setIndexSetting(v => ({ ...v, [type]: value }));
+  };
+
+  const onIndexTypeChange = (type: string) => {
+    let paramsForm: { [key in string]: string } = {};
+    // m is selector not input
+    (INDEX_CONFIG[type].create || [])
+      .filter(t => t !== 'm')
+      .forEach(item => {
+        paramsForm[item] = '';
+      });
+
+    const form = formatForm(paramsForm);
+    resetValidation(form);
+  };
+
+  const handleCreateIndex = () => {
+    const { index_type, metric_type } = indexSetting;
+
+    const params: ParamPair[] = [
+      {
+        key: 'index_type',
+        value: index_type,
+      },
+      {
+        key: 'metric_type',
+        value: metric_type,
+      },
+      ...indexCreateParams.map(p => ({
+        key: p,
+        value: indexSetting[p],
+      })),
+    ];
+
+    handleCreate(params);
+  };
+
+  return (
+    <DialogTemplate
+      title={dialogTrans('createTitle', {
+        type: indexTrans('index'),
+        name: collectionName,
+      })}
+      handleCancel={handleCancel}
+      confirmLabel={btnTrans('create')}
+      handleConfirm={handleCreateIndex}
+      confirmDisabled={disabled}
+    >
+      <CreateForm
+        updateForm={updateStepTwoForm}
+        metricOptions={metricOptions}
+        indexOptions={indexOptions}
+        formValue={indexSetting}
+        checkIsValid={checkIsValid}
+        validation={validation}
+        indexParams={indexCreateParams}
+        indexTypeChange={onIndexTypeChange}
+      />
+    </DialogTemplate>
+  );
+};
+
+export default CreateIndex;

+ 205 - 0
client/src/pages/structure/CreateForm.tsx

@@ -0,0 +1,205 @@
+import { makeStyles, Theme, Typography } from '@material-ui/core';
+import { useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { ITextfieldConfig } from '../../components/customInput/Types';
+import CustomInput from '../../components/customInput/CustomInput';
+import CustomSelector from '../../components/customSelector/CustomSelector';
+import { m_OPTIONS } from '../../consts/Milvus';
+import { FormHelperType } from '../../types/Common';
+import { Option } from '../../components/customSelector/Types';
+
+const useStyles = makeStyles((theme: Theme) => ({
+  wrapper: {
+    maxWidth: '480px',
+  },
+  select: {
+    width: '100%',
+    marginBottom: theme.spacing(2),
+  },
+  paramTitle: {
+    margin: theme.spacing(2, 0),
+    color: '#82838e',
+    lineHeight: '20px',
+    fontSize: '14px',
+  },
+}));
+
+const CreateForm = (
+  props: FormHelperType & {
+    metricOptions: Option[];
+    indexOptions: Option[];
+    indexParams: string[];
+    indexTypeChange?: (type: string) => void;
+  }
+) => {
+  const classes = useStyles();
+  const {
+    updateForm,
+    formValue,
+    checkIsValid,
+    validation,
+    indexParams,
+    indexTypeChange,
+    indexOptions,
+    metricOptions,
+  } = props;
+
+  const { t: commonTrans } = useTranslation();
+  const { t: indexTrans } = useTranslation('index');
+  const { t: warningTrans } = useTranslation('warning');
+
+  const paramsConfig: ITextfieldConfig[] = useMemo(() => {
+    const result = [];
+    const generateNumberConfig = (
+      label: string,
+      key: string,
+      min: number,
+      max: number
+    ) => {
+      const config: ITextfieldConfig = {
+        label,
+        key,
+        onChange: value => {
+          updateForm(key, value);
+        },
+        variant: 'filled',
+        fullWidth: true,
+        type: 'number',
+        value: formValue[key],
+        validations: [
+          {
+            rule: 'require',
+            errorText: warningTrans('required', { name: label }),
+          },
+          {
+            rule: 'range',
+            errorText: warningTrans('range', { min, max }),
+            extraParam: {
+              min,
+              max,
+              type: 'number',
+            },
+          },
+        ],
+      };
+      return config;
+    };
+
+    const nlist = generateNumberConfig('nlist', 'nlist', 1, 65536);
+    const nTrees = generateNumberConfig('nTrees', 'n_trees', 1, 1024);
+
+    const M = generateNumberConfig('M', 'M', 4, 64);
+    const efConstruction = generateNumberConfig(
+      'Ef Construction',
+      'efConstruction',
+      8,
+      512
+    );
+
+    const outDegree = generateNumberConfig('out_degree', 'out_degree', 5, 300);
+    const candidatePoolSize = generateNumberConfig(
+      'candidate_pool_size',
+      'candidate_pool_size',
+      50,
+      1000
+    );
+    const searchLength = generateNumberConfig(
+      'search_length',
+      'search_length',
+      10,
+      300
+    );
+    const knng = generateNumberConfig('knng', 'knng', 5, 300);
+
+    if (indexParams.includes('nlist')) {
+      result.push(nlist);
+    }
+
+    if (indexParams.includes('M')) {
+      result.push(M);
+    }
+
+    if (indexParams.includes('efConstruction')) {
+      result.push(efConstruction);
+    }
+
+    if (indexParams.includes('n_trees')) {
+      result.push(nTrees);
+    }
+
+    if (indexParams.includes('out_degree')) {
+      result.push(outDegree);
+    }
+
+    if (indexParams.includes('candidate_pool_size')) {
+      result.push(candidatePoolSize);
+    }
+
+    if (indexParams.includes('search_length')) {
+      result.push(searchLength);
+    }
+
+    if (indexParams.includes('knng')) {
+      result.push(knng);
+    }
+
+    return result;
+  }, [updateForm, warningTrans, indexParams, formValue]);
+  return (
+    <div className={classes.wrapper}>
+      <CustomSelector
+        label={indexTrans('type')}
+        value={formValue.index_type}
+        options={indexOptions}
+        onChange={(e: { target: { value: unknown } }) => {
+          const type = e.target.value;
+          updateForm('index_type', type as string);
+          // reset metric type value
+          updateForm('metric_type', 'L2');
+          indexTypeChange && indexTypeChange(type as string);
+        }}
+        variant="filled"
+        classes={{ root: classes.select }}
+      />
+
+      <Typography className={classes.paramTitle}>
+        {commonTrans('param')}
+      </Typography>
+      <CustomSelector
+        label={indexTrans('metric')}
+        value={formValue.metric_type}
+        options={metricOptions}
+        onChange={(e: { target: { value: unknown } }) => {
+          const type = e.target.value;
+          updateForm('metric_type', type as string);
+        }}
+        variant="filled"
+        classes={{ root: classes.select }}
+      />
+
+      {indexParams.includes('m') && (
+        <CustomSelector
+          label="m"
+          value={Number(formValue.m)}
+          options={m_OPTIONS}
+          onChange={(e: { target: { value: unknown } }) =>
+            updateForm('m', e.target.value as string)
+          }
+          variant="filled"
+          classes={{ root: classes.select }}
+        />
+      )}
+
+      {paramsConfig.map(v => (
+        <CustomInput
+          type="text"
+          textConfig={v}
+          checkValid={checkIsValid}
+          validInfo={validation}
+          key={v.label}
+        />
+      ))}
+    </div>
+  );
+};
+export default CreateForm;

+ 167 - 0
client/src/pages/structure/IndexTypeElement.tsx

@@ -0,0 +1,167 @@
+import { FC, useCallback, useContext, useEffect, useState } from 'react';
+import Chip from '@material-ui/core/Chip';
+import CustomButton from '../../components/customButton/CustomButton';
+import { IndexHttp } from '../../http/Index';
+import { IndexState } from '../../types/Milvus';
+import {
+  FieldView,
+  IndexCreateParam,
+  IndexManageParam,
+  ParamPair,
+} from './Types';
+import StatusIcon from '../../components/status/StatusIcon';
+import { ChildrenStatusType } from '../../components/status/Types';
+import { useTranslation } from 'react-i18next';
+import { makeStyles, Theme } from '@material-ui/core';
+import icons from '../../components/icons/Icons';
+import { rootContext } from '../../context/Root';
+import CreateIndex from './Create';
+import DeleteTemplate from '../../components/customDialog/DeleteDialogTemplate';
+
+const useStyles = makeStyles((theme: Theme) => ({
+  item: {
+    paddingLeft: theme.spacing(1),
+  },
+  btn: {
+    '& span': {
+      textTransform: 'uppercase',
+      whiteSpace: 'nowrap',
+    },
+  },
+  chip: {
+    backgroundColor: '#e9e9ed',
+  },
+}));
+
+const IndexTypeElement: FC<{
+  data: FieldView;
+  collectionName: string;
+  cb: (collectionName: string) => void;
+}> = ({ data, collectionName, cb }) => {
+  const classes = useStyles();
+
+  const [status, setStatus] = useState<string>('');
+
+  const { t: indexTrans } = useTranslation('index');
+  const { t: btnTrans } = useTranslation('btn');
+  const { t: dialogTrans } = useTranslation('dialog');
+  const { t: successTrans } = useTranslation('success');
+
+  const { setDialog, handleCloseDialog, openSnackBar } =
+    useContext(rootContext);
+
+  const AddIcon = icons.add;
+  const DeleteIcon = icons.delete;
+
+  const fetchStatus = useCallback(async () => {
+    if (data._indexType !== '') {
+      const status = await IndexHttp.getIndexStatus(
+        collectionName,
+        data._fieldName
+      );
+      setStatus(status);
+    }
+  }, [collectionName, data._fieldName, data._indexType]);
+
+  useEffect(() => {
+    fetchStatus();
+  }, [fetchStatus]);
+
+  const requestCreateIndex = async (params: ParamPair[]) => {
+    const indexCreateParam: IndexCreateParam = {
+      collection_name: collectionName,
+      field_name: data._fieldName,
+      extra_params: params,
+    };
+    await IndexHttp.createIndex(indexCreateParam);
+    handleCloseDialog();
+    openSnackBar(indexTrans('createSuccess'));
+    cb(collectionName);
+  };
+
+  const handleCreate = () => {
+    setDialog({
+      open: true,
+      type: 'custom',
+      params: {
+        component: (
+          <CreateIndex
+            collectionName={collectionName}
+            fieldType={data._fieldType}
+            handleCancel={handleCloseDialog}
+            handleCreate={requestCreateIndex}
+          />
+        ),
+      },
+    });
+  };
+
+  const requestDeleteIndex = async () => {
+    const indexDeleteParam: IndexManageParam = {
+      collection_name: collectionName,
+      field_name: data._fieldName,
+    };
+
+    await IndexHttp.deleteIndex(indexDeleteParam);
+    handleCloseDialog();
+    openSnackBar(successTrans('delete', { name: indexTrans('index') }));
+    cb(collectionName);
+  };
+
+  const handleDelete = () => {
+    setDialog({
+      open: true,
+      type: 'custom',
+      params: {
+        component: (
+          <DeleteTemplate
+            label={btnTrans('delete')}
+            title={dialogTrans('deleteTitle', { type: indexTrans('index') })}
+            text={indexTrans('deleteWarning')}
+            handleDelete={requestDeleteIndex}
+          />
+        ),
+      },
+    });
+  };
+
+  const generateElement = () => {
+    if (
+      data._fieldType !== 'BinaryVector' &&
+      data._fieldType !== 'FloatVector'
+    ) {
+      return <div className={classes.item}>--</div>;
+    }
+
+    switch (data._indexType) {
+      case '': {
+        return (
+          <CustomButton
+            disabled={data._createIndexDisabled}
+            className={classes.btn}
+            onClick={handleCreate}
+          >
+            <AddIcon />
+            {indexTrans('create')}
+          </CustomButton>
+        );
+      }
+      default: {
+        return status === IndexState.InProgress ? (
+          <StatusIcon type={ChildrenStatusType.CREATING} />
+        ) : (
+          <Chip
+            label={data._indexType}
+            classes={{ root: classes.chip }}
+            deleteIcon={<DeleteIcon />}
+            onDelete={handleDelete}
+          />
+        );
+      }
+    }
+  };
+
+  return <>{generateElement()}</>;
+};
+
+export default IndexTypeElement;

+ 218 - 0
client/src/pages/structure/Structure.tsx

@@ -0,0 +1,218 @@
+import { makeStyles, Theme, Typography } from '@material-ui/core';
+import { FC, useCallback, useEffect, useState } from 'react';
+import MilvusGrid from '../../components/grid';
+import { ColDefinitionsType } from '../../components/grid/Types';
+import { useTranslation } from 'react-i18next';
+import { usePaginationHook } from '../../hooks/Pagination';
+import icons from '../../components/icons/Icons';
+import CustomToolTip from '../../components/customToolTip/CustomToolTip';
+import { FieldHttp } from '../../http/Field';
+import { FieldView } from './Types';
+import IndexTypeElement from './IndexTypeElement';
+import { DataType } from '../collections/Types';
+import { IndexHttp } from '../../http/Index';
+
+const useStyles = makeStyles((theme: Theme) => ({
+  wrapper: {
+    height: '100%',
+  },
+  icon: {
+    fontSize: '20px',
+    marginLeft: theme.spacing(0.5),
+  },
+  nameWrapper: {
+    display: 'flex',
+    alignItems: 'center',
+
+    '& .key': {
+      width: '16px',
+      height: '16px',
+      marginLeft: theme.spacing(0.5),
+    },
+  },
+
+  paramWrapper: {
+    '& .param': {
+      padding: theme.spacing(0.5),
+
+      marginRight: theme.spacing(2),
+
+      '& .key': {
+        color: '#82838e',
+        display: 'inline-block',
+        marginRight: theme.spacing(0.5),
+      },
+
+      '& .value': {
+        color: '#010e29',
+      },
+    },
+  },
+}));
+
+const Structure: FC<{
+  collectionName: string;
+}> = ({ collectionName }) => {
+  const classes = useStyles();
+  const { t: collectionTrans } = useTranslation('collection');
+  const { t: indexTrans } = useTranslation('index');
+  const InfoIcon = icons.info;
+
+  const [fields, setFields] = useState<FieldView[]>([]);
+  const [loading, setLoading] = useState<boolean>(true);
+
+  const {
+    pageSize,
+    handlePageSize,
+    currentPage,
+    handleCurrentPage,
+    total,
+    data: structureList,
+  } = usePaginationHook(fields);
+
+  const fetchStructureListWithIndex = async (
+    collectionName: string
+  ): Promise<FieldView[]> => {
+    const vectorTypes: DataType[] = ['BinaryVector', 'FloatVector'];
+    const indexList = await IndexHttp.getIndexInfo(collectionName);
+    const structureList = await FieldHttp.getFields(collectionName);
+    let fields: FieldView[] = [];
+    for (const structure of structureList) {
+      let field: FieldView = Object.assign(structure, {
+        _indexParameterPairs: [],
+        _indexType: '',
+      });
+      if (vectorTypes.includes(structure.data_type)) {
+        const index = indexList.find(i => i._fieldName === structure.name);
+
+        field._indexParameterPairs = index?._indexParameterPairs || [];
+        field._indexType = index?._indexType || '';
+        field._createIndexDisabled = indexList.length > 0;
+      }
+
+      fields = [...fields, field];
+    }
+    return fields;
+  };
+
+  const fetchFields = useCallback(
+    async (collectionName: string) => {
+      const KeyIcon = icons.key;
+
+      try {
+        const list = await fetchStructureListWithIndex(collectionName);
+        const fields: FieldView[] = list.map(f =>
+          Object.assign(f, {
+            _fieldNameElement: (
+              <div className={classes.nameWrapper}>
+                {f._fieldName}
+                {f._isPrimaryKey && <KeyIcon classes={{ root: 'key' }} />}
+              </div>
+            ),
+            _indexParamElement: (
+              <div className={classes.paramWrapper}>
+                {f._indexParameterPairs?.length > 0 ? (
+                  f._indexParameterPairs.map(p => (
+                    <span key={p.key} className="param">
+                      <Typography variant="caption" className="key">
+                        {`${p.key}:`}
+                      </Typography>
+                      <Typography variant="caption" className="value">
+                        {p.value}
+                      </Typography>
+                    </span>
+                  ))
+                ) : (
+                  <>--</>
+                )}
+              </div>
+            ),
+            _indexTypeElement: (
+              <IndexTypeElement
+                data={f}
+                collectionName={collectionName}
+                cb={fetchFields}
+              />
+            ),
+          })
+        );
+
+        setFields(fields);
+        setLoading(false);
+      } catch (err) {
+        setLoading(false);
+        throw err;
+      }
+    },
+    [classes.nameWrapper, classes.paramWrapper]
+  );
+
+  useEffect(() => {
+    fetchFields(collectionName);
+  }, [collectionName, fetchFields]);
+
+  const colDefinitions: ColDefinitionsType[] = [
+    {
+      id: '_fieldNameElement',
+      align: 'left',
+      disablePadding: true,
+      label: collectionTrans('fieldName'),
+    },
+    {
+      id: '_fieldType',
+      align: 'left',
+      disablePadding: false,
+      label: collectionTrans('fieldType'),
+    },
+    {
+      id: '_dimension',
+      align: 'left',
+      disablePadding: false,
+      label: (
+        <span className="flex-center">
+          {collectionTrans('dimension')}
+          <CustomToolTip title={collectionTrans('dimensionTooltip')}>
+            <InfoIcon classes={{ root: classes.icon }} />
+          </CustomToolTip>
+        </span>
+      ),
+    },
+    {
+      id: '_indexTypeElement',
+      align: 'left',
+      disablePadding: true,
+      label: indexTrans('type'),
+    },
+    {
+      id: '_indexParamElement',
+      align: 'left',
+      disablePadding: false,
+      label: indexTrans('param'),
+    },
+  ];
+
+  const handlePageChange = (e: any, page: number) => {
+    handleCurrentPage(page);
+  };
+
+  return (
+    <section className={classes.wrapper}>
+      <MilvusGrid
+        toolbarConfigs={[]}
+        colDefinitions={colDefinitions}
+        rows={structureList}
+        rowCount={total}
+        primaryKey="_fieldId"
+        openCheckBox={false}
+        showHoverStyle={false}
+        page={currentPage}
+        onChangePage={handlePageChange}
+        rowsPerPage={pageSize}
+        setRowsPerPage={handlePageSize}
+        isLoading={loading}
+      />
+    </section>
+  );
+};
+
+export default Structure;

+ 63 - 0
client/src/pages/structure/Types.ts

@@ -0,0 +1,63 @@
+import { ReactElement } from 'react';
+import { ManageRequestMethods } from '../../types/Common';
+import { DataType } from '../collections/Types';
+
+export enum INDEX_TYPES_ENUM {
+  IVF_FLAT = 'IVF_FLAT',
+  IVF_PQ = 'IVF_PQ',
+  IVF_SQ8 = 'IVF_SQ8',
+  IVF_SQ8_HYBRID = 'IVF_SQ8_HYBRID',
+  FLAT = 'FLAT',
+  HNSW = 'HNSW',
+  ANNOY = 'ANNOY',
+  RNSG = 'RNSG',
+}
+
+export interface FieldData {
+  _fieldId: string;
+  _isPrimaryKey: boolean;
+  _fieldName: string;
+  _fieldNameElement?: ReactElement;
+  _fieldType: DataType;
+  _dimension: string;
+}
+
+export interface FieldView extends FieldData, IndexView {
+  _createIndexDisabled?: boolean;
+}
+
+export interface Index {
+  params: { key: string; value: string }[];
+}
+
+export interface IndexView {
+  _fieldName: string;
+  _indexType: string;
+  _indexTypeElement?: ReactElement;
+  _indexParameterPairs: { key: string; value: string }[];
+  _indexParamElement?: ReactElement;
+}
+
+export type IndexType =
+  | 'FLAT'
+  | 'IVF_FLAT'
+  | 'IVF_SQ8'
+  // | 'IVF_SQ8_HYBRID'
+  | 'IVF_PQ'
+  | 'RNSG'
+  | 'HNSW'
+  | 'ANNOY';
+
+export interface IndexManageParam {
+  collection_name: string;
+  field_name: string;
+}
+
+export interface IndexCreateParam extends IndexManageParam {
+  extra_params: ParamPair[];
+}
+
+export interface ParamPair {
+  key: string;
+  value: string;
+}

+ 9 - 0
client/src/types/Common.ts

@@ -1,3 +1,5 @@
+import { IValidationItem } from '../hooks/Form';
+
 export interface KeyValuePair {
   label: string;
   value: string | number;
@@ -25,3 +27,10 @@ export enum ManageRequestMethods {
   DELETE = 'delete',
   CREATE = 'create',
 }
+
+export type FormHelperType = {
+  formValue: { [x: string]: any };
+  updateForm: (type: string, value: string) => void;
+  validation: { [key: string]: IValidationItem };
+  checkIsValid: Function;
+};

+ 62 - 3
client/src/utils/Form.ts

@@ -1,7 +1,11 @@
-import { IForm } from "../hooks/Form";
+import { Option } from '../components/customSelector/Types';
+import { METRIC_TYPES_VALUES } from '../consts/Milvus';
+import { IForm } from '../hooks/Form';
+import { DataType } from '../pages/collections/Types';
+import { IndexType } from '../pages/structure/Types';
 
 interface IInfo {
-  [key: string]: any
+  [key: string]: any;
 }
 
 export const formatForm = (info: IInfo): IForm[] => {
@@ -14,4 +18,59 @@ export const formatForm = (info: IInfo): IForm[] => {
     };
   });
   return form;
-}
+};
+
+export const getMetricOptions = (
+  indexType: IndexType,
+  fieldType: DataType
+): Option[] => {
+  const baseFloatOptions = [
+    {
+      value: METRIC_TYPES_VALUES.L2,
+      label: 'L2',
+    },
+    {
+      value: METRIC_TYPES_VALUES.IP,
+      label: 'IP',
+    },
+  ];
+
+  const baseBinaryOptions = [
+    {
+      value: METRIC_TYPES_VALUES.HAMMING,
+      label: 'Hamming',
+    },
+    {
+      value: METRIC_TYPES_VALUES.JACCARD,
+      label: 'Jaccard',
+    },
+    {
+      value: METRIC_TYPES_VALUES.TANIMOTO,
+      label: 'Tanimoto',
+    },
+  ];
+
+  const type = fieldType === 'FloatVector' ? 'ALL' : indexType;
+
+  const baseOptionsMap: { [key: string]: any } = {
+    BinaryVector: {
+      FLAT: [
+        ...baseBinaryOptions,
+        {
+          value: METRIC_TYPES_VALUES.SUBSTRUCTURE,
+          label: 'Substructure',
+        },
+        {
+          value: METRIC_TYPES_VALUES.SUPERSTRUCTURE,
+          label: 'Superstructure',
+        },
+      ],
+      IVF_FLAT: baseBinaryOptions,
+    },
+    FloatVector: {
+      ALL: baseFloatOptions,
+    },
+  };
+
+  return baseOptionsMap[fieldType][type];
+};

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

@@ -1,4 +1,4 @@
-import { METRIC_TYPES_VALUES } from '../consts/Milvus';
+import { MetricType, METRIC_TYPES_VALUES } from '../consts/Milvus';
 
 export type ValidType =
   | 'email'
@@ -28,14 +28,14 @@ export interface IExtraParam {
   type?: 'string' | 'number';
 
   // used for dimension
-  metricType?: number;
+  metricType?: MetricType;
   multipleNumber?: number;
 }
 export type CheckMap = {
   [key in ValidType]: boolean;
 };
 
-export const checkIsEmpty = (value: string): boolean => {
+export const checkEmptyValid = (value: string): boolean => {
   return value.trim() !== '';
 };
 
@@ -133,7 +133,7 @@ export const checkMultiple = (param: {
 
 export const checkDimension = (param: {
   value: string;
-  metricType?: number;
+  metricType?: MetricType;
   multipleNumber?: number;
 }): boolean => {
   const { value, metricType, multipleNumber } = param;
@@ -152,7 +152,7 @@ export const getCheckResult = (param: ICheckMapParam): boolean => {
 
   const checkMap = {
     email: checkEmail(value),
-    require: checkIsEmpty(value),
+    require: checkEmptyValid(value),
     confirm: value === extraParam?.compareValue,
     range: checkRange({
       value,

+ 4 - 4
client/src/utils/__test__/Validation.spec.ts

@@ -1,5 +1,5 @@
 import {
-  checkIsEmpty,
+  checkEmptyValid,
   checkEmail,
   checkPasswordStrength,
   checkRange,
@@ -9,9 +9,9 @@ import {
 } from '../Validation';
 
 describe('Test validation utils', () => {
-  test('test checkIsEmpty function', () => {
-    expect(checkIsEmpty('')).toBeFalsy();
-    expect(checkIsEmpty('test')).toBeTruthy();
+  test('test checkEmptyValid function', () => {
+    expect(checkEmptyValid('')).toBeFalsy();
+    expect(checkEmptyValid('test')).toBeTruthy();
   });
 
   test('test checkEmail function', () => {

+ 15 - 0
package.json

@@ -0,0 +1,15 @@
+{
+  "name": "milvus-insight",
+  "version": "0.1.0",
+  "description": "Milvus insight",
+  "license": "Apache-2.0",
+  "bugs": "https://github.com/milvus-io/milvus-insight/issues",
+  "release": {
+    "branches": [
+      "main",
+      "next",
+      "{name: 'beta', prerelease: true}"
+    ]
+  },
+  "private": true
+}

+ 1 - 1
server/package.json

@@ -30,7 +30,7 @@
     "@nestjs/swagger": "^4.8.0",
     "@types/passport-jwt": "^3.0.5",
     "@types/passport-local": "^1.0.33",
-    "@zilliz/milvus-sdk-node-dev": "^0.1.2",
+    "@zilliz/milvus-sdk-node-dev": "^0.1.4",
     "class-transformer": "^0.4.0",
     "class-validator": "^0.13.1",
     "passport": "^0.4.1",

+ 1 - 1
server/src/collections/collections.service.ts

@@ -91,7 +91,7 @@ export class CollectionsService {
           description: collectionInfo.schema.description,
           autoID: collectionInfo.schema.autoID,
           rowCount: findKeyValue(collectionStatistics.stats, ROW_COUNT),
-          // id: collectionInfo.collectionId
+          id: collectionInfo.collectionID,
         });
       }
     }

+ 3 - 0
server/src/schema/schema.service.ts

@@ -25,6 +25,9 @@ export class SchemaService {
 
   async describeIndex(data: DescribeIndexReq) {
     const res = await this.milvusClient.describeIndex(data);
+    if (res.status.error_code === 'IndexNotExist') {
+      return res;
+    }
     throwErrorFromSDK(res.status);
     return res;
   }

+ 4 - 4
server/yarn.lock

@@ -1287,10 +1287,10 @@
   resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d"
   integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
 
-"@zilliz/milvus-sdk-node-dev@^0.1.2":
-  version "0.1.2"
-  resolved "https://registry.yarnpkg.com/@zilliz/milvus-sdk-node-dev/-/milvus-sdk-node-dev-0.1.2.tgz#be5d546afec4e6a87732beadce2bd8f3261dac58"
-  integrity sha512-WScOEWG0Y9va5JBRoQsh1vfH9W0LOjl5HIZVwP/vcT7Kh0pC1E4N6iwaEpnSkne055D9SxJA7FhN23udrHTrMQ==
+"@zilliz/milvus-sdk-node-dev@^0.1.4":
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/@zilliz/milvus-sdk-node-dev/-/milvus-sdk-node-dev-0.1.4.tgz#858e83b0ba0a309cbdf0b87e313b3b41de0052a4"
+  integrity sha512-4REU6TxqjsIZ7a7mL1M19IPg3oBBfE9kJmC7XUmcFz1+F2fYWpl7Iw4R8g69trpzUNnaFIBgwjAnSb0oVrRLeQ==
   dependencies:
     "@grpc/grpc-js" "^1.2.12"
     "@grpc/proto-loader" "^0.6.0"