Ver código fonte

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

add insert ui
ryjiang 4 anos atrás
pai
commit
4945cb0146
35 arquivos alterados com 845 adições e 124 exclusões
  1. 2 0
      client/package.json
  2. BIN
      client/src/assets/imgs/insert/fail.png
  3. BIN
      client/src/assets/imgs/insert/success.png
  4. 1 1
      client/src/components/customButton/CustomButton.tsx
  5. 15 3
      client/src/components/customDialog/CustomDialogTitle.tsx
  6. 4 1
      client/src/components/customDialog/DialogTemplate.tsx
  7. 1 0
      client/src/components/customDialog/Types.ts
  8. 19 24
      client/src/components/customSelector/CustomSelector.tsx
  9. 3 1
      client/src/components/customSelector/Types.ts
  10. 6 0
      client/src/components/grid/Grid.tsx
  11. 28 13
      client/src/components/grid/Table.tsx
  12. 36 0
      client/src/components/grid/TableEditableHead.tsx
  13. 17 0
      client/src/components/grid/Types.ts
  14. 2 0
      client/src/components/icons/Icons.tsx
  15. 2 1
      client/src/components/icons/Types.ts
  16. 73 23
      client/src/components/insert/Container.tsx
  17. 147 36
      client/src/components/insert/Import.tsx
  18. 221 3
      client/src/components/insert/Preview.tsx
  19. 86 2
      client/src/components/insert/Status.tsx
  20. 15 1
      client/src/components/insert/Types.ts
  21. 10 1
      client/src/components/menu/SimpleMenu.tsx
  22. 5 1
      client/src/components/menu/Types.ts
  23. 8 0
      client/src/components/uploader/Types.ts
  24. 57 3
      client/src/components/uploader/Uploader.tsx
  25. 4 0
      client/src/consts/Insert.ts
  26. 22 0
      client/src/i18n/cn/insert.ts
  27. 22 0
      client/src/i18n/en/insert.ts
  28. 0 2
      client/src/pages/collections/Collections.tsx
  29. 2 2
      client/src/pages/collections/CreateFields.tsx
  30. 0 2
      client/src/pages/partitions/Partitions.tsx
  31. 3 3
      client/src/pages/schema/CreateForm.tsx
  32. 0 1
      client/src/pages/schema/Schema.tsx
  33. 9 0
      client/src/styles/theme.ts
  34. 13 0
      client/src/utils/Insert.ts
  35. 12 0
      client/yarn.lock

+ 2 - 0
client/package.json

@@ -14,6 +14,7 @@
     "@testing-library/user-event": "^12.1.10",
     "@types/jest": "^26.0.15",
     "@types/node": "^12.0.0",
+    "@types/papaparse": "^5.2.6",
     "@types/react": "^17.0.0",
     "@types/react-dom": "^17.0.0",
     "@types/react-highlight-words": "^0.16.2",
@@ -21,6 +22,7 @@
     "axios": "^0.21.1",
     "dayjs": "^1.10.5",
     "i18next": "^20.3.1",
+    "papaparse": "^5.3.1",
     "react": "^17.0.2",
     "react-app-rewired": "^2.1.8",
     "react-dom": "^17.0.2",

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


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


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

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

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

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

+ 4 - 1
client/src/components/customDialog/DialogTemplate.tsx

@@ -27,6 +27,7 @@ const DialogTemplate: FC<DialogContainerProps> = ({
   children,
   showActions = true,
   showCancel = true,
+  showCloseIcon = true,
 }) => {
   const { t } = useTranslation('btn');
   const cancel = cancelLabel || t('cancel');
@@ -36,7 +37,9 @@ const DialogTemplate: FC<DialogContainerProps> = ({
 
   return (
     <>
-      <CustomDialogTitle onClose={handleClose}>{title}</CustomDialogTitle>
+      <CustomDialogTitle onClose={handleClose} showCloseIcon={showCloseIcon}>
+        {title}
+      </CustomDialogTitle>
       <DialogContent>{children}</DialogContent>
       {showActions && (
         <DialogActions className={classes.actions}>

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

@@ -24,6 +24,7 @@ export type DialogContainerProps = {
   title: string;
   cancelLabel?: string | ReactElement;
   confirmLabel?: string | ReactElement;
+  showCloseIcon?: boolean;
   handleClose: () => void;
   handleCancel?: () => void;
   handleConfirm: (param: any) => void;

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

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

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

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

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

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

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

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

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

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

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

@@ -53,6 +53,15 @@ export type TableHeadType = {
   openCheckBox?: boolean;
 };
 
+export type TableEditableHeadType = {
+  editHeads: EditableHeads[];
+};
+
+export type EditableHeads = {
+  component: ReactElement;
+  value: string;
+};
+
 export type TableType = {
   selected: any[];
   onSelected: (e: React.MouseEvent, row: any) => void;
@@ -67,6 +76,10 @@ export type TableType = {
   showHoverStyle?: boolean;
   isLoading?: boolean;
   setPageSize?: (size: number) => void;
+  headEditable?: boolean;
+  editHeads: EditableHeads[];
+  // with unit like '20px'
+  tableCellMaxWidth?: string;
 };
 
 export type ColDefinitionsType = {
@@ -114,6 +127,10 @@ export type MilvusGridType = ToolBarType & {
   disableSelect?: boolean;
   noData?: string;
   showHoverStyle?: boolean;
+  headEditable?: boolean;
+  editHeads?: EditableHeads[];
+  // with unit like '20px'
+  tableCellMaxWidth?: string;
 };
 
 export type ActionBarType = {

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

@@ -18,6 +18,7 @@ import ArrowBackIosIcon from '@material-ui/icons/ArrowBackIos';
 import ExitToAppIcon from '@material-ui/icons/ExitToApp';
 import ArrowForwardIosIcon from '@material-ui/icons/ArrowForwardIos';
 import RemoveCircleOutlineIcon from '@material-ui/icons/RemoveCircleOutline';
+import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown';
 import { SvgIcon } from '@material-ui/core';
 import { ReactComponent as MilvusIcon } from '../../assets/icons/milvus.svg';
 import { ReactComponent as OverviewIcon } from '../../assets/icons/overview.svg';
@@ -48,6 +49,7 @@ const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
   logout: (props = {}) => <ExitToAppIcon {...props} />,
   rightArrow: (props = {}) => <ArrowForwardIosIcon {...props} />,
   remove: (props = {}) => <RemoveCircleOutlineIcon {...props} />,
+  dropdown: (props = {}) => <ArrowDropDownIcon {...props} />,
 
   milvus: (props = {}) => (
     <SvgIcon viewBox="0 0 44 31" component={MilvusIcon} {...props} />

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

@@ -25,4 +25,5 @@ export type IconsType =
   | 'load'
   | 'remove'
   | 'key'
-  | 'upload';
+  | 'upload'
+  | 'dropdown';

+ 73 - 23
client/src/components/insert/Container.tsx

@@ -1,5 +1,5 @@
 import { makeStyles, Theme } from '@material-ui/core';
-import { FC, useContext, useMemo, useState } from 'react';
+import { FC, ReactElement, useContext, useMemo, useState } from 'react';
 import { useTranslation } from 'react-i18next';
 import DialogTemplate from '../customDialog/DialogTemplate';
 import icons from '../icons/Icons';
@@ -13,6 +13,7 @@ import {
   InsertStepperEnum,
 } from './Types';
 import { Option } from '../customSelector/Types';
+import { parse } from 'papaparse';
 
 const getStyles = makeStyles((theme: Theme) => ({
   icon: {
@@ -51,7 +52,7 @@ const InsertContainer: FC<InsertContentProps> = ({
 
   const { t: insertTrans } = useTranslation('insert');
   const { t: btnTrans } = useTranslation('btn');
-  const { handleCloseDialog } = useContext(rootContext);
+  const { handleCloseDialog, openSnackBar } = useContext(rootContext);
   const [activeStep, setActiveStep] = useState<InsertStepperEnum>(
     InsertStepperEnum.import
   );
@@ -60,21 +61,34 @@ const InsertContainer: FC<InsertContentProps> = ({
   );
   // const [nextDisabled, setNextDisabled] = useState<boolean>(false);
 
+  // selected collection name
+  const [collectionValue, setCollectionValue] =
+    useState<string>(selectedCollection);
+  // selected partition name
+  const [partitionValue, setPartitionValue] =
+    useState<string>(selectedPartition);
+  // use contain field names yes as default
+  const [isContainFieldNames, setIsContainFieldNames] = useState<number>(1);
+  // uploaded file name
+  const [fileName, setFileName] = useState<string>('');
+  // uploaded csv data (type: string)
+  const [csvData, setCsvData] = useState<any[]>([]);
+
   const BackIcon = icons.back;
 
+  // modal actions part, buttons label text or component
   const { confirm, cancel } = useMemo(() => {
-    /**
-     * activeStep type is InsertStepperEnum
-     * so index 0 represents import,
-     * index 1 represents preview,
-     * index 2 represents status
-     */
-    const labelList = [
-      {
+    const labelMap: {
+      [key in InsertStepperEnum]: {
+        confirm: string;
+        cancel: string | ReactElement;
+      };
+    } = {
+      [InsertStepperEnum.import]: {
         confirm: btnTrans('next'),
         cancel: btnTrans('cancel'),
       },
-      {
+      [InsertStepperEnum.preview]: {
         confirm: btnTrans('insert'),
         cancel: (
           <>
@@ -83,12 +97,12 @@ const InsertContainer: FC<InsertContentProps> = ({
           </>
         ),
       },
-      {
+      [InsertStepperEnum.status]: {
         confirm: btnTrans('done'),
         cancel: '',
       },
-    ];
-    return labelList[activeStep];
+    };
+    return labelMap[activeStep];
   }, [activeStep, btnTrans, BackIcon, classes.icon]);
 
   const { showActions, showCancel } = useMemo(() => {
@@ -98,13 +112,35 @@ const InsertContainer: FC<InsertContentProps> = ({
     };
   }, [insertStatus]);
 
-  const handleInsertData = () => {
-    // mock status change
+  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 { data } = parse(csv);
+    const uploadFieldNamesLength = (data as string[])[0].length;
+    const validation = checkUploadFileValidation(uploadFieldNamesLength);
+    if (!validation) {
+      // open snackbar
+      openSnackBar(insertTrans('uploadFieldNamesLenWarning'), 'error');
+      // reset filename
+      setFileName('');
+      return;
+    }
+    setCsvData(data);
+  };
+
+  const handleInsertData = async () => {
     setInsertStauts(InsertStatusEnum.loading);
-    handleInsert();
-    setTimeout(() => {
-      setInsertStauts(InsertStatusEnum.success);
-    }, 500);
+    const res = await handleInsert();
+    const status = res ? InsertStatusEnum.success : InsertStatusEnum.error;
+    setInsertStauts(status);
   };
 
   const handleNext = () => {
@@ -144,13 +180,25 @@ const InsertContainer: FC<InsertContentProps> = ({
         return (
           <InsertImport
             collectionOptions={collectionOptions}
-            selectedCollection={selectedCollection}
             partitionOptions={partitionOptions}
-            selectedPartition={selectedPartition}
+            selectedCollection={collectionValue}
+            selectedPartition={partitionValue}
+            handleCollectionChange={setCollectionValue}
+            handlePartitionChange={setPartitionValue}
+            handleUploadedData={handleUploadedData}
+            fileName={fileName}
+            setFileName={setFileName}
           />
         );
       case InsertStepperEnum.preview:
-        return <InsertPreview schemaOptions={schemaOptions} />;
+        return (
+          <InsertPreview
+            schemaOptions={schemaOptions}
+            data={previewData}
+            isContainFieldNames={isContainFieldNames}
+            handleIsContainedChange={setIsContainFieldNames}
+          />
+        );
       // default represents InsertStepperEnum.status
       default:
         return <InsertStatus status={insertStatus} />;
@@ -168,6 +216,8 @@ const InsertContainer: FC<InsertContentProps> = ({
       confirmDisabled={false}
       showActions={showActions}
       showCancel={showCancel}
+      // don't show close icon when insert not finish
+      showCloseIcon={insertStatus !== InsertStatusEnum.loading}
     >
       {generateContent(activeStep)}
     </DialogTemplate>

+ 147 - 36
client/src/components/insert/Import.tsx

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

+ 221 - 3
client/src/components/insert/Preview.tsx

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

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

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

+ 15 - 1
client/src/components/insert/Types.ts

@@ -9,7 +9,7 @@ export interface InsertContentProps {
   partitions: PartitionData[];
   selectedPartition: string;
   schema: FieldData[];
-  handleInsert: () => void;
+  handleInsert: () => Promise<boolean>;
 }
 
 export enum InsertStepperEnum {
@@ -27,14 +27,28 @@ export enum InsertStatusEnum {
 }
 
 export interface InsertImportProps {
+  // selectors options
   collectionOptions: Option[];
   partitionOptions: Option[];
+  // selectors value
   selectedCollection: string;
   selectedPartition: string;
+
+  // selectors change methods
+  handleCollectionChange: (collectionName: string) => void;
+  handlePartitionChange: (partitionName: string) => void;
+  // handle uploaded data
+  handleUploadedData: (data: string) => void;
+  fileName: string;
+  setFileName: (fileName: string) => void;
 }
 
 export interface InsertPreviewProps {
   schemaOptions: Option[];
+  data: any[];
+
+  isContainFieldNames: number;
+  handleIsContainedChange: (isContained: number) => void;
 }
 
 export interface InsertStatusProps {

+ 10 - 1
client/src/components/menu/SimpleMenu.tsx

@@ -10,6 +10,11 @@ import { makeStyles, Theme } from '@material-ui/core';
 const getStyles = makeStyles((theme: Theme) => ({
   menuItem: {
     minWidth: '160px',
+    padding: theme.spacing(1),
+
+    '&:hover': {
+      backgroundColor: '#f9f9f9',
+    },
   },
 }));
 
@@ -59,7 +64,11 @@ const SimpleMenu: FC<SimpleMenuType> = props => {
                 }}
                 key={v.label + i}
               >
-                {v.label}
+                {v.wrapperClass ? (
+                  <span className={v.wrapperClass}>{v.label}</span>
+                ) : (
+                  v.label
+                )}
               </MenuItem>
             ) : (
               <span key={i}>{v.label}</span>

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

@@ -3,7 +3,11 @@ import { ReactElement } from 'react';
 
 export type SimpleMenuType = {
   label: string;
-  menuItems: { label: string | ReactElement; callback?: () => void }[];
+  menuItems: {
+    label: string | ReactElement;
+    callback?: () => void;
+    wrapperClass?: string;
+  }[];
   buttonProps?: ButtonProps;
   className?: string;
 };

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

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

+ 57 - 3
client/src/components/uploader/Uploader.tsx

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

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

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

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

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

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

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

+ 0 - 2
client/src/pages/collections/Collections.tsx

@@ -379,8 +379,6 @@ const Collections = () => {
           rows={collectionList}
           rowCount={total}
           primaryKey="_name"
-          openCheckBox={true}
-          showHoverStyle={true}
           selected={selectedCollections}
           setSelected={handleSelectChange}
           page={currentPage}

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

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

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

@@ -319,8 +319,6 @@ const Partitions: FC<{
         rows={partitionList}
         rowCount={total}
         primaryKey="id"
-        openCheckBox={true}
-        showHoverStyle={true}
         selected={selectedPartitions}
         setSelected={handleSelectChange}
         page={currentPage}

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

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

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

@@ -210,7 +210,6 @@ const Schema: FC<{
         rows={schemaList}
         rowCount={total}
         primaryKey="_fieldId"
-        openCheckBox={false}
         showHoverStyle={false}
         page={currentPage}
         onChangePage={handlePageChange}

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

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

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

@@ -0,0 +1,13 @@
+import { generateId } from './Common';
+
+/**
+ * function to convert uploaded csv to MilvusGrid component accepted data type
+ * @param data uploaded csv data, e.g. [['name1', 12], ['name2', 14]]
+ * @returns key value pair object array, use index as key, e.g. [{0: 'name1', 1: 12}, {0: 'name2', 1: 14}]
+ */
+export const transferCsvArrayToTableData = (data: any[][]) => {
+  return data.reduce(
+    (result, arr) => [...result, { ...arr, id: generateId() }],
+    []
+  );
+};

+ 12 - 0
client/yarn.lock

@@ -1931,6 +1931,13 @@
   resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e"
   integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==
 
+"@types/papaparse@^5.2.6":
+  version "5.2.6"
+  resolved "https://registry.yarnpkg.com/@types/papaparse/-/papaparse-5.2.6.tgz#0bba18de4d15eff65883bc7c0794e0134de9e7c7"
+  integrity sha512-xGKSd0UTn58N1h0+zf8mW863Rv8BvXcGibEgKFtBIXZlcDXAmX/T4RdDO2mwmrmOypUDt5vRgo2v32a78JdqUA==
+  dependencies:
+    "@types/node" "*"
+
 "@types/parse-json@^4.0.0":
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
@@ -8107,6 +8114,11 @@ pako@~1.0.5:
   resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
   integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
 
+papaparse@^5.3.1:
+  version "5.3.1"
+  resolved "https://registry.yarnpkg.com/papaparse/-/papaparse-5.3.1.tgz#770b7a9124d821d4b2132132b7bd7dce7194b5b1"
+  integrity sha512-Dbt2yjLJrCwH2sRqKFFJaN5XgIASO9YOFeFP8rIBRG2Ain8mqk5r1M6DkfvqEVozVcz3r3HaUGw253hA1nLIcA==
+
 parallel-transform@^1.1.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.2.0.tgz#9049ca37d6cb2182c3b1d2c720be94d14a5814fc"