Browse Source

Merge branch 'main' into readme

ruiyi.jiang 4 years ago
parent
commit
d722014c2f
57 changed files with 1747 additions and 267 deletions
  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.
 # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
 
 
 # dependencies
 # dependencies
+node_modules
 /client/node_modules
 /client/node_modules
 /client/build
 /client/build
 /.pnp
 /.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.
       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`.
       Learn how to configure a non-root public URL by running `npm run build`.
     -->
     -->
-    <title>Milvus Admin</title>
+    <title>Milvus Insight</title>
   </head>
   </head>
 
 
   <body>
   <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();
   const classes = getStyles();
   return (
   return (
     <span className={classes.errWrapper}>
     <span className={classes.errWrapper}>
-      {Icons.error({
+      {/* {Icons.error({
         fontSize: 'small',
         fontSize: 'small',
         classes: {
         classes: {
           root: classes.errBtn,
           root: classes.errBtn,
         },
         },
-      })}
+      })} */}
       {hint}
       {hint}
     </span>
     </span>
   );
   );

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

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

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

@@ -66,6 +66,7 @@ export type TableType = {
   noData?: string;
   noData?: string;
   showHoverStyle?: boolean;
   showHoverStyle?: boolean;
   isLoading?: boolean;
   isLoading?: boolean;
+  setPageSize?: (size: number) => void;
 };
 };
 
 
 export type ColDefinitionsType = {
 export type ColDefinitionsType = {
@@ -93,6 +94,8 @@ export type ColDefinitionsType = {
 export type MilvusGridType = ToolBarType & {
 export type MilvusGridType = ToolBarType & {
   rowCount: number;
   rowCount: number;
   rowsPerPage?: number;
   rowsPerPage?: number;
+  // used to dynamic set page size by table container and row height
+  setRowsPerPage?: (size: number) => void;
   primaryKey: string;
   primaryKey: string;
   onChangePage?: (e: any, nextPageNum: number) => void;
   onChangePage?: (e: any, nextPageNum: number) => void;
   labelDisplayedRows?: (obj: any) => string;
   labelDisplayedRows?: (obj: any) => string;

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

@@ -98,10 +98,11 @@ const MilvusGrid: FC<MilvusGridType> = props => {
     searchForm,
     searchForm,
     openCheckBox = true,
     openCheckBox = true,
     disableSelect = false,
     disableSelect = false,
-    noData = t('grid.noData'),
+    noData = gridTrans.noData,
     showHoverStyle = true,
     showHoverStyle = true,
     selected = [],
     selected = [],
     setSelected = () => {},
     setSelected = () => {},
+    setRowsPerPage = () => {},
   } = props;
   } = props;
 
 
   const _isSelected = (row: { [x: string]: any }) => {
   const _isSelected = (row: { [x: string]: any }) => {
@@ -206,6 +207,7 @@ const MilvusGrid: FC<MilvusGridType> = props => {
           noData={noData}
           noData={noData}
           showHoverStyle={showHoverStyle}
           showHoverStyle={showHoverStyle}
           isLoading={isLoading}
           isLoading={isLoading}
+          setPageSize={setRowsPerPage}
         ></Table>
         ></Table>
         {rowCount ? (
         {rowCount ? (
           <TablePagination
           <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 InfoIcon } from '../../assets/icons/info.svg';
 import { ReactComponent as ReleaseIcon } from '../../assets/icons/release.svg';
 import { ReactComponent as ReleaseIcon } from '../../assets/icons/release.svg';
 import { ReactComponent as LoadIcon } from '../../assets/icons/load.svg';
 import { ReactComponent as LoadIcon } from '../../assets/icons/load.svg';
+import { ReactComponent as KeyIcon } from '../../assets/icons/key.svg';
 
 
 const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
 const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
   search: (props = {}) => <SearchIcon {...props} />,
   search: (props = {}) => <SearchIcon {...props} />,
@@ -68,6 +69,9 @@ const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
   load: (props = {}) => (
   load: (props = {}) => (
     <SvgIcon viewBox="0 0 24 24" component={LoadIcon} {...props} />
     <SvgIcon viewBox="0 0 24 24" component={LoadIcon} {...props} />
   ),
   ),
+  key: (props = {}) => (
+    <SvgIcon viewBox="0 0 16 16" component={KeyIcon} {...props} />
+  ),
 };
 };
 
 
 export default icons;
 export default icons;

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

@@ -23,4 +23,5 @@ export type IconsType =
   | 'info'
   | 'info'
   | 'release'
   | 'release'
   | 'load'
   | '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 {
 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 = [
 export const METRIC_TYPES = [
@@ -39,10 +17,6 @@ export const METRIC_TYPES = [
     value: METRIC_TYPES_VALUES.IP,
     value: METRIC_TYPES_VALUES.IP,
     label: 'IP',
     label: 'IP',
   },
   },
-  {
-    value: METRIC_TYPES_VALUES.HAMMING,
-    label: 'Hamming',
-  },
   {
   {
     value: METRIC_TYPES_VALUES.SUBSTRUCTURE,
     value: METRIC_TYPES_VALUES.SUBSTRUCTURE,
     label: 'Substructure',
     label: 'Substructure',
@@ -51,6 +25,10 @@ export const METRIC_TYPES = [
     value: METRIC_TYPES_VALUES.SUPERSTRUCTURE,
     value: METRIC_TYPES_VALUES.SUPERSTRUCTURE,
     label: 'Superstructure',
     label: 'Superstructure',
   },
   },
+  {
+    value: METRIC_TYPES_VALUES.HAMMING,
+    label: 'Hamming',
+  },
   {
   {
     value: METRIC_TYPES_VALUES.JACCARD,
     value: METRIC_TYPES_VALUES.JACCARD,
     label: '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';
 export type searchKeywordsType = 'nprobe' | 'ef' | 'search_k' | 'search_length';
 
 
@@ -124,18 +103,15 @@ export const m_OPTIONS = [
 
 
 export const INDEX_OPTIONS_MAP = {
 export const INDEX_OPTIONS_MAP = {
   FLOAT_POINT: Object.keys(INDEX_CONFIG).map(v => ({ label: v, value: v })),
   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: 'FLAT', value: 'FLAT' },
     { label: 'IVF_FLAT', value: 'IVF_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 { useState } from 'react';
 import { IValidation } from '../components/customInput/Types';
 import { IValidation } from '../components/customInput/Types';
-import { checkIsEmpty, getCheckResult } from '../utils/Validation';
+import { checkEmptyValid, getCheckResult } from '../utils/Validation';
 
 
 export interface IForm {
 export interface IForm {
   key: string;
   key: string;
@@ -95,7 +95,7 @@ export const useFormValidation = (form: IForm[]): IValidationInfo => {
 
 
   const checkFormValid = (form: IForm[]): boolean => {
   const checkFormValid = (form: IForm[]): boolean => {
     const requireCheckItems = form.filter(f => f.needCheck);
     const requireCheckItems = form.filter(f => f.needCheck);
-    if (requireCheckItems.some(item => !checkIsEmpty(item.value))) {
+    if (requireCheckItems.some(item => !checkEmptyValid(item.value))) {
       return false;
       return false;
     }
     }
 
 

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

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

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

@@ -55,9 +55,7 @@ export default class BaseModel {
    */
    */
   static async create(options: updateParamsType) {
   static async create(options: updateParamsType) {
     const { path, data } = options;
     const { path, data } = options;
-
     const res = await http.post(path, data);
     const res = await http.post(path, data);
-
     return new this(res.data.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 description!: string;
   private rowCount!: string;
   private rowCount!: string;
   private index_status!: string;
   private index_status!: string;
+  private id!: string;
 
 
   static COLLECTIONS_URL = '/collections';
   static COLLECTIONS_URL = '/collections';
   static COLLECTIONS_INDEX_STATUS_URL = '/collections/indexes/status';
   static COLLECTIONS_INDEX_STATUS_URL = '/collections/indexes/status';
@@ -60,7 +61,7 @@ export class CollectionHttp extends BaseModel implements CollectionView {
   }
   }
 
 
   get _id() {
   get _id() {
-    return '12';
+    return this.id;
   }
   }
 
 
   get _name() {
   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() {
   get _id() {
     return this.id;
     return this.id;
   }
   }

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

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

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

@@ -21,6 +21,7 @@ const commonTrans = {
     copy: 'Copy',
     copy: 'Copy',
     copied: 'Copied',
     copied: 'Copied',
   },
   },
+  param: 'Parameter',
 };
 };
 
 
 export default commonTrans;
 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.`,
   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.`,
   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;
 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:
   deleteWarning:
     'You are trying to delete partition. This action cannot be undone.',
     'You are trying to delete partition. This action cannot be undone.',
+  deletePartitionError: 'default partition cannot be deleted',
 };
 };
 
 
 export default partitionTrans;
 export default partitionTrans;

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

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

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

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

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

@@ -21,6 +21,7 @@ const commonTrans = {
     copy: 'Copy',
     copy: 'Copy',
     copied: 'Copied',
     copied: 'Copied',
   },
   },
+  param: 'Parameter',
 };
 };
 
 
 export default commonTrans;
 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.`,
   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.`,
   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;
 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:
   deleteWarning:
     'You are trying to delete partition. This action cannot be undone.',
     'You are trying to delete partition. This action cannot be undone.',
+  deletePartitionError: 'default partition cannot be deleted',
 };
 };
 
 
 export default partitionTrans;
 export default partitionTrans;

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

@@ -1,12 +1,8 @@
 const warningTrans = {
 const warningTrans = {
-  closeDialog: 'Will lose your data, are you sure to leave?',
-
   required: '{{name}} is required',
   required: '{{name}} is required',
   positive: '{{name}} should be positive',
   positive: '{{name}} should be positive',
   integer: '{{name}} should be integers',
   integer: '{{name}} should be integers',
   range: 'range is {{min}} ~ {{max}}',
   range: 'range is {{min}} ~ {{max}}',
-
-  deletePartition: 'default partition cannot be deleted',
 };
 };
 
 
 export default warningTrans;
 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 partitionEn from './en/partition';
 import successEn from './en/success';
 import successEn from './en/success';
 import successCn from './cn/success';
 import successCn from './cn/success';
+import indexEn from './en/index';
+import indexCn from './cn/index';
 
 
 export const resources = {
 export const resources = {
   cn: {
   cn: {
@@ -31,6 +33,7 @@ export const resources = {
     dialog: dialogCn,
     dialog: dialogCn,
     partition: partitionCn,
     partition: partitionCn,
     success: successCn,
     success: successCn,
+    index: indexCn,
   },
   },
   en: {
   en: {
     translation: commonEn,
     translation: commonEn,
@@ -42,6 +45,7 @@ export const resources = {
     dialog: dialogEn,
     dialog: dialogEn,
     partition: partitionEn,
     partition: partitionEn,
     success: successEn,
     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 { useHistory, useLocation, useParams } from 'react-router-dom';
 import { useMemo } from 'react';
 import { useMemo } from 'react';
 import { parseLocationSearch } from '../../utils/Format';
 import { parseLocationSearch } from '../../utils/Format';
+import Structure from '../structure/Structure';
 
 
 enum TAB_EMUM {
 enum TAB_EMUM {
   'partition',
   'partition',
@@ -24,7 +25,7 @@ const Collection = () => {
   const history = useHistory();
   const history = useHistory();
   const location = useLocation();
   const location = useLocation();
 
 
-  const { t } = useTranslation('collection');
+  const { t: collectionTrans } = useTranslation('collection');
 
 
   const activeTabIndex = useMemo(() => {
   const activeTabIndex = useMemo(() => {
     const { activeIndex } = location.search
     const { activeIndex } = location.search
@@ -40,12 +41,12 @@ const Collection = () => {
 
 
   const tabs: ITab[] = [
   const tabs: ITab[] = [
     {
     {
-      label: t('partitionTab'),
+      label: collectionTrans('partitionTab'),
       component: <Partitions collectionName={collectionName} />,
       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 { ALL_ROUTER_TYPES } from '../../router/Types';
 import MilvusGrid from '../../components/grid';
 import MilvusGrid from '../../components/grid';
 import CustomToolBar from '../../components/grid/ToolBar';
 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 { ColDefinitionsType, ToolBarConfig } from '../../components/grid/Types';
 import { usePaginationHook } from '../../hooks/Pagination';
 import { usePaginationHook } from '../../hooks/Pagination';
 import icons from '../../components/icons/Icons';
 import icons from '../../components/icons/Icons';
@@ -46,6 +46,7 @@ const Collections = () => {
   const [collections, setCollections] = useState<CollectionView[]>([]);
   const [collections, setCollections] = useState<CollectionView[]>([]);
   const {
   const {
     pageSize,
     pageSize,
+    handlePageSize,
     currentPage,
     currentPage,
     handleCurrentPage,
     handleCurrentPage,
     total,
     total,
@@ -58,7 +59,7 @@ const Collections = () => {
 
 
   const { setDialog, handleCloseDialog, openSnackBar } =
   const { setDialog, handleCloseDialog, openSnackBar } =
     useContext(rootContext);
     useContext(rootContext);
-  const { t } = useTranslation('collection');
+  const { t: collectionTrans } = useTranslation('collection');
   const { t: btnTrans } = useTranslation('btn');
   const { t: btnTrans } = useTranslation('btn');
   const { t: dialogTrans } = useTranslation('dialog');
   const { t: dialogTrans } = useTranslation('dialog');
   const { t: successTrans } = useTranslation('success');
   const { t: successTrans } = useTranslation('success');
@@ -106,26 +107,36 @@ const Collections = () => {
 
 
   const handleCreateCollection = async (param: CollectionCreateParam) => {
   const handleCreateCollection = async (param: CollectionCreateParam) => {
     const data: CollectionCreateParam = JSON.parse(JSON.stringify(param));
     const data: CollectionCreateParam = JSON.parse(JSON.stringify(param));
-    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);
     await CollectionHttp.createCollection(data);
     handleCloseDialog();
     handleCloseDialog();
-    openSnackBar(successTrans('create', { name: t('collection') }));
+    openSnackBar(
+      successTrans('create', { name: collectionTrans('collection') })
+    );
     fetchData();
     fetchData();
   };
   };
 
 
   const handleRelease = async (data: CollectionView) => {
   const handleRelease = async (data: CollectionView) => {
     const res = await CollectionHttp.releaseCollection(data._name);
     const res = await CollectionHttp.releaseCollection(data._name);
-    openSnackBar(successTrans('release', { name: t('collection') }));
+    openSnackBar(
+      successTrans('release', { name: collectionTrans('collection') })
+    );
     fetchData();
     fetchData();
     return res;
     return res;
   };
   };
 
 
   const handleLoad = async (data: CollectionView) => {
   const handleLoad = async (data: CollectionView) => {
     const res = await CollectionHttp.loadCollection(data._name);
     const res = await CollectionHttp.loadCollection(data._name);
-    openSnackBar(successTrans('load', { name: t('collection') }));
+    openSnackBar(successTrans('load', { name: collectionTrans('collection') }));
     fetchData();
     fetchData();
     return res;
     return res;
   };
   };
@@ -134,7 +145,9 @@ const Collections = () => {
     for (const item of selectedCollections) {
     for (const item of selectedCollections) {
       await CollectionHttp.deleteCollection(item._name);
       await CollectionHttp.deleteCollection(item._name);
     }
     }
-    openSnackBar(successTrans('delete', { name: t('collection') }));
+    openSnackBar(
+      successTrans('delete', { name: collectionTrans('collection') })
+    );
     fetchData();
     fetchData();
     handleCloseDialog();
     handleCloseDialog();
     setSelectedCollections([]);
     setSelectedCollections([]);
@@ -142,7 +155,7 @@ const Collections = () => {
 
 
   const toolbarConfigs: ToolBarConfig[] = [
   const toolbarConfigs: ToolBarConfig[] = [
     {
     {
-      label: t('create'),
+      label: collectionTrans('create'),
       onClick: () => {
       onClick: () => {
         setDialog({
         setDialog({
           open: true,
           open: true,
@@ -166,15 +179,17 @@ const Collections = () => {
             component: (
             component: (
               <DeleteTemplate
               <DeleteTemplate
                 label={btnTrans('delete')}
                 label={btnTrans('delete')}
-                title={dialogTrans('deleteTitle', { type: t('collection') })}
-                text={t('deleteWarning')}
+                title={dialogTrans('deleteTitle', {
+                  type: collectionTrans('collection'),
+                })}
+                text={collectionTrans('deleteWarning')}
                 handleDelete={handleDelete}
                 handleDelete={handleDelete}
               />
               />
             ),
             ),
           },
           },
         });
         });
       },
       },
-      label: t('delete'),
+      label: collectionTrans('delete'),
       icon: 'delete',
       icon: 'delete',
       disabled: data => data.length === 0,
       disabled: data => data.length === 0,
     },
     },
@@ -185,19 +200,19 @@ const Collections = () => {
       id: '_id',
       id: '_id',
       align: 'left',
       align: 'left',
       disablePadding: true,
       disablePadding: true,
-      label: t('id'),
+      label: collectionTrans('id'),
     },
     },
     {
     {
       id: 'nameElement',
       id: 'nameElement',
       align: 'left',
       align: 'left',
       disablePadding: true,
       disablePadding: true,
-      label: t('name'),
+      label: collectionTrans('name'),
     },
     },
     {
     {
       id: 'statusElement',
       id: 'statusElement',
       align: 'left',
       align: 'left',
       disablePadding: false,
       disablePadding: false,
-      label: t('status'),
+      label: collectionTrans('status'),
     },
     },
     {
     {
       id: '_rowCount',
       id: '_rowCount',
@@ -205,8 +220,8 @@ const Collections = () => {
       disablePadding: false,
       disablePadding: false,
       label: (
       label: (
         <span className="flex-center">
         <span className="flex-center">
-          {t('rowCount')}
-          <CustomToolTip title={t('tooltip')}>
+          {collectionTrans('rowCount')}
+          <CustomToolTip title={collectionTrans('tooltip')}>
             <InfoIcon classes={{ root: classes.icon }} />
             <InfoIcon classes={{ root: classes.icon }} />
           </CustomToolTip>
           </CustomToolTip>
         </span>
         </span>
@@ -216,7 +231,7 @@ const Collections = () => {
       id: '_desc',
       id: '_desc',
       align: 'left',
       align: 'left',
       disablePadding: false,
       disablePadding: false,
-      label: t('desc'),
+      label: collectionTrans('desc'),
     },
     },
     {
     {
       id: 'indexCreatingElement',
       id: 'indexCreatingElement',
@@ -277,6 +292,7 @@ const Collections = () => {
           page={currentPage}
           page={currentPage}
           onChangePage={handlePageChange}
           onChangePage={handlePageChange}
           rowsPerPage={pageSize}
           rowsPerPage={pageSize}
+          setRowsPerPage={handlePageSize}
           isLoading={loading}
           isLoading={loading}
         />
         />
       ) : (
       ) : (
@@ -285,7 +301,7 @@ const Collections = () => {
           <EmptyCard
           <EmptyCard
             wrapperClass={`page-empty-card ${classes.emptyWrapper}`}
             wrapperClass={`page-empty-card ${classes.emptyWrapper}`}
             icon={<CollectionIcon />}
             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 { ITextfieldConfig } from '../../components/customInput/Types';
 import { rootContext } from '../../context/Root';
 import { rootContext } from '../../context/Root';
 import { useFormValidation } from '../../hooks/Form';
 import { useFormValidation } from '../../hooks/Form';
-import { generateId } from '../../utils/Common';
 import { formatForm } from '../../utils/Form';
 import { formatForm } from '../../utils/Form';
 import CreateFields from './CreateFields';
 import CreateFields from './CreateFields';
 import {
 import {
@@ -43,7 +42,7 @@ const useStyles = makeStyles((theme: Theme) => ({
 const CreateCollection: FC<CollectionCreateProps> = ({ handleCreate }) => {
 const CreateCollection: FC<CollectionCreateProps> = ({ handleCreate }) => {
   const classes = useStyles();
   const classes = useStyles();
   const { handleCloseDialog } = useContext(rootContext);
   const { handleCloseDialog } = useContext(rootContext);
-  const { t } = useTranslation('collection');
+  const { t: collectionTrans } = useTranslation('collection');
   const { t: btnTrans } = useTranslation('btn');
   const { t: btnTrans } = useTranslation('btn');
   const { t: warningTrans } = useTranslation('warning');
   const { t: warningTrans } = useTranslation('warning');
 
 
@@ -52,31 +51,45 @@ const CreateCollection: FC<CollectionCreateProps> = ({ handleCreate }) => {
     description: '',
     description: '',
     autoID: true,
     autoID: true,
   });
   });
+
   const [fields, setFields] = useState<Field[]>([
   const [fields, setFields] = useState<Field[]>([
     {
     {
       data_type: DataTypeEnum.Int64,
       data_type: DataTypeEnum.Int64,
       is_primary_key: true,
       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: '',
       description: '',
       isDefault: true,
       isDefault: true,
-      id: generateId(),
+      id: '1',
     },
     },
     {
     {
       data_type: DataTypeEnum.FloatVector,
       data_type: DataTypeEnum.FloatVector,
       is_primary_key: false,
       is_primary_key: false,
-      name: '',
-      dimension: '',
+      name: null,
+      dimension: '128',
       description: '',
       description: '',
       isDefault: true,
       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 checkedForm = useMemo(() => {
     const { collection_name } = form;
     const { collection_name } = form;
     return formatForm({ collection_name });
     return formatForm({ collection_name });
   }, [form]);
   }, [form]);
+
   const { validation, checkIsValid, disabled } = useFormValidation(checkedForm);
   const { validation, checkIsValid, disabled } = useFormValidation(checkedForm);
 
 
   const changeIsAutoID = (value: boolean) => {
   const changeIsAutoID = (value: boolean) => {
@@ -92,21 +105,23 @@ const CreateCollection: FC<CollectionCreateProps> = ({ handleCreate }) => {
 
 
   const generalInfoConfigs: ITextfieldConfig[] = [
   const generalInfoConfigs: ITextfieldConfig[] = [
     {
     {
-      label: t('name'),
-      key: 'name',
+      label: collectionTrans('name'),
+      key: 'collection_name',
       value: form.collection_name,
       value: form.collection_name,
       onChange: (value: string) => handleInputChange('collection_name', value),
       onChange: (value: string) => handleInputChange('collection_name', value),
       variant: 'filled',
       variant: 'filled',
       validations: [
       validations: [
         {
         {
           rule: 'require',
           rule: 'require',
-          errorText: warningTrans('required', { name: t('name') }),
+          errorText: warningTrans('required', {
+            name: collectionTrans('name'),
+          }),
         },
         },
       ],
       ],
       className: classes.input,
       className: classes.input,
     },
     },
     {
     {
-      label: t('description'),
+      label: collectionTrans('description'),
       key: 'description',
       key: 'description',
       value: form.description,
       value: form.description,
       onChange: (value: string) => handleInputChange('description', value),
       onChange: (value: string) => handleInputChange('description', value),
@@ -117,24 +132,33 @@ const CreateCollection: FC<CollectionCreateProps> = ({ handleCreate }) => {
   ];
   ];
 
 
   const handleCreateCollection = () => {
   const handleCreateCollection = () => {
+    const vectorType = [DataTypeEnum.BinaryVector, DataTypeEnum.FloatVector];
     const param: CollectionCreateParam = {
     const param: CollectionCreateParam = {
       ...form,
       ...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);
     handleCreate(param);
   };
   };
 
 
   return (
   return (
     <DialogTemplate
     <DialogTemplate
-      title={t('createTitle')}
+      title={collectionTrans('createTitle')}
       handleCancel={handleCloseDialog}
       handleCancel={handleCloseDialog}
       confirmLabel={btnTrans('create')}
       confirmLabel={btnTrans('create')}
       handleConfirm={handleCreateCollection}
       handleConfirm={handleCreateCollection}
-      confirmDisabled={disabled || !fieldsAllValid}
+      confirmDisabled={disabled || !allFieldsValid}
     >
     >
       <form>
       <form>
         <fieldset className={classes.fieldset}>
         <fieldset className={classes.fieldset}>
-          <legend>{t('general')}</legend>
+          <legend>{collectionTrans('general')}</legend>
           {generalInfoConfigs.map(config => (
           {generalInfoConfigs.map(config => (
             <CustomInput
             <CustomInput
               key={config.key}
               key={config.key}
@@ -147,11 +171,11 @@ const CreateCollection: FC<CollectionCreateProps> = ({ handleCreate }) => {
         </fieldset>
         </fieldset>
 
 
         <fieldset className={classes.fieldset}>
         <fieldset className={classes.fieldset}>
-          <legend>{t('structure')}</legend>
+          <legend>{collectionTrans('structure')}</legend>
           <CreateFields
           <CreateFields
             fields={fields}
             fields={fields}
             setFields={setFields}
             setFields={setFields}
-            setfieldsAllValid={setFieldsAllValid}
+            setFieldsValidation={setFieldsValidation}
             autoID={form.autoID}
             autoID={form.autoID}
             setAutoID={changeIsAutoID}
             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 CustomButton from '../../components/customButton/CustomButton';
 import CustomSelector from '../../components/customSelector/CustomSelector';
 import CustomSelector from '../../components/customSelector/CustomSelector';
 import icons from '../../components/icons/Icons';
 import icons from '../../components/icons/Icons';
+import { PRIMARY_KEY_FIELD } from '../../consts/Milvus';
 import { generateId } from '../../utils/Common';
 import { generateId } from '../../utils/Common';
 import { getCreateFieldType } from '../../utils/Format';
 import { getCreateFieldType } from '../../utils/Format';
+import { checkEmptyValid, getCheckResult } from '../../utils/Validation';
 import {
 import {
   ALL_OPTIONS,
   ALL_OPTIONS,
   AUTO_ID_OPTIONS,
   AUTO_ID_OPTIONS,
@@ -38,6 +40,7 @@ const useStyles = makeStyles((theme: Theme) => ({
   },
   },
   select: {
   select: {
     width: '160px',
     width: '160px',
+    marginBottom: '22px',
   },
   },
   descInput: {
   descInput: {
     minWidth: '270px',
     minWidth: '270px',
@@ -57,20 +60,34 @@ const useStyles = makeStyles((theme: Theme) => ({
   mb2: {
   mb2: {
     marginBottom: theme.spacing(2),
     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> = ({
 const CreateFields: FC<CreateFieldsProps> = ({
   fields,
   fields,
   setFields,
   setFields,
-  // @TODO validation
-  setfieldsAllValid,
   setAutoID,
   setAutoID,
   autoID,
   autoID,
+  setFieldsValidation,
 }) => {
 }) => {
-  const { t } = useTranslation('collection');
+  const { t: collectionTrans } = useTranslation('collection');
+  const { t: warningTrans } = useTranslation('warning');
+
   const classes = useStyles();
   const classes = useStyles();
 
 
-  const primaryInt64Value = 'INT64 (Primary key)';
   const AddIcon = icons.add;
   const AddIcon = icons.add;
   const RemoveIcon = icons.remove;
   const RemoveIcon = icons.remove;
 
 
@@ -79,49 +96,146 @@ const CreateFields: FC<CreateFieldsProps> = ({
     label: string,
     label: string,
     value: number,
     value: number,
     onChange: (value: DataTypeEnum) => void
     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 => {
     const newFields = fields.map(f => {
       if (f.id !== id) {
       if (f.id !== id) {
         return f;
         return f;
@@ -131,25 +245,33 @@ const CreateFields: FC<CreateFieldsProps> = ({
         [key]: value,
         [key]: value,
       };
       };
     });
     });
-
     setFields(newFields);
     setFields(newFields);
   };
   };
 
 
   const handleAddNewField = () => {
   const handleAddNewField = () => {
+    const id = generateId();
     const newDefaultItem: Field = {
     const newDefaultItem: Field = {
-      name: '',
+      name: null,
       data_type: DataTypeEnum.Int16,
       data_type: DataTypeEnum.Int16,
       is_primary_key: false,
       is_primary_key: false,
       description: '',
       description: '',
       isDefault: false,
       isDefault: false,
-      id: generateId(),
+      dimension: '128',
+      id,
+    };
+    const newValidation = {
+      id,
+      name: false,
+      dimension: true,
     };
     };
     setFields([...fields, newDefaultItem]);
     setFields([...fields, newDefaultItem]);
+    setFieldsValidation(v => [...v, newValidation]);
   };
   };
 
 
   const handleRemoveField = (field: Field) => {
   const handleRemoveField = (field: Field) => {
     const newFields = fields.filter(f => f.id !== field.id);
     const newFields = fields.filter(f => f.id !== field.id);
     setFields(newFields);
     setFields(newFields);
+    setFieldsValidation(v => v.filter(item => item.id !== field.id));
   };
   };
 
 
   const generatePrimaryKeyRow = (
   const generatePrimaryKeyRow = (
@@ -158,21 +280,18 @@ const CreateFields: FC<CreateFieldsProps> = ({
   ): ReactElement => {
   ): ReactElement => {
     return (
     return (
       <div className={`${classes.rowWrapper} ${classes.mb3}`}>
       <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
         <CustomSelector
-          label={t('autoId')}
+          label={collectionTrans('autoId')}
           options={AUTO_ID_OPTIONS}
           options={AUTO_ID_OPTIONS}
           value={autoID ? 'true' : 'false'}
           value={autoID ? 'true' : 'false'}
           onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
           onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
@@ -183,12 +302,7 @@ const CreateFields: FC<CreateFieldsProps> = ({
           classes={{ root: classes.select }}
           classes={{ root: classes.select }}
         />
         />
 
 
-        {getInput(
-          t('description'),
-          field.description,
-          (value: string) => changeFields(field.id, 'description', value),
-          classes.descInput
-        )}
+        {generateDesc(field)}
       </div>
       </div>
     );
     );
   };
   };
@@ -199,33 +313,21 @@ const CreateFields: FC<CreateFieldsProps> = ({
         <div className={`${classes.rowWrapper} ${classes.mb2}`}>
         <div className={`${classes.rowWrapper} ${classes.mb2}`}>
           {getSelector(
           {getSelector(
             'vector',
             'vector',
-            t('fieldType'),
+            collectionTrans('fieldType'),
             field.data_type,
             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>
         </div>
 
 
         <CustomButton onClick={handleAddNewField} className={classes.mb2}>
         <CustomButton onClick={handleAddNewField} className={classes.mb2}>
           <AddIcon />
           <AddIcon />
-          <span className={classes.btnTxt}>{t('newBtn')}</span>
+          <span className={classes.btnTxt}>{collectionTrans('newBtn')}</span>
         </CustomButton>
         </CustomButton>
       </>
       </>
     );
     );
@@ -241,22 +343,15 @@ const CreateFields: FC<CreateFieldsProps> = ({
         >
         >
           <RemoveIcon />
           <RemoveIcon />
         </IconButton>
         </IconButton>
-        {getInput(t('fieldName'), field.name, (value: string) =>
-          changeFields(field.id, 'name', value)
-        )}
+        {generateFieldName(field)}
         {getSelector(
         {getSelector(
           'all',
           'all',
-          t('fieldType'),
+          collectionTrans('fieldType'),
           field.data_type,
           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>
       </div>
     );
     );
   };
   };
@@ -267,28 +362,16 @@ const CreateFields: FC<CreateFieldsProps> = ({
         <IconButton classes={{ root: classes.iconBtn }} aria-label="delete">
         <IconButton classes={{ root: classes.iconBtn }} aria-label="delete">
           <RemoveIcon />
           <RemoveIcon />
         </IconButton>
         </IconButton>
-        {getInput(t('fieldName'), field.name, (value: string) =>
-          changeFields(field.id, 'name', value)
-        )}
+        {generateFieldName(field)}
         {getSelector(
         {getSelector(
           'all',
           'all',
-          t('fieldType'),
+          collectionTrans('fieldType'),
           field.data_type,
           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>
       </div>
     );
     );
   };
   };

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

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

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

@@ -48,9 +48,9 @@ const Connect = () => {
   const { setAddress } = useContext(authContext);
   const { setAddress } = useContext(authContext);
   const { openSnackBar } = useContext(rootContext);
   const { openSnackBar } = useContext(rootContext);
   const classes = useStyles();
   const classes = useStyles();
-  const { t } = useTranslation();
+  const { t: commonTrans } = useTranslation();
   const { t: warningTrans } = useTranslation('warning');
   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: btnTrans } = useTranslation('btn');
   const { t: successTrans } = useTranslation('success');
   const { t: successTrans } = useTranslation('success');
 
 

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

@@ -28,24 +28,26 @@ const useStyles = makeStyles((theme: Theme) => ({
 const Overview = () => {
 const Overview = () => {
   useNavigationHook(ALL_ROUTER_TYPES.OVERVIEW);
   useNavigationHook(ALL_ROUTER_TYPES.OVERVIEW);
   const classes = useStyles();
   const classes = useStyles();
-  const { t } = useTranslation('overview');
+  const { t: overviewTrans } = useTranslation('overview');
   const { t: collectionTrans } = useTranslation('collection');
   const { t: collectionTrans } = useTranslation('collection');
 
 
   const mockStatistics: StatisticsCardProps = {
   const mockStatistics: StatisticsCardProps = {
     data: [
     data: [
       {
       {
-        label: t('load'),
+        label: overviewTrans('load'),
         value: formatNumber(4337),
         value: formatNumber(4337),
         valueColor: '#07d197',
         valueColor: '#07d197',
       },
       },
       {
       {
-        label: t('all'),
+        label: overviewTrans('all'),
         value: formatNumber(30000),
         value: formatNumber(30000),
         valueColor: '#06aff2',
         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',
         valueColor: '#0689d2',
       },
       },
     ],
     ],
@@ -94,7 +96,9 @@ const Overview = () => {
   return (
   return (
     <section className="page-wrapper">
     <section className="page-wrapper">
       <StatisticsCard data={mockStatistics.data} />
       <StatisticsCard data={mockStatistics.data} />
-      <Typography className={classes.collectionTitle}>{t('load')}</Typography>
+      <Typography className={classes.collectionTitle}>
+        {overviewTrans('load')}
+      </Typography>
       {mockCollections.length > 0 ? (
       {mockCollections.length > 0 ? (
         <div className={classes.cardsWrapper}>
         <div className={classes.cardsWrapper}>
           {mockCollections.map(collection => (
           {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 RightArrowIcon = icons.rightArrow;
   const InfoIcon = icons.info;
   const InfoIcon = icons.info;
   const ReleaseIcon = icons.release;
   const ReleaseIcon = icons.release;
-  const { t } = useTranslation('collection');
+  const { t: collectionTrans } = useTranslation('collection');
   const { t: btnTrans } = useTranslation('btn');
   const { t: btnTrans } = useTranslation('btn');
 
 
   const handleRelease = () => {};
   const handleRelease = () => {};
@@ -79,8 +79,8 @@ const CollectionCard: FC<CollectionCardProps> = ({
         <RightArrowIcon classes={{ root: classes.icon }} />
         <RightArrowIcon classes={{ root: classes.icon }} />
       </Link>
       </Link>
       <div className={classes.content}>
       <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 }} />
           <InfoIcon classes={{ root: classes.icon }} />
         </CustomToolTip>
         </CustomToolTip>
         <Typography className={classes.rowCount}>{rowCount}</Typography>
         <Typography className={classes.rowCount}>{rowCount}</Typography>

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

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

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

@@ -32,7 +32,6 @@ const Partitions: FC<{
   const classes = useStyles();
   const classes = useStyles();
   const { t } = useTranslation('partition');
   const { t } = useTranslation('partition');
   const { t: successTrans } = useTranslation('success');
   const { t: successTrans } = useTranslation('success');
-  const { t: warningTrans } = useTranslation('warning');
   const { t: btnTrans } = useTranslation('btn');
   const { t: btnTrans } = useTranslation('btn');
   const { t: dialogTrans } = useTranslation('dialog');
   const { t: dialogTrans } = useTranslation('dialog');
   const InfoIcon = icons.info;
   const InfoIcon = icons.info;
@@ -46,6 +45,7 @@ const Partitions: FC<{
   const [partitions, setPartitions] = useState<PartitionView[]>([]);
   const [partitions, setPartitions] = useState<PartitionView[]>([]);
   const {
   const {
     pageSize,
     pageSize,
+    handlePageSize,
     currentPage,
     currentPage,
     handleCurrentPage,
     handleCurrentPage,
     total,
     total,
@@ -88,7 +88,16 @@ const Partitions: FC<{
     handleCloseDialog();
     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 handleLoad = async (data: PartitionView) => {
     const param: PartitionParam = {
     const param: PartitionParam = {
@@ -145,7 +154,7 @@ const Partitions: FC<{
         selectedPartitions.length === 0 ||
         selectedPartitions.length === 0 ||
         selectedPartitions.some(p => p._name === '_default'),
         selectedPartitions.some(p => p._name === '_default'),
       tooltip: 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}
         page={currentPage}
         onChangePage={handlePageChange}
         onChangePage={handlePageChange}
         rowsPerPage={pageSize}
         rowsPerPage={pageSize}
+        setRowsPerPage={handlePageSize}
         isLoading={loading}
         isLoading={loading}
       />
       />
     </section>
     </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 {
 export interface KeyValuePair {
   label: string;
   label: string;
   value: string | number;
   value: string | number;
@@ -25,3 +27,10 @@ export enum ManageRequestMethods {
   DELETE = 'delete',
   DELETE = 'delete',
   CREATE = 'create',
   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 {
 interface IInfo {
-  [key: string]: any
+  [key: string]: any;
 }
 }
 
 
 export const formatForm = (info: IInfo): IForm[] => {
 export const formatForm = (info: IInfo): IForm[] => {
@@ -14,4 +18,59 @@ export const formatForm = (info: IInfo): IForm[] => {
     };
     };
   });
   });
   return form;
   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 =
 export type ValidType =
   | 'email'
   | 'email'
@@ -28,14 +28,14 @@ export interface IExtraParam {
   type?: 'string' | 'number';
   type?: 'string' | 'number';
 
 
   // used for dimension
   // used for dimension
-  metricType?: number;
+  metricType?: MetricType;
   multipleNumber?: number;
   multipleNumber?: number;
 }
 }
 export type CheckMap = {
 export type CheckMap = {
   [key in ValidType]: boolean;
   [key in ValidType]: boolean;
 };
 };
 
 
-export const checkIsEmpty = (value: string): boolean => {
+export const checkEmptyValid = (value: string): boolean => {
   return value.trim() !== '';
   return value.trim() !== '';
 };
 };
 
 
@@ -133,7 +133,7 @@ export const checkMultiple = (param: {
 
 
 export const checkDimension = (param: {
 export const checkDimension = (param: {
   value: string;
   value: string;
-  metricType?: number;
+  metricType?: MetricType;
   multipleNumber?: number;
   multipleNumber?: number;
 }): boolean => {
 }): boolean => {
   const { value, metricType, multipleNumber } = param;
   const { value, metricType, multipleNumber } = param;
@@ -152,7 +152,7 @@ export const getCheckResult = (param: ICheckMapParam): boolean => {
 
 
   const checkMap = {
   const checkMap = {
     email: checkEmail(value),
     email: checkEmail(value),
-    require: checkIsEmpty(value),
+    require: checkEmptyValid(value),
     confirm: value === extraParam?.compareValue,
     confirm: value === extraParam?.compareValue,
     range: checkRange({
     range: checkRange({
       value,
       value,

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

@@ -1,5 +1,5 @@
 import {
 import {
-  checkIsEmpty,
+  checkEmptyValid,
   checkEmail,
   checkEmail,
   checkPasswordStrength,
   checkPasswordStrength,
   checkRange,
   checkRange,
@@ -9,9 +9,9 @@ import {
 } from '../Validation';
 } from '../Validation';
 
 
 describe('Test validation utils', () => {
 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', () => {
   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",
     "@nestjs/swagger": "^4.8.0",
     "@types/passport-jwt": "^3.0.5",
     "@types/passport-jwt": "^3.0.5",
     "@types/passport-local": "^1.0.33",
     "@types/passport-local": "^1.0.33",
-    "@zilliz/milvus-sdk-node-dev": "^0.1.2",
+    "@zilliz/milvus-sdk-node-dev": "^0.1.4",
     "class-transformer": "^0.4.0",
     "class-transformer": "^0.4.0",
     "class-validator": "^0.13.1",
     "class-validator": "^0.13.1",
     "passport": "^0.4.1",
     "passport": "^0.4.1",

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

@@ -91,7 +91,7 @@ export class CollectionsService {
           description: collectionInfo.schema.description,
           description: collectionInfo.schema.description,
           autoID: collectionInfo.schema.autoID,
           autoID: collectionInfo.schema.autoID,
           rowCount: findKeyValue(collectionStatistics.stats, ROW_COUNT),
           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) {
   async describeIndex(data: DescribeIndexReq) {
     const res = await this.milvusClient.describeIndex(data);
     const res = await this.milvusClient.describeIndex(data);
+    if (res.status.error_code === 'IndexNotExist') {
+      return res;
+    }
     throwErrorFromSDK(res.status);
     throwErrorFromSDK(res.status);
     return res;
     return res;
   }
   }

+ 4 - 4
server/yarn.lock

@@ -1287,10 +1287,10 @@
   resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d"
   resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d"
   integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
   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:
   dependencies:
     "@grpc/grpc-js" "^1.2.12"
     "@grpc/grpc-js" "^1.2.12"
     "@grpc/proto-loader" "^0.6.0"
     "@grpc/proto-loader" "^0.6.0"