Browse Source

Merge pull request #40 from nameczz/feature/collection-validation

Feature/collection validation
ryjiang 4 years ago
parent
commit
fe6f9f2be7

+ 2 - 2
client/src/components/customInput/CustomInput.tsx

@@ -257,12 +257,12 @@ const createHelperTextNode = (hint: string): ReactElement => {
   const classes = getStyles();
   return (
     <span className={classes.errWrapper}>
-      {Icons.error({
+      {/* {Icons.error({
         fontSize: 'small',
         classes: {
           root: classes.errBtn,
         },
-      })}
+      })} */}
       {hint}
     </span>
   );

+ 2 - 0
client/src/consts/Milvus.tsx

@@ -109,6 +109,8 @@ export const INDEX_OPTIONS_MAP = {
   ],
 };
 
+export const PRIMARY_KEY_FIELD = 'INT64 (Primary key)';
+
 export enum EmbeddingTypeEnum {
   float = 'FLOAT_POINT',
   binary = 'BINARY',

+ 2 - 2
client/src/hooks/Form.ts

@@ -1,6 +1,6 @@
 import { useState } from 'react';
 import { IValidation } from '../components/customInput/Types';
-import { checkIsEmpty, getCheckResult } from '../utils/Validation';
+import { checkEmptyValid, getCheckResult } from '../utils/Validation';
 
 export interface IForm {
   key: string;
@@ -95,7 +95,7 @@ export const useFormValidation = (form: IForm[]): IValidationInfo => {
 
   const checkFormValid = (form: IForm[]): boolean => {
     const requireCheckItems = form.filter(f => f.needCheck);
-    if (requireCheckItems.some(item => !checkIsEmpty(item.value))) {
+    if (requireCheckItems.some(item => !checkEmptyValid(item.value))) {
       return false;
     }
 

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

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

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

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

+ 11 - 5
client/src/pages/collections/Collections.tsx

@@ -4,7 +4,7 @@ import { useNavigationHook } from '../../hooks/Navigation';
 import { ALL_ROUTER_TYPES } from '../../router/Types';
 import MilvusGrid from '../../components/grid';
 import CustomToolBar from '../../components/grid/ToolBar';
-import { CollectionCreateParam, CollectionView } from './Types';
+import { CollectionCreateParam, CollectionView, DataTypeEnum } from './Types';
 import { ColDefinitionsType, ToolBarConfig } from '../../components/grid/Types';
 import { usePaginationHook } from '../../hooks/Pagination';
 import icons from '../../components/icons/Icons';
@@ -106,10 +106,16 @@ const Collections = () => {
 
   const handleCreateCollection = async (param: CollectionCreateParam) => {
     const data: CollectionCreateParam = JSON.parse(JSON.stringify(param));
-    data.fields = data.fields.map(v => ({
-      ...v,
-      type_params: [{ key: 'dim', value: v.dimension }],
-    }));
+    const vectorType = [DataTypeEnum.BinaryVector, DataTypeEnum.FloatVector];
+
+    data.fields = data.fields.map(v =>
+      vectorType.includes(v.data_type)
+        ? {
+            ...v,
+            type_params: [{ key: 'dim', value: v.dimension }],
+          }
+        : v
+    );
     await CollectionHttp.createCollection(data);
     handleCloseDialog();
     openSnackBar(successTrans('create', { name: t('collection') }));

+ 32 - 10
client/src/pages/collections/Create.tsx

@@ -6,7 +6,6 @@ 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 {
@@ -52,31 +51,45 @@ const CreateCollection: FC<CollectionCreateProps> = ({ handleCreate }) => {
     description: '',
     autoID: true,
   });
+
   const [fields, setFields] = useState<Field[]>([
     {
       data_type: DataTypeEnum.Int64,
       is_primary_key: true,
-      name: '',
+      name: null, // we need hide helpertext at first time, so we use null to detect user enter input or not.
       description: '',
       isDefault: true,
-      id: generateId(),
+      id: '1',
     },
     {
       data_type: DataTypeEnum.FloatVector,
       is_primary_key: false,
-      name: '',
-      dimension: '',
+      name: null,
+      dimension: '128',
       description: '',
       isDefault: true,
-      id: generateId(),
+      id: '2',
     },
   ]);
-  const [fieldsAllValid, setFieldsAllValid] = useState<boolean>(true);
+
+  const [fieldsValidation, setFieldsValidation] = useState<
+    {
+      [x: string]: string | boolean;
+    }[]
+  >([
+    { id: '1', name: false },
+    { id: '2', name: false, dimension: true },
+  ]);
+
+  const allFieldsValid = useMemo(() => {
+    return fieldsValidation.every(v => Object.keys(v).every(key => !!v[key]));
+  }, [fieldsValidation]);
 
   const checkedForm = useMemo(() => {
     const { collection_name } = form;
     return formatForm({ collection_name });
   }, [form]);
+
   const { validation, checkIsValid, disabled } = useFormValidation(checkedForm);
 
   const changeIsAutoID = (value: boolean) => {
@@ -117,9 +130,18 @@ const CreateCollection: FC<CollectionCreateProps> = ({ handleCreate }) => {
   ];
 
   const handleCreateCollection = () => {
+    const vectorType = [DataTypeEnum.BinaryVector, DataTypeEnum.FloatVector];
     const param: CollectionCreateParam = {
       ...form,
-      fields,
+      fields: fields.map(v => {
+        return {
+          name: v.name,
+          description: v.description,
+          is_primary_key: v.is_primary_key,
+          data_type: v.data_type,
+          dimension: vectorType.includes(v.data_type) ? v.dimension : undefined,
+        };
+      }),
     };
     handleCreate(param);
   };
@@ -130,7 +152,7 @@ const CreateCollection: FC<CollectionCreateProps> = ({ handleCreate }) => {
       handleCancel={handleCloseDialog}
       confirmLabel={btnTrans('create')}
       handleConfirm={handleCreateCollection}
-      confirmDisabled={disabled || !fieldsAllValid}
+      confirmDisabled={disabled || !allFieldsValid}
     >
       <form>
         <fieldset className={classes.fieldset}>
@@ -151,7 +173,7 @@ const CreateCollection: FC<CollectionCreateProps> = ({ handleCreate }) => {
           <CreateFields
             fields={fields}
             setFields={setFields}
-            setfieldsAllValid={setFieldsAllValid}
+            setFieldsValidation={setFieldsValidation}
             autoID={form.autoID}
             setAutoID={changeIsAutoID}
           />

+ 180 - 98
client/src/pages/collections/CreateFields.tsx

@@ -4,8 +4,10 @@ import { useTranslation } from 'react-i18next';
 import CustomButton from '../../components/customButton/CustomButton';
 import CustomSelector from '../../components/customSelector/CustomSelector';
 import icons from '../../components/icons/Icons';
+import { PRIMARY_KEY_FIELD } from '../../consts/Milvus';
 import { generateId } from '../../utils/Common';
 import { getCreateFieldType } from '../../utils/Format';
+import { checkEmptyValid, getCheckResult } from '../../utils/Validation';
 import {
   ALL_OPTIONS,
   AUTO_ID_OPTIONS,
@@ -38,6 +40,7 @@ const useStyles = makeStyles((theme: Theme) => ({
   },
   select: {
     width: '160px',
+    marginBottom: '22px',
   },
   descInput: {
     minWidth: '270px',
@@ -57,20 +60,34 @@ const useStyles = makeStyles((theme: Theme) => ({
   mb2: {
     marginBottom: theme.spacing(2),
   },
+  helperText: {
+    color: theme.palette.error.main,
+  },
 }));
 
+type inputType = {
+  label: string;
+  value: string | number | null;
+  handleChange?: (value: string) => void;
+  className?: string;
+  inputClassName?: string;
+  isReadOnly?: boolean;
+  validate?: (value: string | number | null) => string;
+  type?: 'number' | 'text';
+};
+
 const CreateFields: FC<CreateFieldsProps> = ({
   fields,
   setFields,
-  // @TODO validation
-  setfieldsAllValid,
   setAutoID,
   autoID,
+  setFieldsValidation,
 }) => {
-  const { t } = useTranslation('collection');
+  const { t: collectionTrans } = useTranslation('collection');
+  const { t: warningTrans } = useTranslation('warning');
+
   const classes = useStyles();
 
-  const primaryInt64Value = 'INT64 (Primary key)';
   const AddIcon = icons.add;
   const RemoveIcon = icons.remove;
 
@@ -94,36 +111,131 @@ const CreateFields: FC<CreateFieldsProps> = ({
     );
   };
 
-  const getInput = (
-    label: string,
-    value: string | number,
-    handleChange: (value: string) => void,
-    className = '',
-    inputClassName = '',
-    isReadOnly = false
-  ) => (
-    <TextField
-      label={label}
-      value={value}
-      onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
-        handleChange(e.target.value as string);
-      }}
-      variant="filled"
-      className={className}
-      InputProps={{
-        classes: {
-          input: inputClassName,
+  const getInput = (data: inputType) => {
+    const {
+      label,
+      value,
+      handleChange = () => {},
+      className = '',
+      inputClassName = '',
+      isReadOnly = false,
+      validate = (value: string | number | null) => ' ',
+      type = 'text',
+    } = data;
+    return (
+      <TextField
+        label={label}
+        // value={value}
+        onBlur={(e: React.ChangeEvent<{ value: unknown }>) => {
+          handleChange(e.target.value as string);
+        }}
+        variant="filled"
+        className={className}
+        InputProps={{
+          classes: {
+            input: inputClassName,
+          },
+        }}
+        disabled={isReadOnly}
+        helperText={validate(value)}
+        FormHelperTextProps={{
+          className: classes.helperText,
+        }}
+        defaultValue={value}
+        type={type}
+      />
+    );
+  };
+
+  const generateFieldName = (field: Field) => {
+    return getInput({
+      label: collectionTrans('fieldName'),
+      value: field.name,
+      handleChange: (value: string) => {
+        const isValid = checkEmptyValid(value);
+        setFieldsValidation(v =>
+          v.map(item =>
+            item.id === field.id! ? { ...item, name: isValid } : item
+          )
+        );
+
+        changeFields(field.id!, 'name', value);
+      },
+      validate: (value: any) => {
+        if (value === null) return ' ';
+        const isValid = checkEmptyValid(value);
+
+        return isValid
+          ? ' '
+          : warningTrans('required', { name: collectionTrans('fieldName') });
+      },
+    });
+  };
+
+  const generateDesc = (field: Field) => {
+    return getInput({
+      label: collectionTrans('description'),
+      value: field.description,
+      handleChange: (value: string) =>
+        changeFields(field.id!, 'description', value),
+      className: classes.descInput,
+    });
+  };
+
+  const generateDimension = (field: Field) => {
+    const validateDimension = (value: string) => {
+      const isPositive = getCheckResult({
+        value,
+        rule: 'positiveNumber',
+      });
+      const isMutiple = getCheckResult({
+        value,
+        rule: 'multiple',
+        extraParam: {
+          multipleNumber: 8,
         },
-      }}
-      disabled={isReadOnly}
-    />
-  );
+      });
+      if (field.data_type === DataTypeEnum.BinaryVector) {
+        return {
+          isMutiple,
+          isPositive,
+        };
+      }
+      return {
+        isPositive,
+      };
+    };
+    return getInput({
+      label: collectionTrans('dimension'),
+      value: field.dimension as number,
+      handleChange: (value: string) => {
+        const { isPositive, isMutiple } = validateDimension(value);
+        const isValid =
+          field.data_type === DataTypeEnum.BinaryVector
+            ? !!isMutiple && isPositive
+            : isPositive;
 
-  const changeFields = (
-    id: string,
-    key: string,
-    value: string | DataTypeEnum
-  ) => {
+        changeFields(field.id!, 'dimension', `${value}`);
+
+        setFieldsValidation(v =>
+          v.map(item =>
+            item.id === field.id! ? { ...item, dimension: isValid } : item
+          )
+        );
+      },
+      type: 'number',
+      validate: (value: any) => {
+        const { isPositive, isMutiple } = validateDimension(value);
+        if (isMutiple === false) {
+          return collectionTrans('dimensionMutipleWarning');
+        }
+
+        return isPositive ? ' ' : collectionTrans('dimensionPositiveWarning');
+      },
+    });
+  };
+
+  const changeFields = (id: string, key: string, value: any) => {
     const newFields = fields.map(f => {
       if (f.id !== id) {
         return f;
@@ -137,20 +249,29 @@ const CreateFields: FC<CreateFieldsProps> = ({
   };
 
   const handleAddNewField = () => {
+    const id = generateId();
     const newDefaultItem: Field = {
-      name: '',
+      name: null,
       data_type: DataTypeEnum.Int16,
       is_primary_key: false,
       description: '',
       isDefault: false,
-      id: generateId(),
+      dimension: '128',
+      id,
+    };
+    const newValidation = {
+      id,
+      name: false,
+      dimension: true,
     };
     setFields([...fields, newDefaultItem]);
+    setFieldsValidation(v => [...v, newValidation]);
   };
 
   const handleRemoveField = (field: Field) => {
     const newFields = fields.filter(f => f.id !== field.id);
     setFields(newFields);
+    setFieldsValidation(v => v.filter(item => item.id !== field.id));
   };
 
   const generatePrimaryKeyRow = (
@@ -159,21 +280,18 @@ const CreateFields: FC<CreateFieldsProps> = ({
   ): ReactElement => {
     return (
       <div className={`${classes.rowWrapper} ${classes.mb3}`}>
-        {getInput(
-          t('fieldType'),
-          primaryInt64Value,
-          () => {},
-          classes.primaryInput,
-          classes.input,
-          true
-        )}
+        {getInput({
+          label: collectionTrans('fieldType'),
+          value: PRIMARY_KEY_FIELD,
+          className: classes.primaryInput,
+          inputClassName: classes.input,
+          isReadOnly: true,
+        })}
 
-        {getInput(t('fieldName'), field.name, (value: string) =>
-          changeFields(field.id, 'name', value)
-        )}
+        {generateFieldName(field)}
 
         <CustomSelector
-          label={t('autoId')}
+          label={collectionTrans('autoId')}
           options={AUTO_ID_OPTIONS}
           value={autoID ? 'true' : 'false'}
           onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
@@ -184,12 +302,7 @@ const CreateFields: FC<CreateFieldsProps> = ({
           classes={{ root: classes.select }}
         />
 
-        {getInput(
-          t('description'),
-          field.description,
-          (value: string) => changeFields(field.id, 'description', value),
-          classes.descInput
-        )}
+        {generateDesc(field)}
       </div>
     );
   };
@@ -200,33 +313,21 @@ const CreateFields: FC<CreateFieldsProps> = ({
         <div className={`${classes.rowWrapper} ${classes.mb2}`}>
           {getSelector(
             'vector',
-            t('fieldType'),
+            collectionTrans('fieldType'),
             field.data_type,
-            (value: DataTypeEnum) => changeFields(field.id, 'data_type', value)
+            (value: DataTypeEnum) => changeFields(field.id!, 'data_type', value)
           )}
 
-          {getInput(t('fieldName'), field.name, (value: string) =>
-            changeFields(field.id, 'name', value)
-          )}
+          {generateFieldName(field)}
 
-          {getInput(
-            t('dimension'),
-            field.dimension as number,
-            (value: string) => changeFields(field.id, 'dimension', value),
-            'dimension'
-          )}
+          {generateDimension(field)}
 
-          {getInput(
-            t('description'),
-            field.description,
-            (value: string) => changeFields(field.id, 'description', value),
-            classes.descInput
-          )}
+          {generateDesc(field)}
         </div>
 
         <CustomButton onClick={handleAddNewField} className={classes.mb2}>
           <AddIcon />
-          <span className={classes.btnTxt}>{t('newBtn')}</span>
+          <span className={classes.btnTxt}>{collectionTrans('newBtn')}</span>
         </CustomButton>
       </>
     );
@@ -242,22 +343,15 @@ const CreateFields: FC<CreateFieldsProps> = ({
         >
           <RemoveIcon />
         </IconButton>
-        {getInput(t('fieldName'), field.name, (value: string) =>
-          changeFields(field.id, 'name', value)
-        )}
+        {generateFieldName(field)}
         {getSelector(
           'all',
-          t('fieldType'),
+          collectionTrans('fieldType'),
           field.data_type,
-          (value: DataTypeEnum) => changeFields(field.id, 'data_type', value)
+          (value: DataTypeEnum) => changeFields(field.id!, 'data_type', value)
         )}
 
-        {getInput(
-          t('description'),
-          field.description,
-          (value: string) => changeFields(field.id, 'description', value),
-          classes.descInput
-        )}
+        {generateDesc(field)}
       </div>
     );
   };
@@ -268,28 +362,16 @@ const CreateFields: FC<CreateFieldsProps> = ({
         <IconButton classes={{ root: classes.iconBtn }} aria-label="delete">
           <RemoveIcon />
         </IconButton>
-        {getInput(t('fieldName'), field.name, (value: string) =>
-          changeFields(field.id, 'name', value)
-        )}
+        {generateFieldName(field)}
         {getSelector(
           'all',
-          t('fieldType'),
+          collectionTrans('fieldType'),
           field.data_type,
-          (value: DataTypeEnum) => changeFields(field.id, 'data_type', value)
-        )}
-        {getInput(
-          t('dimension'),
-          field.dimension as number,
-          (value: string) => changeFields(field.id, 'dimension', value),
-          'dimension'
+          (value: DataTypeEnum) => changeFields(field.id!, 'data_type', value)
         )}
+        {generateDimension(field)}
 
-        {getInput(
-          t('description'),
-          field.description,
-          (value: string) => changeFields(field.id, 'description', value),
-          classes.descInput
-        )}
+        {generateDesc(field)}
       </div>
     );
   };

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

@@ -49,13 +49,13 @@ export type DataType =
   | 'FloatVector';
 
 export interface Field {
-  name: string;
+  name: string | null;
   data_type: DataTypeEnum;
   is_primary_key: boolean;
   description: string;
   dimension?: number | string;
   isDefault?: boolean;
-  id: string;
+  id?: string;
   type_params?: { key: string; value: any }[];
 }
 
@@ -68,7 +68,9 @@ export type CreateFieldType =
 export interface CreateFieldsProps {
   fields: Field[];
   setFields: Dispatch<SetStateAction<Field[]>>;
-  setfieldsAllValid: Dispatch<SetStateAction<boolean>>;
+  setFieldsValidation: Dispatch<
+    SetStateAction<{ [x: string]: string | boolean }[]>
+  >;
   autoID: boolean;
   setAutoID: (value: boolean) => void;
 }

+ 2 - 2
client/src/utils/Validation.ts

@@ -35,7 +35,7 @@ export type CheckMap = {
   [key in ValidType]: boolean;
 };
 
-export const checkIsEmpty = (value: string): boolean => {
+export const checkEmptyValid = (value: string): boolean => {
   return value.trim() !== '';
 };
 
@@ -152,7 +152,7 @@ export const getCheckResult = (param: ICheckMapParam): boolean => {
 
   const checkMap = {
     email: checkEmail(value),
-    require: checkIsEmpty(value),
+    require: checkEmptyValid(value),
     confirm: value === extraParam?.compareValue,
     range: checkRange({
       value,