Browse Source

Merge pull request #126 from Tumao727/feature/insert-data

update insert interactions
ryjiang 4 years ago
parent
commit
4addd95bab

+ 2 - 1
client/src/components/grid/ToolBar.tsx

@@ -79,7 +79,8 @@ const CustomToolBar: FC<ToolBarType> = props => {
                 startIcon={Icon}
                 color="primary"
                 disabled={disabled}
-                variant="contained"
+                // use contained variant as default
+                variant={c.btnVariant || 'contained'}
                 tooltip={c.tooltip}
                 className={classes.btn}
               >

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

@@ -40,6 +40,7 @@ export type ToolBarConfig = Partial<TableSwitchType> &
     type?: 'iconBtn' | 'buttton' | 'switch' | 'select' | 'groupSelect';
     position?: 'right' | 'left';
     component?: ReactElement;
+    btnVariant?: 'contained' | 'outlined' | 'text';
   };
 
 export type TableHeadType = {

+ 154 - 39
client/src/components/insert/Container.tsx

@@ -1,5 +1,13 @@
 import { makeStyles, Theme } from '@material-ui/core';
-import { FC, ReactElement, useContext, useMemo, useState } from 'react';
+import {
+  FC,
+  ReactElement,
+  useCallback,
+  useContext,
+  useEffect,
+  useMemo,
+  useState,
+} from 'react';
 import { useTranslation } from 'react-i18next';
 import DialogTemplate from '../customDialog/DialogTemplate';
 import icons from '../icons/Icons';
@@ -14,6 +22,8 @@ import {
 } from './Types';
 import { Option } from '../customSelector/Types';
 import { parse } from 'papaparse';
+import { PartitionHttp } from '../../http/Partition';
+import { combineHeadsAndData } from '../../utils/Insert';
 
 const getStyles = makeStyles((theme: Theme) => ({
   icon: {
@@ -23,33 +33,20 @@ const getStyles = makeStyles((theme: Theme) => ({
 
 /**
  * this component contains processes during insert
- * all datas and methods passed in as props, no interactions with server done in it
+ * including import, preview and status
  */
 
 const InsertContainer: FC<InsertContentProps> = ({
-  collections,
-  selectedCollection,
-  partitions,
-  selectedPartition,
-  schema,
+  collections = [],
+  defaultSelectedCollection,
+  defaultSelectedPartition,
+
+  partitions = [],
+  schema = [],
   handleInsert,
 }) => {
   const classes = getStyles();
 
-  // props children component needed:
-  const collectionOptions: Option[] = collections.map(c => ({
-    label: c._name,
-    value: c._name,
-  }));
-  const partitionOptions: Option[] = partitions.map(p => ({
-    label: p._name,
-    value: p._name,
-  }));
-  const schemaOptions: Option[] = schema.map(s => ({
-    label: s._fieldName,
-    value: s._fieldId,
-  }));
-
   const { t: insertTrans } = useTranslation('insert');
   const { t: btnTrans } = useTranslation('btn');
   const { handleCloseDialog, openSnackBar } = useContext(rootContext);
@@ -59,14 +56,18 @@ const InsertContainer: FC<InsertContentProps> = ({
   const [insertStatus, setInsertStauts] = useState<InsertStatusEnum>(
     InsertStatusEnum.init
   );
-  // const [nextDisabled, setNextDisabled] = useState<boolean>(false);
+  const [insertFailMsg, setInsertFailMsg] = useState<string>('');
+
+  const [nextDisabled, setNextDisabled] = useState<boolean>(false);
 
   // selected collection name
-  const [collectionValue, setCollectionValue] =
-    useState<string>(selectedCollection);
+  const [collectionValue, setCollectionValue] = useState<string>(
+    defaultSelectedCollection
+  );
   // selected partition name
-  const [partitionValue, setPartitionValue] =
-    useState<string>(selectedPartition);
+  const [partitionValue, setPartitionValue] = useState<string>(
+    defaultSelectedPartition
+  );
   // use contain field names yes as default
   const [isContainFieldNames, setIsContainFieldNames] = useState<number>(1);
   // uploaded file name
@@ -74,6 +75,77 @@ const InsertContainer: FC<InsertContentProps> = ({
   // uploaded csv data (type: string)
   const [csvData, setCsvData] = useState<any[]>([]);
 
+  // handle changed table heads
+  const [tableHeads, setTableHeads] = useState<string[]>([]);
+
+  const [partitionOptions, setPartitionOptions] = useState<Option[]>([]);
+
+  const previewData = useMemo(() => {
+    // we only show top 4 results of uploaded csv data
+    const end = isContainFieldNames ? 5 : 4;
+    return csvData.slice(0, end);
+  }, [csvData, isContainFieldNames]);
+
+  useEffect(() => {
+    if (activeStep === InsertStepperEnum.import) {
+      /**
+       * 1. must choose collection and partition
+       * 2. must upload a csv file
+       */
+      const selectValid = collectionValue !== '' && partitionValue !== '';
+      const uploadValid = csvData.length > 0;
+      const condition = selectValid && uploadValid;
+      setNextDisabled(!condition);
+    }
+    if (activeStep === InsertStepperEnum.preview) {
+      /**
+       * table heads shouldn't be empty
+       */
+      const headsValid = tableHeads.every(h => h !== '');
+      setNextDisabled(!headsValid);
+    }
+  }, [activeStep, collectionValue, partitionValue, csvData, tableHeads]);
+
+  useEffect(() => {
+    const heads = isContainFieldNames
+      ? previewData[0]
+      : new Array(previewData[0].length).fill('');
+
+    setTableHeads(heads);
+  }, [previewData, isContainFieldNames]);
+
+  const fetchPartition = useCallback(async () => {
+    if (collectionValue) {
+      const partitions = await PartitionHttp.getPartitions(collectionValue);
+      const partitionOptions: Option[] = partitions.map(p => ({
+        label: p._formatName,
+        value: p._name,
+      }));
+      setPartitionOptions(partitionOptions);
+    }
+  }, [collectionValue]);
+
+  useEffect(() => {
+    // if not on partitions page, we need to fetch partitions according to selected collection
+    if (partitions.length === 0) {
+      fetchPartition();
+    } else {
+      const options = partitions
+        .map(p => ({
+          label: p._formatName,
+          value: p._name,
+        }))
+        // when there's single selected partition
+        // insert dialog partitions shouldn't selectable
+        .filter(
+          partition =>
+            partition.label === defaultSelectedPartition ||
+            defaultSelectedPartition === ''
+        );
+      setPartitionOptions(options);
+    }
+  }, [partitions, fetchPartition, defaultSelectedPartition]);
+
   const BackIcon = icons.back;
 
   // modal actions part, buttons label text or component
@@ -112,37 +184,78 @@ const InsertContainer: FC<InsertContentProps> = ({
     };
   }, [insertStatus]);
 
+  // props children component needed:
+  const collectionOptions: Option[] = useMemo(
+    () =>
+      defaultSelectedCollection === ''
+        ? collections.map(c => ({
+            label: c._name,
+            value: c._name,
+          }))
+        : [
+            {
+              label: defaultSelectedCollection,
+              value: defaultSelectedCollection,
+            },
+          ],
+    [collections, defaultSelectedCollection]
+  );
+
+  const schemaOptions: Option[] = useMemo(() => {
+    const list =
+      schema.length > 0
+        ? schema
+        : collections.find(c => c._name === collectionValue)?._fields;
+    return (list || []).map(s => ({
+      label: s._fieldName,
+      value: s._fieldId,
+    }));
+  }, [schema, collectionValue, collections]);
+
   const checkUploadFileValidation = (fieldNamesLength: number): boolean => {
     return schemaOptions.length === fieldNamesLength;
   };
 
-  const previewData = useMemo(() => {
-    // we only show top 4 results of uploaded csv data
-    const end = isContainFieldNames ? 5 : 4;
-    return csvData.slice(0, end);
-  }, [csvData, isContainFieldNames]);
-
-  const handleUploadedData = (csv: string) => {
+  const handleUploadedData = (csv: string, uploader: HTMLFormElement) => {
     const { data } = parse(csv);
     const uploadFieldNamesLength = (data as string[])[0].length;
     const validation = checkUploadFileValidation(uploadFieldNamesLength);
     if (!validation) {
       // open snackbar
       openSnackBar(insertTrans('uploadFieldNamesLenWarning'), 'error');
-      // reset filename
+      // reset uploader value and filename
       setFileName('');
+      uploader.value = null;
       return;
     }
     setCsvData(data);
   };
 
   const handleInsertData = async () => {
+    // combine table heads and data
+    const tableData = isContainFieldNames ? csvData.slice(1) : csvData;
+    const data = combineHeadsAndData(tableHeads, tableData);
+
     setInsertStauts(InsertStatusEnum.loading);
-    const res = await handleInsert();
-    const status = res ? InsertStatusEnum.success : InsertStatusEnum.error;
+    const { result, msg } = await handleInsert(
+      collectionValue,
+      partitionValue,
+      data
+    );
+
+    if (!result) {
+      setInsertFailMsg(msg);
+    }
+    const status = result ? InsertStatusEnum.success : InsertStatusEnum.error;
     setInsertStauts(status);
   };
 
+  const handleCollectionChange = (name: string) => {
+    setCollectionValue(name);
+    // reset partition
+    setPartitionValue('');
+  };
+
   const handleNext = () => {
     switch (activeStep) {
       case InsertStepperEnum.import:
@@ -183,7 +296,7 @@ const InsertContainer: FC<InsertContentProps> = ({
             partitionOptions={partitionOptions}
             selectedCollection={collectionValue}
             selectedPartition={partitionValue}
-            handleCollectionChange={setCollectionValue}
+            handleCollectionChange={handleCollectionChange}
             handlePartitionChange={setPartitionValue}
             handleUploadedData={handleUploadedData}
             fileName={fileName}
@@ -195,13 +308,15 @@ const InsertContainer: FC<InsertContentProps> = ({
           <InsertPreview
             schemaOptions={schemaOptions}
             data={previewData}
+            tableHeads={tableHeads}
+            setTableHeads={setTableHeads}
             isContainFieldNames={isContainFieldNames}
             handleIsContainedChange={setIsContainFieldNames}
           />
         );
       // default represents InsertStepperEnum.status
       default:
-        return <InsertStatus status={insertStatus} />;
+        return <InsertStatus status={insertStatus} failMsg={insertFailMsg} />;
     }
   };
 
@@ -213,7 +328,7 @@ const InsertContainer: FC<InsertContentProps> = ({
       cancelLabel={cancel}
       handleCancel={handleBack}
       handleConfirm={handleNext}
-      confirmDisabled={false}
+      confirmDisabled={nextDisabled}
       showActions={showActions}
       showCancel={showCancel}
       // don't show close icon when insert not finish

+ 4 - 1
client/src/components/insert/Import.tsx

@@ -120,6 +120,7 @@ const InsertImport: FC<InsertImportProps> = ({
         <div className="selectorWrapper">
           <CustomSelector
             options={collectionOptions}
+            disabled={collectionOptions.length === 0}
             wrapperClass="selector"
             labelClass="selectLabel"
             value={selectedCollection}
@@ -127,12 +128,14 @@ const InsertImport: FC<InsertImportProps> = ({
             label={collectionTrans('collection')}
             onChange={(e: { target: { value: unknown } }) => {
               const collection = e.target.value;
-              handleCollectionChange(collection as string);
+              handleCollectionChange &&
+                handleCollectionChange(collection as string);
             }}
           />
           <Divider classes={{ root: 'divider' }} />
           <CustomSelector
             options={partitionOptions}
+            disabled={partitionOptions.length === 0}
             wrapperClass="selector"
             labelClass="selectLabel"
             value={selectedPartition}

+ 4 - 18
client/src/components/insert/Preview.tsx

@@ -1,4 +1,4 @@
-import { FC, useCallback, useEffect, useMemo, useState } from 'react';
+import { FC, useCallback, useMemo } from 'react';
 import { makeStyles, Theme, Typography } from '@material-ui/core';
 import { useTranslation } from 'react-i18next';
 import { InsertPreviewProps } from './Types';
@@ -84,18 +84,13 @@ const getTableData = (
   return transferCsvArrayToTableData(csvData);
 };
 
-const getDefaultHeads = (
-  data: any[],
-  isContainFieldNames: number
-): string[] => {
-  return isContainFieldNames ? data[0] : new Array(data[0].length).fill('');
-};
-
 const InsertPreview: FC<InsertPreviewProps> = ({
   schemaOptions,
   data,
   isContainFieldNames,
   handleIsContainedChange,
+  tableHeads,
+  setTableHeads,
 }) => {
   const classes = getStyles();
   const { t: insertTrans } = useTranslation('insert');
@@ -104,24 +99,15 @@ const InsertPreview: FC<InsertPreviewProps> = ({
   // table needed table structure, metadata from csv
   const tableData = getTableData(data, isContainFieldNames);
 
-  const [tableHeads, setTableHeads] = useState<string[]>(
-    getDefaultHeads(data, isContainFieldNames)
-  );
-
   const handleTableHeadChange = useCallback(
     (index: number, label: string) => {
       const newHeads = [...tableHeads];
       newHeads[index] = label;
       setTableHeads(newHeads);
     },
-    [tableHeads]
+    [tableHeads, setTableHeads]
   );
 
-  useEffect(() => {
-    const newHeads = getDefaultHeads(data, isContainFieldNames);
-    setTableHeads(newHeads);
-  }, [data, isContainFieldNames]);
-
   const editHeads = useMemo(
     () =>
       tableHeads.map((head: string, index: number) => ({

+ 2 - 1
client/src/components/insert/Status.tsx

@@ -32,7 +32,7 @@ const getStyles = makeStyles((theme: Theme) => ({
   },
 }));
 
-const InsertStatus: FC<InsertStatusProps> = ({ status }) => {
+const InsertStatus: FC<InsertStatusProps> = ({ status, failMsg }) => {
   const { t: insertTrans } = useTranslation('insert');
   const classes = getStyles({ status });
 
@@ -69,6 +69,7 @@ const InsertStatus: FC<InsertStatusProps> = ({ status }) => {
       <Typography variant="h4" className={classes.text}>
         {insertTrans('statusError')}
       </Typography>
+      {failMsg && <Typography className={classes.text}>{failMsg}</Typography>}
     </>
   );
 

+ 28 - 9
client/src/components/insert/Types.ts

@@ -1,15 +1,29 @@
 import { CollectionData } from '../../pages/collections/Types';
-import { PartitionData } from '../../pages/partitions/Types';
+import { PartitionView } from '../../pages/partitions/Types';
 import { FieldData } from '../../pages/schema/Types';
 import { Option } from '../customSelector/Types';
 
 export interface InsertContentProps {
-  collections: CollectionData[];
-  selectedCollection: string;
-  partitions: PartitionData[];
-  selectedPartition: string;
-  schema: FieldData[];
-  handleInsert: () => Promise<boolean>;
+  // optional on partition page since its collection is fixed
+  collections?: CollectionData[];
+  // required on partition page since user can't select collection to get schema
+  schema?: FieldData[];
+  // required on partition page
+  partitions?: PartitionView[];
+
+  // insert default selected collection
+  // if default value is not '', collections not selectable
+  defaultSelectedCollection: string;
+
+  // insert default selected partition
+  // if default value is not '', partitions not selectable
+  defaultSelectedPartition: string;
+
+  handleInsert: (
+    collectionName: string,
+    partitionName: string,
+    fieldData: any[]
+  ) => Promise<{ result: boolean; msg: string }>;
 }
 
 export enum InsertStepperEnum {
@@ -35,10 +49,11 @@ export interface InsertImportProps {
   selectedPartition: string;
 
   // selectors change methods
-  handleCollectionChange: (collectionName: string) => void;
+  // optional if collection not selectable
+  handleCollectionChange?: (collectionName: string) => void;
   handlePartitionChange: (partitionName: string) => void;
   // handle uploaded data
-  handleUploadedData: (data: string) => void;
+  handleUploadedData: (data: string, uploader: HTMLFormElement) => void;
   fileName: string;
   setFileName: (fileName: string) => void;
 }
@@ -47,10 +62,14 @@ export interface InsertPreviewProps {
   schemaOptions: Option[];
   data: any[];
 
+  tableHeads: string[];
+  setTableHeads: (heads: string[]) => void;
+
   isContainFieldNames: number;
   handleIsContainedChange: (isContained: number) => void;
 }
 
 export interface InsertStatusProps {
   status: InsertStatusEnum;
+  failMsg: string;
 }

+ 7 - 2
client/src/components/menu/SimpleMenu.tsx

@@ -8,6 +8,10 @@ import CustomButton from '../customButton/CustomButton';
 import { makeStyles, Theme } from '@material-ui/core';
 
 const getStyles = makeStyles((theme: Theme) => ({
+  menuPaper: {
+    boxShadow: '0px 4px 24px rgba(0, 0, 0, 0.08)',
+    borderRadius: '4px',
+  },
   menuItem: {
     minWidth: '160px',
     padding: theme.spacing(1),
@@ -49,9 +53,10 @@ const SimpleMenu: FC<SimpleMenuType> = props => {
         keepMounted
         open={Boolean(anchorEl)}
         onClose={handleClose}
+        classes={{ paper: classes.menuPaper }}
         getContentAnchorEl={null}
-        anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
-        transformOrigin={{ vertical: 'top', horizontal: 'center' }}
+        // anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
+        // transformOrigin={{ vertical: 'top', horizontal: 'center' }}
       >
         <div>
           {menuItems.map((v, i) =>

+ 4 - 2
client/src/components/uploader/Types.ts

@@ -7,7 +7,9 @@ export interface UploaderProps {
   // snackbar warning when uploaded file size is over limit
   overSizeWarning?: string;
   setFileName: (fileName: string) => void;
-  handleUploadedData: (data: string) => void;
-  handleUploadFileChange?: (file: File) => void;
+  // handle uploader uploaded
+  handleUploadedData: (data: string, uploader: HTMLFormElement) => void;
+  // handle uploader onchange
+  handleUploadFileChange?: (file: File, uploader: HTMLFormElement) => void;
   handleUploadError?: () => void;
 }

+ 2 - 2
client/src/components/uploader/Uploader.tsx

@@ -31,7 +31,7 @@ const Uploader: FC<UploaderProps> = ({
     reader.onload = async e => {
       const data = reader.result;
       if (data) {
-        handleUploadedData(data as string);
+        handleUploadedData(data as string, inputRef.current!);
       }
     };
     // handle upload error
@@ -55,7 +55,7 @@ const Uploader: FC<UploaderProps> = ({
       }
 
       setFileName(file.name || 'file');
-      handleUploadFileChange && handleUploadFileChange(file);
+      handleUploadFileChange && handleUploadFileChange(file, inputRef.current!);
       reader.readAsText(file, 'utf8');
     };
     uploader.click();

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

@@ -1,8 +1,10 @@
 import { ChildrenStatusType, StatusEnum } from '../components/status/Types';
-import { CollectionView } from '../pages/collections/Types';
+import { CollectionView, InsertDataParam } from '../pages/collections/Types';
+import { Field } from '../pages/schema/Types';
 import { IndexState, ShowCollectionsType } from '../types/Milvus';
 import { formatNumber } from '../utils/Common';
 import BaseModel from './BaseModel';
+import { FieldHttp } from './Field';
 
 export class CollectionHttp extends BaseModel implements CollectionView {
   private autoID!: string;
@@ -12,6 +14,9 @@ export class CollectionHttp extends BaseModel implements CollectionView {
   private index_status!: string;
   private id!: string;
   private isLoaded!: boolean;
+  private schema!: {
+    fields: Field[];
+  };
 
   static COLLECTIONS_URL = '/collections';
   static COLLECTIONS_INDEX_STATUS_URL = '/collections/indexes/status';
@@ -30,6 +35,13 @@ export class CollectionHttp extends BaseModel implements CollectionView {
     return super.findAll({ path: this.COLLECTIONS_URL, params: data || {} });
   }
 
+  static getCollection(name: string) {
+    return super.search({
+      path: `${this.COLLECTIONS_URL}/${name}`,
+      params: {},
+    });
+  }
+
   static createCollection(data: any) {
     return super.create({ path: this.COLLECTIONS_URL, data });
   }
@@ -61,6 +73,13 @@ export class CollectionHttp extends BaseModel implements CollectionView {
     return super.search({ path: this.COLLECTIONS_STATISTICS_URL, params: {} });
   }
 
+  static insertData(collectionName: string, param: InsertDataParam) {
+    return super.create({
+      path: `${this.COLLECTIONS_URL}/${collectionName}/insert`,
+      data: param,
+    });
+  }
+
   get _autoId() {
     return this.autoID;
   }
@@ -85,6 +104,10 @@ export class CollectionHttp extends BaseModel implements CollectionView {
     return this.isLoaded === true ? StatusEnum.loaded : StatusEnum.unloaded;
   }
 
+  get _fields() {
+    return this.schema.fields.map(f => new FieldHttp(f));
+  }
+
   get _indexState() {
     switch (this.index_status) {
       case IndexState.InProgress:

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

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

+ 58 - 25
client/src/pages/collections/Collections.tsx

@@ -4,7 +4,12 @@ import { useNavigationHook } from '../../hooks/Navigation';
 import { ALL_ROUTER_TYPES } from '../../router/Types';
 import MilvusGrid from '../../components/grid/Grid';
 import CustomToolBar from '../../components/grid/ToolBar';
-import { CollectionCreateParam, CollectionView, DataTypeEnum } from './Types';
+import {
+  CollectionCreateParam,
+  CollectionView,
+  DataTypeEnum,
+  InsertDataParam,
+} from './Types';
 import { ColDefinitionsType, ToolBarConfig } from '../../components/grid/Types';
 import { usePaginationHook } from '../../hooks/Pagination';
 import icons from '../../components/icons/Icons';
@@ -26,6 +31,7 @@ import {
 import Highlighter from 'react-highlight-words';
 import { parseLocationSearch } from '../../utils/Format';
 import InsertContainer from '../../components/insert/Container';
+import { MilvusHttp } from '../../http/Milvus';
 
 const useStyles = makeStyles((theme: Theme) => ({
   emptyWrapper: {
@@ -133,6 +139,31 @@ const Collections = () => {
     fetchData();
   }, [fetchData]);
 
+  const handleInsert = async (
+    collectionName: string,
+    partitionName: string,
+    fieldData: any[]
+  ): Promise<{ result: boolean; msg: string }> => {
+    const param: InsertDataParam = {
+      partition_names: [partitionName],
+      fields_data: fieldData,
+    };
+    try {
+      await CollectionHttp.insertData(collectionName, param);
+      await MilvusHttp.flush(collectionName);
+      // update collections
+      fetchData();
+      return { result: true, msg: '' };
+    } catch (err) {
+      const {
+        response: {
+          data: { message },
+        },
+      } = err;
+      return { result: false, msg: message || '' };
+    }
+  };
+
   const handleCreateCollection = async (param: CollectionCreateParam) => {
     const data: CollectionCreateParam = JSON.parse(JSON.stringify(param));
     const vectorType = [DataTypeEnum.BinaryVector, DataTypeEnum.FloatVector];
@@ -229,30 +260,32 @@ const Collections = () => {
       },
       icon: 'add',
     },
-    // {
-    //   label: btnTrans('insert'),
-    //   onClick: () => {
-    //     const component = (
-    //       <InsertContainer
-    //         collections={[]}
-    //         selectedCollection={''}
-    //         partitions={[]}
-    //         selectedPartition={''}
-    //         schema={[]}
-    //         handleInsert={() => {}}
-    //       />
-    //     );
-    //     handleInsertDialog(component);
-    //   },
-    //   /**
-    //    * insert validation:
-    //    * 1. At least 1 available collection
-    //    * 2. selected collections quantity shouldn't over 1
-    //    */
-    //   disabled: () =>
-    //     collectionList.length === 0 || selectedCollections.length > 1,
-    //   icon: 'upload',
-    // },
+    {
+      label: btnTrans('insert'),
+      onClick: () => {
+        handleInsertDialog(
+          <InsertContainer
+            collections={collections}
+            defaultSelectedCollection={
+              selectedCollections.length === 1
+                ? selectedCollections[0]._name
+                : ''
+            }
+            // user can't select partition on collection page, so default value is ''
+            defaultSelectedPartition={''}
+            handleInsert={handleInsert}
+          />
+        );
+      },
+      /**
+       * insert validation:
+       * 1. At least 1 available collection
+       * 2. selected collections quantity shouldn't over 1
+       */
+      disabled: () =>
+        collectionList.length === 0 || selectedCollections.length > 1,
+      btnVariant: 'outlined',
+    },
     {
       type: 'iconBtn',
       onClick: () => {

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

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

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

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

+ 9 - 0
client/src/pages/schema/Types.ts

@@ -12,6 +12,15 @@ export enum INDEX_TYPES_ENUM {
   RNSG = 'RNSG',
 }
 
+export interface Field {
+  data_type: DataType;
+  fieldID: string;
+  type_params: { key: string; value: string }[];
+  is_primary_key: true;
+  name: string;
+  description: string;
+}
+
 export interface FieldData {
   _fieldId: string;
   _isPrimaryKey: boolean;

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

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