Browse Source

add import data modal stepper

tumao 4 years ago
parent
commit
cc136a169f

+ 5 - 0
client/src/assets/icons/upload.svg

@@ -0,0 +1,5 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M14 6.66669C13.6318 6.66669 13.3333 6.36821 13.3333 6.00002L13.3333 3.33335C13.3333 3.15654 13.2631 2.98697 13.1381 2.86195C13.0131 2.73692 12.8435 2.66669 12.6667 2.66669L3.33334 2.66669C3.15653 2.66669 2.98696 2.73693 2.86193 2.86195C2.73691 2.98697 2.66667 3.15654 2.66667 3.33335L2.66667 6.00002C2.66667 6.36821 2.36819 6.66669 2 6.66669C1.63181 6.66669 1.33334 6.36821 1.33334 6.00002L1.33334 3.33335C1.33334 2.80292 1.54405 2.29421 1.91912 1.91914C2.2942 1.54407 2.8029 1.33335 3.33334 1.33335L12.6667 1.33335C13.1971 1.33335 13.7058 1.54407 14.0809 1.91914C14.456 2.29421 14.6667 2.80292 14.6667 3.33335L14.6667 6.00002C14.6667 6.36821 14.3682 6.66669 14 6.66669Z" fill="white"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M11.8047 9.80474C11.5444 10.0651 11.1223 10.0651 10.8619 9.80474L8 6.94281L5.13807 9.80474C4.87772 10.0651 4.45561 10.0651 4.19526 9.80474C3.93491 9.54439 3.93491 9.12228 4.19526 8.86193L7.5286 5.5286C7.78894 5.26825 8.21105 5.26825 8.4714 5.5286L11.8047 8.86193C12.0651 9.12228 12.0651 9.54439 11.8047 9.80474Z" fill="white"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M8.00001 14.6667C7.63182 14.6667 7.33334 14.3682 7.33334 14L7.33334 6.00002C7.33334 5.63183 7.63181 5.33335 8 5.33335C8.36819 5.33335 8.66667 5.63183 8.66667 6.00002L8.66667 14C8.66667 14.3682 8.36819 14.6667 8.00001 14.6667Z" fill="white"/>
+</svg>

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

@@ -14,7 +14,7 @@ describe('test dialog template component', () => {
         <I18nextProvider i18n={i18n}>
           <DialogTemplate
             title="dialog template"
-            handleCancel={mockCancelFn}
+            handleClose={mockCancelFn}
             handleConfirm={mockConfirmFn}
           >
             dialog content
@@ -38,7 +38,7 @@ describe('test dialog template component', () => {
         <I18nextProvider i18n={i18n}>
           <DialogTemplate
             title="dialog template"
-            handleCancel={mockCancelFn}
+            handleClose={mockCancelFn}
             handleConfirm={mockConfirmFn}
             confirmDisabled={true}
           >

+ 23 - 15
client/src/components/customDialog/DialogTemplate.tsx

@@ -19,35 +19,43 @@ const useStyles = makeStyles((theme: Theme) => ({
 const DialogTemplate: FC<DialogContainerProps> = ({
   title,
   cancelLabel,
+  handleClose,
   handleCancel,
   confirmLabel,
   handleConfirm,
   confirmDisabled,
   children,
+  showActions = true,
+  showCancel = true,
 }) => {
   const { t } = useTranslation('btn');
   const cancel = cancelLabel || t('cancel');
   const confirm = confirmLabel || t('confirm');
   const classes = useStyles();
+  const onCancel = handleCancel || handleClose;
 
   return (
     <>
-      <CustomDialogTitle onClose={handleCancel}>{title}</CustomDialogTitle>
+      <CustomDialogTitle onClose={handleClose}>{title}</CustomDialogTitle>
       <DialogContent>{children}</DialogContent>
-      <DialogActions className={classes.actions}>
-        <CustomButton onClick={handleCancel} color="default" name="cancel">
-          {cancel}
-        </CustomButton>
-        <CustomButton
-          variant="contained"
-          onClick={handleConfirm}
-          color="primary"
-          disabled={confirmDisabled}
-          name="confirm"
-        >
-          {confirm}
-        </CustomButton>
-      </DialogActions>
+      {showActions && (
+        <DialogActions className={classes.actions}>
+          {showCancel && (
+            <CustomButton onClick={onCancel} color="default" name="cancel">
+              {cancel}
+            </CustomButton>
+          )}
+          <CustomButton
+            variant="contained"
+            onClick={handleConfirm}
+            color="primary"
+            disabled={confirmDisabled}
+            name="confirm"
+          >
+            {confirm}
+          </CustomButton>
+        </DialogActions>
+      )}
     </>
   );
 };

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

@@ -1,3 +1,4 @@
+import { ReactElement } from 'react';
 import { DialogType } from '../../context/Types';
 export type CustomDialogType = DialogType & {
   onClose: () => void;
@@ -21,9 +22,12 @@ export type DeleteDialogContentType = {
 
 export type DialogContainerProps = {
   title: string;
-  cancelLabel?: string;
-  confirmLabel?: string;
-  handleCancel: () => void;
+  cancelLabel?: string | ReactElement;
+  confirmLabel?: string | ReactElement;
+  handleClose: () => void;
+  handleCancel?: () => void;
   handleConfirm: (param: any) => void;
   confirmDisabled?: boolean;
+  showActions?: boolean;
+  showCancel?: boolean;
 };

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

@@ -27,6 +27,7 @@ import { ReactComponent as InfoIcon } from '../../assets/icons/info.svg';
 import { ReactComponent as ReleaseIcon } from '../../assets/icons/release.svg';
 import { ReactComponent as LoadIcon } from '../../assets/icons/load.svg';
 import { ReactComponent as KeyIcon } from '../../assets/icons/key.svg';
+import { ReactComponent as UploadIcon } from '../../assets/icons/upload.svg';
 
 const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
   search: (props = {}) => <SearchIcon {...props} />,
@@ -72,6 +73,9 @@ const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
   key: (props = {}) => (
     <SvgIcon viewBox="0 0 16 16" component={KeyIcon} {...props} />
   ),
+  upload: (props = {}) => (
+    <SvgIcon viewBox="0 0 16 16" component={UploadIcon} {...props} />
+  ),
 };
 
 export default icons;

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

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

+ 23 - 0
client/src/hooks/Insert.tsx

@@ -0,0 +1,23 @@
+import { ReactElement, useContext } from 'react';
+import { rootContext } from '../context/Root';
+
+export const useInsertHook = () => {
+  const { setDialog } = useContext(rootContext);
+
+  const handleInsertDialog = (
+    // stepper container, contains all contents
+    component: ReactElement
+  ) => {
+    setDialog({
+      open: true,
+      type: 'custom',
+      params: {
+        component,
+      },
+    });
+  };
+
+  return {
+    handleInsertDialog,
+  };
+};

+ 2 - 2
client/src/hooks/Dialog.tsx → client/src/hooks/ReleaseAndLoad.tsx

@@ -8,11 +8,11 @@ import { StatusEnum } from '../components/status/Types';
 import { CollectionData } from '../pages/overview/collectionCard/Types';
 
 // handle release and load dialog
-export interface DialogHookProps {
+export interface LoadAndReleaseHookProps {
   type: 'partition' | 'collection';
 }
 
-export const useDialogHook = (props: DialogHookProps) => {
+export const useLoadAndReleaseHook = (props: LoadAndReleaseHookProps) => {
   const { type } = props;
   const { setDialog } = useContext(rootContext);
   const { t: dialogTrans } = useTranslation('dialog');

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

@@ -11,6 +11,10 @@ const btnTrans = {
   release: 'Release',
   create: 'Create',
   load: 'Load',
+  insert: 'Import Data',
+  next: 'Next',
+  previous: 'Previous',
+  done: 'Done',
 };
 
 export default btnTrans;

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

@@ -0,0 +1,6 @@
+const insertTrans = {
+  import: 'Import Data',
+  targetTip: 'Where to put your data',
+};
+
+export default insertTrans;

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

@@ -11,6 +11,10 @@ const btnTrans = {
   delete: 'Delete',
   release: 'Release',
   load: 'Load',
+  insert: 'Import Data',
+  next: 'Next',
+  previous: 'Previous',
+  done: 'Done',
 };
 
 export default btnTrans;

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

@@ -0,0 +1,6 @@
+const insertTrans = {
+  import: 'Import Data',
+  targetTip: 'Where to put your data',
+};
+
+export default insertTrans;

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

@@ -21,6 +21,8 @@ import successEn from './en/success';
 import successCn from './cn/success';
 import indexEn from './en/index';
 import indexCn from './cn/index';
+import insertEn from './en/insert';
+import insertCn from './cn/insert';
 
 export const resources = {
   cn: {
@@ -34,6 +36,7 @@ export const resources = {
     partition: partitionCn,
     success: successCn,
     index: indexCn,
+    insert: insertCn,
   },
   en: {
     translation: commonEn,
@@ -46,6 +49,7 @@ export const resources = {
     partition: partitionEn,
     success: successEn,
     index: indexEn,
+    insert: insertEn,
   },
 };
 

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

@@ -19,9 +19,10 @@ import { rootContext } from '../../context/Root';
 import CreateCollection from './Create';
 import DeleteTemplate from '../../components/customDialog/DeleteDialogTemplate';
 import { CollectionHttp } from '../../http/Collection';
-import { useDialogHook } from '../../hooks/Dialog';
+import { useLoadAndReleaseHook } from '../../hooks/ReleaseAndLoad';
 import Highlighter from 'react-highlight-words';
 import { parseLocationSearch } from '../../utils/Format';
+import InsertContainer from '../insert/Container';
 
 const useStyles = makeStyles((theme: Theme) => ({
   emptyWrapper: {
@@ -52,7 +53,7 @@ const { search = '' } = parseLocationSearch(window.location.search);
 
 const Collections = () => {
   useNavigationHook(ALL_ROUTER_TYPES.COLLECTIONS);
-  const { handleAction } = useDialogHook({ type: 'collection' });
+  const { handleAction } = useLoadAndReleaseHook({ type: 'collection' });
   const [collections, setCollections] = useState<CollectionView[]>([]);
   const [searchedCollections, setSearchedCollections] = useState<
     CollectionView[]
@@ -224,6 +225,26 @@ const Collections = () => {
       },
       icon: 'add',
     },
+    {
+      label: btnTrans('insert'),
+      onClick: () => {
+        setDialog({
+          open: true,
+          type: 'custom',
+          params: {
+            component: <InsertContainer />,
+          },
+        });
+      },
+      /**
+       * 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',
+    },
     {
       type: 'iconBtn',
       onClick: () => {

+ 1 - 1
client/src/pages/collections/Create.tsx

@@ -175,7 +175,7 @@ const CreateCollection: FC<CollectionCreateProps> = ({ handleCreate }) => {
   return (
     <DialogTemplate
       title={collectionTrans('createTitle')}
-      handleCancel={handleCloseDialog}
+      handleClose={handleCloseDialog}
       confirmLabel={btnTrans('create')}
       handleConfirm={handleCreateCollection}
       confirmDisabled={disabled || !allFieldsValid}

+ 145 - 0
client/src/pages/insert/Container.tsx

@@ -0,0 +1,145 @@
+import { makeStyles, Theme } from '@material-ui/core';
+import { useContext, useMemo, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import DialogTemplate from '../../components/customDialog/DialogTemplate';
+import icons from '../../components/icons/Icons';
+import { rootContext } from '../../context/Root';
+import InsertImport from './Import';
+import InsertPreview from './Preview';
+import InsertStatus from './Status';
+import { InsertStatusEnum, InsertStepperEnum } from './Types';
+
+const getStyles = makeStyles((theme: Theme) => ({
+  icon: {
+    fontSize: '16px',
+  },
+}));
+
+/**
+ * this component contains processes during insert
+ * all interactions with server are done in it
+ */
+
+const InsertContainer = () => {
+  const classes = getStyles();
+
+  const { t: insertTrans } = useTranslation('insert');
+  const { t: btnTrans } = useTranslation('btn');
+  const { handleCloseDialog } = useContext(rootContext);
+  const [activeStep, setActiveStep] = useState<InsertStepperEnum>(
+    InsertStepperEnum.import
+  );
+  const [insertStatus, setInsertStauts] = useState<InsertStatusEnum>(
+    InsertStatusEnum.init
+  );
+  // const [nextDisabled, setNextDisabled] = useState<boolean>(false);
+
+  const BackIcon = icons.back;
+
+  const { confirm, cancel } = useMemo(() => {
+    /**
+     * activeStep type is InsertStepperEnum
+     * so index 0 represents import,
+     * index 1 represents preview,
+     * index 2 represents status
+     */
+    const labelList = [
+      {
+        confirm: btnTrans('next'),
+        cancel: btnTrans('cancel'),
+      },
+      {
+        confirm: btnTrans('insert'),
+        cancel: (
+          <>
+            <BackIcon classes={{ root: classes.icon }} />
+            {btnTrans('previous')}
+          </>
+        ),
+      },
+      {
+        confirm: btnTrans('done'),
+        cancel: '',
+      },
+    ];
+    return labelList[activeStep];
+  }, [activeStep, btnTrans, BackIcon, classes.icon]);
+
+  const { showActions, showCancel } = useMemo(() => {
+    return {
+      showActions: insertStatus !== InsertStatusEnum.loading,
+      showCancel: insertStatus === InsertStatusEnum.init,
+    };
+  }, [insertStatus]);
+
+  const handleInsertData = () => {
+    // mock status change
+    setInsertStauts(InsertStatusEnum.loading);
+    setTimeout(() => {
+      setInsertStauts(InsertStatusEnum.success);
+    }, 500);
+  };
+
+  const handleNext = () => {
+    switch (activeStep) {
+      case InsertStepperEnum.import:
+        setActiveStep(activeStep => activeStep + 1);
+        break;
+      case InsertStepperEnum.preview:
+        setActiveStep(activeStep => activeStep + 1);
+        // @TODO insert interactions
+        handleInsertData();
+        break;
+      // default represent InsertStepperEnum.status
+      default:
+        handleCloseDialog();
+        break;
+    }
+  };
+
+  const handleBack = () => {
+    switch (activeStep) {
+      case InsertStepperEnum.import:
+        handleCloseDialog();
+        break;
+      case InsertStepperEnum.preview:
+        setActiveStep(activeStep => activeStep - 1);
+        // @TODO reset uploaded file?
+        break;
+      // default represent InsertStepperEnum.status
+      // status don't have cancel button
+      default:
+        break;
+    }
+  };
+
+  const generateContent = (activeStep: InsertStepperEnum) => {
+    switch (activeStep) {
+      case InsertStepperEnum.import:
+        return <InsertImport />;
+      case InsertStepperEnum.preview:
+        return <InsertPreview />;
+      // default represents InsertStepperEnum.status
+      default:
+        return <InsertStatus />;
+    }
+  };
+
+  return (
+    <DialogTemplate
+      title={insertTrans('import')}
+      handleClose={handleCloseDialog}
+      confirmLabel={confirm}
+      cancelLabel={cancel}
+      handleCancel={handleBack}
+      handleConfirm={handleNext}
+      confirmDisabled={false}
+      showActions={showActions}
+      showCancel={showCancel}
+    >
+      {generateContent(activeStep)}
+    </DialogTemplate>
+  );
+};
+
+export default InsertContainer;

+ 94 - 0
client/src/pages/insert/Import.tsx

@@ -0,0 +1,94 @@
+import { useTranslation } from 'react-i18next';
+import Typography from '@material-ui/core/Typography';
+import { makeStyles, Theme, Divider } from '@material-ui/core';
+import CustomSelector from '../../components/customSelector/CustomSelector';
+import { Option } from '../../components/customSelector/Types';
+
+const getStyles = makeStyles((theme: Theme) => ({
+  tip: {
+    color: theme.palette.milvusGrey.dark,
+  },
+  selectorWrapper: {
+    display: 'flex',
+    justifyContent: 'space-between',
+    alignItems: 'center',
+
+    '& .selector': {
+      flexBasis: '40%',
+      minWidth: '256px',
+    },
+
+    '& .divider': {
+      width: '20px',
+      margin: theme.spacing(0, 4),
+      color: theme.palette.milvusGrey.dark,
+    },
+  },
+  uploadWrapper: {
+    backgroundColor: '#f9f9f9',
+    padding: theme.spacing(1),
+  },
+}));
+
+const InsertImport = () => {
+  const { t: insertTrans } = useTranslation('insert');
+  const { t: collectionTrans } = useTranslation('collection');
+  const { t: partitionTrans } = useTranslation('partition');
+  const classes = getStyles();
+
+  const collectionOptions: Option[] = [
+    {
+      label: 'a',
+      value: 'a',
+    },
+    {
+      label: 'b',
+      value: 'b',
+    },
+  ];
+  const partitionOptions: Option[] = [
+    {
+      label: 'a',
+      value: 'a',
+    },
+    {
+      label: 'b',
+      value: 'b',
+    },
+  ];
+
+  const handleCollectionChange = () => {};
+  const handlePartitionChange = () => {};
+
+  return (
+    <section>
+      <Typography className={classes.tip}>
+        {insertTrans('targetTip')}
+      </Typography>
+
+      <form className={classes.selectorWrapper}>
+        <CustomSelector
+          options={collectionOptions}
+          classes={{ root: 'selector' }}
+          value={''}
+          variant="filled"
+          label={collectionTrans('collection')}
+          onChange={handleCollectionChange}
+        />
+        <Divider classes={{ root: 'divider' }} />
+        <CustomSelector
+          options={partitionOptions}
+          classes={{ root: 'selector' }}
+          value={''}
+          variant="filled"
+          label={partitionTrans('partition')}
+          onChange={handlePartitionChange}
+        />
+      </form>
+
+      <div className={classes.uploadWrapper}></div>
+    </section>
+  );
+};
+
+export default InsertImport;

+ 6 - 0
client/src/pages/insert/Preview.tsx

@@ -0,0 +1,6 @@
+const InsertPreview = () => {
+  console.log('enter preview');
+  return <div>preview</div>;
+};
+
+export default InsertPreview;

+ 6 - 0
client/src/pages/insert/Status.tsx

@@ -0,0 +1,6 @@
+const InsertStatus = () => {
+  console.log('enter insert status');
+  return <div>status</div>;
+};
+
+export default InsertStatus;

+ 13 - 0
client/src/pages/insert/Types.ts

@@ -0,0 +1,13 @@
+export enum InsertStepperEnum {
+  import,
+  preview,
+  status,
+}
+
+export enum InsertStatusEnum {
+  // init means not begin yet
+  init = 'init',
+  loading = 'loading',
+  success = 'success',
+  error = 'error',
+}

+ 2 - 2
client/src/pages/overview/Overview.tsx

@@ -5,7 +5,7 @@ import EmptyCard from '../../components/cards/EmptyCard';
 import icons from '../../components/icons/Icons';
 import { StatusEnum } from '../../components/status/Types';
 import { rootContext } from '../../context/Root';
-import { useDialogHook } from '../../hooks/Dialog';
+import { useLoadAndReleaseHook } from '../../hooks/ReleaseAndLoad';
 import { useNavigationHook } from '../../hooks/Navigation';
 import { CollectionHttp } from '../../http/Collection';
 import { ALL_ROUTER_TYPES } from '../../router/Types';
@@ -31,7 +31,7 @@ const useStyles = makeStyles((theme: Theme) => ({
 
 const Overview = () => {
   useNavigationHook(ALL_ROUTER_TYPES.OVERVIEW);
-  const { handleAction } = useDialogHook({ type: 'collection' });
+  const { handleAction } = useLoadAndReleaseHook({ type: 'collection' });
   const classes = useStyles();
   const { t: overviewTrans } = useTranslation('overview');
   const { t: collectionTrans } = useTranslation('collection');

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

@@ -63,7 +63,7 @@ const CreatePartition: FC<PartitionCreateProps> = ({
   return (
     <DialogTemplate
       title={partitionTrans('createTitle')}
-      handleCancel={handleClose}
+      handleClose={handleClose}
       confirmLabel={btnTrans('create')}
       handleConfirm={handleCreatePartition}
       confirmDisabled={disabled}

+ 1 - 1
client/src/pages/schema/Create.tsx

@@ -138,7 +138,7 @@ const CreateIndex = (props: {
         type: indexTrans('index'),
         name: collectionName,
       })}
-      handleCancel={handleCancel}
+      handleClose={handleCancel}
       confirmLabel={btnTrans('create')}
       handleConfirm={handleCreateIndex}
       confirmDisabled={disabled}

+ 5 - 0
client/src/styles/common.css

@@ -1,3 +1,8 @@
+/**
+ * we usually use Material makeStyles to write component / page style
+ * this file is used for some global scope reusable style
+ */
+
 /* reset some elements styles */
 fieldset {
   border: 0;