Browse Source

add create collection ui

tumao 4 years ago
parent
commit
95c689160f

+ 8 - 1
client/src/components/customDialog/CustomDialog.tsx

@@ -22,6 +22,9 @@ const useStyles = makeStyles((theme: Theme) =>
       borderRadius: '8px',
       padding: 0,
     },
+    paperSm: {
+      maxWidth: '80%',
+    },
     title: {
       // padding: theme.spacing(4),
       '& p': {
@@ -80,7 +83,11 @@ const CustomDialog: FC<CustomDialogType> = props => {
 
   return (
     <Dialog
-      classes={{ paper: classes.paper, container: `${containerClass}` }}
+      classes={{
+        paper: classes.paper,
+        paperWidthSm: classes.paperSm,
+        container: `${containerClass}`,
+      }}
       open={open}
       onClose={handleCancel}
     >

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

@@ -17,6 +17,7 @@ import ExpandMore from '@material-ui/icons/ExpandMore';
 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 { SvgIcon } from '@material-ui/core';
 import { ReactComponent as MilvusIcon } from '../../assets/icons/milvus.svg';
 import { ReactComponent as OverviewIcon } from '../../assets/icons/overview.svg';
@@ -44,6 +45,7 @@ const icons: { [x in IconsType]: (props?: any) => React.ReactElement } = {
   back: (props = {}) => <ArrowBackIosIcon {...props} />,
   logout: (props = {}) => <ExitToAppIcon {...props} />,
   rightArrow: (props = {}) => <ArrowForwardIosIcon {...props} />,
+  remove: (props = {}) => <RemoveCircleOutlineIcon {...props} />,
 
   milvus: (props = {}) => (
     <SvgIcon viewBox="0 0 44 31" component={MilvusIcon} {...props} />

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

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

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

@@ -19,6 +19,12 @@ const collectionTrans = {
   general: '1. General Info',
   structure: '2. Define Structure',
   description: 'Description (Optional)',
+  fieldType: 'Field Type',
+  vectorFieldType: 'Vector Field Type',
+  fieldName: 'Field Name',
+  autoId: 'Auto ID',
+  dimension: 'Dimension',
+  newBtn: 'add new field',
 };
 
 export default collectionTrans;

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

@@ -19,6 +19,12 @@ const collectionTrans = {
   general: '1. General Info',
   structure: '2. Define Structure',
   description: 'Description (Optional)',
+  fieldType: 'Field Type',
+  vectorFieldType: 'Vector Field Type',
+  fieldName: 'Field Name',
+  autoId: 'Auto ID',
+  dimension: 'Dimension',
+  newBtn: 'add new field',
 };
 
 export default collectionTrans;

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

@@ -44,7 +44,7 @@ const Collections = () => {
     CollectionView[]
   >([]);
 
-  const { setDialog } = useContext(rootContext);
+  const { setDialog, handleCloseDialog } = useContext(rootContext);
   const { t } = useTranslation('collection');
 
   const classes = useStyles();
@@ -90,7 +90,7 @@ const Collections = () => {
   }, []);
 
   const handleCreateCollection = (param: CollectionCreateParam) => {
-    console.log('===== param', param);
+    handleCloseDialog();
   };
 
   const toolbarConfigs: ToolBarConfig[] = [

+ 11 - 0
client/src/pages/collections/Constants.ts

@@ -39,3 +39,14 @@ export const ALL_OPTIONS: KeyValuePair[] = [
     value: DataTypeEnum.Double,
   },
 ];
+
+export const AUTO_ID_OPTIONS: KeyValuePair[] = [
+  {
+    label: 'On',
+    value: 'On',
+  },
+  {
+    label: 'Off',
+    value: 'Off',
+  },
+];

+ 38 - 3
client/src/pages/collections/Create.tsx

@@ -6,9 +6,15 @@ import CustomInput from '../../components/customInput/CustomInput';
 import { ITextfieldConfig } from '../../components/customInput/Types';
 import { rootContext } from '../../context/Root';
 import { useFormValidation } from '../../hooks/Form';
+import { generateId } from '../../utils/Common';
 import { formatForm } from '../../utils/Form';
 import CreateFields from './CreateFields';
-import { CollectionCreateProps, DataTypeEnum, Field } from './Types';
+import {
+  CollectionCreateParam,
+  CollectionCreateProps,
+  DataTypeEnum,
+  Field,
+} from './Types';
 
 const useStyles = makeStyles((theme: Theme) => ({
   fieldset: {
@@ -17,6 +23,11 @@ const useStyles = makeStyles((theme: Theme) => ({
     justifyContent: 'space-between',
     alignItems: 'center',
 
+    '&:last-child': {
+      flexDirection: 'column',
+      alignItems: 'flex-start',
+    },
+
     '& legend': {
       marginBottom: theme.spacing(2),
       color: `#82838e`,
@@ -24,6 +35,9 @@ const useStyles = makeStyles((theme: Theme) => ({
       fontSize: '14px',
     },
   },
+  input: {
+    width: '48%',
+  },
 }));
 
 const CreateCollection: FC<CollectionCreateProps> = ({ handleCreate }) => {
@@ -45,6 +59,7 @@ const CreateCollection: FC<CollectionCreateProps> = ({ handleCreate }) => {
       name: '',
       desc: '',
       isDefault: true,
+      id: generateId(),
     },
     {
       type: DataTypeEnum.FloatVector,
@@ -53,9 +68,10 @@ const CreateCollection: FC<CollectionCreateProps> = ({ handleCreate }) => {
       dimension: '',
       desc: '',
       isDefault: true,
+      id: generateId(),
     },
   ]);
-  const [fieldsAllValid, setFieldsAllValid] = useState<boolean>(false);
+  const [fieldsAllValid, setFieldsAllValid] = useState<boolean>(true);
 
   const checkedForm = useMemo(() => {
     const { name } = form;
@@ -63,6 +79,13 @@ const CreateCollection: FC<CollectionCreateProps> = ({ handleCreate }) => {
   }, [form]);
   const { validation, checkIsValid, disabled } = useFormValidation(checkedForm);
 
+  const changeIsAutoID = (value: boolean) => {
+    setForm({
+      ...form,
+      autoID: value,
+    });
+  };
+
   const handleInputChange = (key: string, value: string) => {
     setForm(v => ({ ...v, [key]: value }));
   };
@@ -80,6 +103,7 @@ const CreateCollection: FC<CollectionCreateProps> = ({ handleCreate }) => {
           errorText: warningTrans('required', { name: t('name') }),
         },
       ],
+      className: classes.input,
     },
     {
       label: t('description'),
@@ -88,15 +112,24 @@ const CreateCollection: FC<CollectionCreateProps> = ({ handleCreate }) => {
       onChange: (value: string) => handleInputChange('desc', value),
       variant: 'filled',
       validations: [],
+      className: classes.input,
     },
   ];
 
+  const handleCreateCollection = () => {
+    const param: CollectionCreateParam = {
+      ...form,
+      fields,
+    };
+    handleCreate(param);
+  };
+
   return (
     <DialogTemplate
       title={t('createTitle')}
       handleCancel={handleCloseDialog}
       confirmLabel={btnTrans('create')}
-      handleConfirm={handleCreate}
+      handleConfirm={handleCreateCollection}
       confirmDisabled={disabled || !fieldsAllValid}
     >
       <form>
@@ -119,6 +152,8 @@ const CreateCollection: FC<CollectionCreateProps> = ({ handleCreate }) => {
             fields={fields}
             setFields={setFields}
             setfieldsAllValid={setFieldsAllValid}
+            autoID={form.autoID}
+            setAutoID={changeIsAutoID}
           />
         </fieldset>
       </form>

+ 315 - 0
client/src/pages/collections/CreateFields.tsx

@@ -0,0 +1,315 @@
+import { makeStyles, Theme, TextField, IconButton } from '@material-ui/core';
+import { FC, Fragment, ReactElement } from 'react';
+import { useTranslation } from 'react-i18next';
+import CustomButton from '../../components/customButton/CustomButton';
+import CustomSelector from '../../components/customSelector/CustomSelector';
+import icons from '../../components/icons/Icons';
+import { generateId } from '../../utils/Common';
+import { getCreateFieldType } from '../../utils/Format';
+import {
+  ALL_OPTIONS,
+  AUTO_ID_OPTIONS,
+  VECTOR_FIELDS_OPTIONS,
+} from './Constants';
+import {
+  CreateFieldsProps,
+  CreateFieldType,
+  DataTypeEnum,
+  Field,
+} from './Types';
+
+const useStyles = makeStyles((theme: Theme) => ({
+  rowWrapper: {
+    display: 'flex',
+    flexWrap: 'nowrap',
+    alignItems: 'center',
+    gap: '10px',
+    width: '100%',
+
+    '& .dimension': {
+      maxWidth: '160px',
+    },
+  },
+  input: {
+    fontSize: '14px',
+  },
+  primaryInput: {
+    maxWidth: '160px',
+  },
+  select: {
+    width: '160px',
+  },
+  descInput: {
+    minWidth: '270px',
+    flexGrow: 1,
+  },
+  btnTxt: {
+    textTransform: 'uppercase',
+  },
+  iconBtn: {
+    padding: 0,
+    width: '20px',
+    height: '20px',
+  },
+  mb3: {
+    marginBottom: theme.spacing(3),
+  },
+  mb2: {
+    marginBottom: theme.spacing(2),
+  },
+}));
+
+const CreateFields: FC<CreateFieldsProps> = ({
+  fields,
+  setFields,
+  // @TODO validation
+  setfieldsAllValid,
+  setAutoID,
+  autoID,
+}) => {
+  const { t } = useTranslation('collection');
+  const classes = useStyles();
+
+  const primaryInt64Value = 'INT64 (Primary key)';
+  const AddIcon = icons.add;
+  const RemoveIcon = icons.remove;
+
+  const getSelector = (
+    type: 'all' | 'vector',
+    label: string,
+    value: number,
+    onChange: (value: DataTypeEnum) => void
+  ) => (
+    <CustomSelector
+      options={type === 'all' ? ALL_OPTIONS : VECTOR_FIELDS_OPTIONS}
+      onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
+        onChange(e.target.value as DataTypeEnum);
+      }}
+      value={value}
+      variant="filled"
+      label={label}
+      classes={{ root: classes.select }}
+    />
+  );
+
+  const getInput = (
+    label: string,
+    value: string | number,
+    handleChange: (value: string) => void,
+    className = '',
+    inputClassName = '',
+    isReadOnly = false
+  ) => (
+    <TextField
+      label={label}
+      value={value}
+      onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
+        handleChange(e.target.value as string);
+      }}
+      variant="filled"
+      className={className}
+      InputProps={{
+        classes: {
+          input: inputClassName,
+        },
+      }}
+      disabled={isReadOnly}
+    />
+  );
+
+  const changeFields = (
+    id: string,
+    key: string,
+    value: string | DataTypeEnum
+  ) => {
+    const newFields = fields.map(f => {
+      if (f.id !== id) {
+        return f;
+      }
+      return {
+        ...f,
+        [key]: value,
+      };
+    });
+
+    setFields(newFields);
+  };
+
+  const handleAddNewField = () => {
+    const newDefaultItem: Field = {
+      name: '',
+      type: DataTypeEnum.Int16,
+      isPrimaryKey: false,
+      desc: '',
+      isDefault: false,
+      id: generateId(),
+    };
+    setFields([...fields, newDefaultItem]);
+  };
+
+  const handleRemoveField = (field: Field) => {
+    const newFields = fields.filter(f => f.id !== field.id);
+    setFields(newFields);
+  };
+
+  const generatePrimaryKeyRow = (field: Field): ReactElement => {
+    return (
+      <div className={`${classes.rowWrapper} ${classes.mb3}`}>
+        {getInput(
+          t('fieldType'),
+          primaryInt64Value,
+          () => {},
+          classes.primaryInput,
+          classes.input,
+          true
+        )}
+
+        {getInput(t('fieldName'), field.name, (value: string) =>
+          changeFields(field.id, 'name', value)
+        )}
+
+        <CustomSelector
+          label={t('autoId')}
+          options={AUTO_ID_OPTIONS}
+          value={autoID ? 'On' : 'Off'}
+          onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
+            const autoId = e.target.value === 'On';
+            setAutoID(autoId);
+          }}
+          variant="filled"
+          classes={{ root: classes.select }}
+        />
+
+        {getInput(
+          t('description'),
+          field.desc,
+          (value: string) => changeFields(field.id, 'desc', value),
+          classes.descInput
+        )}
+      </div>
+    );
+  };
+
+  const generateDefaultVectorRow = (field: Field): ReactElement => {
+    return (
+      <>
+        <div className={`${classes.rowWrapper} ${classes.mb2}`}>
+          {getSelector(
+            'vector',
+            t('fieldType'),
+            field.type,
+            (value: DataTypeEnum) => changeFields(field.id, 'type', value)
+          )}
+
+          {getInput(t('fieldName'), field.name, (value: string) =>
+            changeFields(field.id, 'name', value)
+          )}
+
+          {getInput(
+            t('dimension'),
+            field.dimension as number,
+            (value: string) => changeFields(field.id, 'dimension', value),
+            'dimension'
+          )}
+
+          {getInput(
+            t('description'),
+            field.desc,
+            (value: string) => changeFields(field.id, 'desc', value),
+            classes.descInput
+          )}
+        </div>
+
+        <CustomButton onClick={handleAddNewField} className={classes.mb2}>
+          <AddIcon />
+          <span className={classes.btnTxt}>{t('newBtn')}</span>
+        </CustomButton>
+      </>
+    );
+  };
+
+  const generateNumberRow = (field: Field): ReactElement => {
+    return (
+      <div className={`${classes.rowWrapper} ${classes.mb2}`}>
+        <IconButton
+          onClick={() => handleRemoveField(field)}
+          classes={{ root: classes.iconBtn }}
+          aria-label="delete"
+        >
+          <RemoveIcon />
+        </IconButton>
+        {getInput(t('fieldName'), field.name, (value: string) =>
+          changeFields(field.id, 'name', value)
+        )}
+        {getSelector('all', t('fieldType'), field.type, (value: DataTypeEnum) =>
+          changeFields(field.id, 'type', value)
+        )}
+
+        {getInput(
+          t('description'),
+          field.desc,
+          (value: string) => changeFields(field.id, 'desc', value),
+          classes.descInput
+        )}
+      </div>
+    );
+  };
+
+  const generateVectorRow = (field: Field) => {
+    return (
+      <div className={`${classes.rowWrapper} ${classes.mb2}`}>
+        <IconButton classes={{ root: classes.iconBtn }} aria-label="delete">
+          <RemoveIcon />
+        </IconButton>
+        {getInput(t('fieldName'), field.name, (value: string) =>
+          changeFields(field.id, 'name', value)
+        )}
+        {getSelector('all', t('fieldType'), field.type, (value: DataTypeEnum) =>
+          changeFields(field.id, 'type', value)
+        )}
+        {getInput(
+          t('dimension'),
+          field.dimension as number,
+          (value: string) => changeFields(field.id, 'dimension', value),
+          'dimension'
+        )}
+
+        {getInput(
+          t('description'),
+          field.desc,
+          (value: string) => changeFields(field.id, 'desc', value),
+          classes.descInput
+        )}
+      </div>
+    );
+  };
+
+  const generateFieldRow = (field: Field) => {
+    const createType: CreateFieldType = getCreateFieldType(field);
+    switch (createType) {
+      case 'primaryKey': {
+        return generatePrimaryKeyRow(field);
+      }
+      case 'defaultVector': {
+        return generateDefaultVectorRow(field);
+      }
+      case 'vector': {
+        return generateVectorRow(field);
+      }
+      // use number as default createType
+      default: {
+        return generateNumberRow(field);
+      }
+    }
+  };
+
+  return (
+    <>
+      {fields.map((field, index) => (
+        <Fragment key={index}>{generateFieldRow(field)}</Fragment>
+      ))}
+    </>
+  );
+};
+
+export default CreateFields;

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

@@ -41,12 +41,19 @@ export interface Field {
   desc: string;
   dimension?: number | string;
   isDefault?: boolean;
+  id: string;
 }
 
-export type CreateFieldType = 'primaryKey' | 'vector' | 'number';
+export type CreateFieldType =
+  | 'primaryKey'
+  | 'defaultVector'
+  | 'vector'
+  | 'number';
 
 export interface CreateFieldsProps {
   fields: Field[];
   setFields: Dispatch<SetStateAction<Field[]>>;
   setfieldsAllValid: Dispatch<SetStateAction<boolean>>;
+  autoID: boolean;
+  setAutoID: (value: boolean) => void;
 }

+ 4 - 1
client/src/utils/Format.ts

@@ -4,7 +4,6 @@ import {
   DataTypeEnum,
   Field,
 } from '../pages/collections/Types';
-import { KeyValuePair } from '../types/Common';
 
 /**
  * transform large capacity to capacity in b.
@@ -104,6 +103,10 @@ export const getCreateFieldType = (config: Field): CreateFieldType => {
     return 'primaryKey';
   }
 
+  if (config.isDefault) {
+    return 'defaultVector';
+  }
+
   const vectorTypes = [DataTypeEnum.BinaryVector, DataTypeEnum.FloatVector];
   if (vectorTypes.includes(config.type)) {
     return 'vector';