2
0

CreateFields.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516
  1. import { makeStyles, Theme, TextField, IconButton } from '@material-ui/core';
  2. import { FC, Fragment, ReactElement, useMemo } from 'react';
  3. import { useTranslation } from 'react-i18next';
  4. import CustomButton from '../../components/customButton/CustomButton';
  5. import CustomSelector from '../../components/customSelector/CustomSelector';
  6. import icons from '../../components/icons/Icons';
  7. import { generateId } from '../../utils/Common';
  8. import { getCreateFieldType } from '../../utils/Format';
  9. import {
  10. checkEmptyValid,
  11. checkRange,
  12. getCheckResult,
  13. } from '../../utils/Validation';
  14. import {
  15. ALL_OPTIONS,
  16. AUTO_ID_OPTIONS,
  17. PRIMARY_FIELDS_OPTIONS,
  18. VECTOR_FIELDS_OPTIONS,
  19. } from './Constants';
  20. import {
  21. CreateFieldsProps,
  22. CreateFieldType,
  23. DataTypeEnum,
  24. Field,
  25. } from './Types';
  26. const useStyles = makeStyles((theme: Theme) => ({
  27. optionalWrapper: {
  28. width: '100%',
  29. paddingRight: theme.spacing(1),
  30. maxHeight: '240px',
  31. overflowY: 'auto',
  32. },
  33. rowWrapper: {
  34. display: 'flex',
  35. flexWrap: 'nowrap',
  36. alignItems: 'center',
  37. // only Safari 14.1+ support flexbox gap
  38. // gap: '10px',
  39. width: '100%',
  40. '& > *': {
  41. marginLeft: '10px',
  42. },
  43. '& .dimension': {
  44. maxWidth: '160px',
  45. },
  46. },
  47. input: {
  48. fontSize: '14px',
  49. },
  50. primaryInput: {
  51. maxWidth: '160px',
  52. '&:first-child': {
  53. marginLeft: 0,
  54. },
  55. },
  56. select: {
  57. width: '160px',
  58. marginBottom: '22px',
  59. '&:first-child': {
  60. marginLeft: 0,
  61. },
  62. },
  63. descInput: {
  64. minWidth: '100px',
  65. flexGrow: 1,
  66. },
  67. btnTxt: {
  68. textTransform: 'uppercase',
  69. },
  70. iconBtn: {
  71. marginLeft: 0,
  72. padding: 0,
  73. width: '20px',
  74. height: '20px',
  75. },
  76. mb2: {
  77. marginBottom: theme.spacing(2),
  78. },
  79. helperText: {
  80. lineHeight: '20px',
  81. margin: theme.spacing(0.25, 0),
  82. marginLeft: '12px',
  83. },
  84. }));
  85. type inputType = {
  86. label: string;
  87. value: string | number | null;
  88. handleChange?: (value: string) => void;
  89. className?: string;
  90. inputClassName?: string;
  91. isReadOnly?: boolean;
  92. validate?: (value: string | number | null) => string;
  93. type?: 'number' | 'text';
  94. };
  95. const CreateFields: FC<CreateFieldsProps> = ({
  96. fields,
  97. setFields,
  98. setAutoID,
  99. autoID,
  100. setFieldsValidation,
  101. }) => {
  102. const { t: collectionTrans } = useTranslation('collection');
  103. const { t: warningTrans } = useTranslation('warning');
  104. const classes = useStyles();
  105. const AddIcon = icons.add;
  106. const RemoveIcon = icons.remove;
  107. const { requiredFields, optionalFields } = useMemo(
  108. () =>
  109. fields.reduce(
  110. (acc, field) => {
  111. const createType: CreateFieldType = getCreateFieldType(field);
  112. const requiredTypes: CreateFieldType[] = [
  113. 'primaryKey',
  114. 'defaultVector',
  115. ];
  116. const key = requiredTypes.includes(createType)
  117. ? 'requiredFields'
  118. : 'optionalFields';
  119. acc[key].push({
  120. ...field,
  121. createType,
  122. });
  123. return acc;
  124. },
  125. {
  126. requiredFields: [] as Field[],
  127. optionalFields: [] as Field[],
  128. }
  129. ),
  130. [fields]
  131. );
  132. const getSelector = (
  133. type: 'all' | 'vector',
  134. label: string,
  135. value: number,
  136. onChange: (value: DataTypeEnum) => void,
  137. options?: any[]
  138. ) => {
  139. return (
  140. <CustomSelector
  141. wrapperClass={classes.select}
  142. options={
  143. options
  144. ? options
  145. : type === 'all'
  146. ? ALL_OPTIONS
  147. : VECTOR_FIELDS_OPTIONS
  148. }
  149. onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
  150. onChange(e.target.value as DataTypeEnum);
  151. }}
  152. value={value}
  153. variant="filled"
  154. label={label}
  155. />
  156. );
  157. };
  158. const getInput = (data: inputType) => {
  159. const {
  160. label,
  161. value,
  162. handleChange = () => {},
  163. className = '',
  164. inputClassName = '',
  165. isReadOnly = false,
  166. validate = (value: string | number | null) => ' ',
  167. type = 'text',
  168. } = data;
  169. return (
  170. <TextField
  171. label={label}
  172. // value={value}
  173. onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
  174. handleChange(e.target.value as string);
  175. }}
  176. variant="filled"
  177. className={className}
  178. InputProps={{
  179. classes: {
  180. input: inputClassName,
  181. },
  182. }}
  183. disabled={isReadOnly}
  184. error={validate(value) !== ' '}
  185. helperText={validate(value)}
  186. FormHelperTextProps={{
  187. className: classes.helperText,
  188. }}
  189. defaultValue={value}
  190. type={type}
  191. />
  192. );
  193. };
  194. const generateFieldName = (field: Field) => {
  195. return getInput({
  196. label: collectionTrans('fieldName'),
  197. value: field.name,
  198. handleChange: (value: string) => {
  199. const isValid = checkEmptyValid(value);
  200. setFieldsValidation(v =>
  201. v.map(item =>
  202. item.id === field.id! ? { ...item, name: isValid } : item
  203. )
  204. );
  205. changeFields(field.id!, 'name', value);
  206. },
  207. validate: (value: any) => {
  208. if (value === null) return ' ';
  209. const isValid = checkEmptyValid(value);
  210. return isValid
  211. ? ' '
  212. : warningTrans('required', { name: collectionTrans('fieldName') });
  213. },
  214. });
  215. };
  216. const generateDesc = (field: Field) => {
  217. return getInput({
  218. label: collectionTrans('description'),
  219. value: field.description,
  220. handleChange: (value: string) =>
  221. changeFields(field.id!, 'description', value),
  222. className: classes.descInput,
  223. });
  224. };
  225. const generateDimension = (field: Field) => {
  226. const validateDimension = (value: string) => {
  227. const isPositive = getCheckResult({
  228. value,
  229. rule: 'positiveNumber',
  230. });
  231. const isMutiple = getCheckResult({
  232. value,
  233. rule: 'multiple',
  234. extraParam: {
  235. multipleNumber: 8,
  236. },
  237. });
  238. if (field.data_type === DataTypeEnum.BinaryVector) {
  239. return {
  240. isMutiple,
  241. isPositive,
  242. };
  243. }
  244. return {
  245. isPositive,
  246. };
  247. };
  248. return getInput({
  249. label: collectionTrans('dimension'),
  250. value: field.dimension as number,
  251. handleChange: (value: string) => {
  252. const { isPositive, isMutiple } = validateDimension(value);
  253. const isValid =
  254. field.data_type === DataTypeEnum.BinaryVector
  255. ? !!isMutiple && isPositive
  256. : isPositive;
  257. changeFields(field.id!, 'dimension', `${value}`);
  258. setFieldsValidation(v =>
  259. v.map(item =>
  260. item.id === field.id! ? { ...item, dimension: isValid } : item
  261. )
  262. );
  263. },
  264. type: 'number',
  265. validate: (value: any) => {
  266. const { isPositive, isMutiple } = validateDimension(value);
  267. if (isMutiple === false) {
  268. return collectionTrans('dimensionMutipleWarning');
  269. }
  270. return isPositive ? ' ' : collectionTrans('dimensionPositiveWarning');
  271. },
  272. });
  273. };
  274. const generateMaxLength = (field: Field) => {
  275. return getInput({
  276. label: 'Max Length',
  277. value: field.max_length!,
  278. type: 'number',
  279. handleChange: (value: string) =>
  280. changeFields(field.id!, 'max_length', value),
  281. validate: (value: any) => {
  282. if (value === null) return ' ';
  283. const isEmptyValid = checkEmptyValid(value);
  284. const isRangeValid = checkRange({
  285. value,
  286. min: 1,
  287. max: 65535,
  288. type: 'number',
  289. });
  290. return !isEmptyValid
  291. ? warningTrans('required', {
  292. name: collectionTrans('fieldName'),
  293. })
  294. : !isRangeValid
  295. ? warningTrans('range', {
  296. min: 1,
  297. max: 65535,
  298. })
  299. : ' ';
  300. },
  301. });
  302. };
  303. const changeFields = (id: string, key: string, value: any) => {
  304. const newFields = fields.map(f => {
  305. if (f.id !== id) {
  306. return f;
  307. }
  308. return {
  309. ...f,
  310. [key]: value,
  311. };
  312. });
  313. setFields(newFields);
  314. };
  315. const handleAddNewField = () => {
  316. const id = generateId();
  317. const newDefaultItem: Field = {
  318. name: null,
  319. data_type: DataTypeEnum.Int16,
  320. is_primary_key: false,
  321. description: '',
  322. isDefault: false,
  323. dimension: '128',
  324. max_length: null,
  325. id,
  326. };
  327. const newValidation = {
  328. id,
  329. name: false,
  330. dimension: true,
  331. };
  332. setFields([...fields, newDefaultItem]);
  333. setFieldsValidation(v => [...v, newValidation]);
  334. };
  335. const handleRemoveField = (field: Field) => {
  336. const newFields = fields.filter(f => f.id !== field.id);
  337. setFields(newFields);
  338. setFieldsValidation(v => v.filter(item => item.id !== field.id));
  339. };
  340. const generatePrimaryKeyRow = (
  341. field: Field,
  342. autoID: boolean
  343. ): ReactElement => {
  344. const isVarChar = field.data_type === DataTypeEnum.VarChar;
  345. const autoIdOff = isVarChar ? 'false' : autoID ? 'true' : 'false';
  346. return (
  347. <div className={`${classes.rowWrapper}`}>
  348. {getSelector(
  349. 'vector',
  350. `Primary ${collectionTrans('fieldType')} `,
  351. field.data_type,
  352. (value: DataTypeEnum) => {
  353. changeFields(field.id!, 'data_type', value);
  354. if (value === DataTypeEnum.VarChar) {
  355. setAutoID(false);
  356. }
  357. },
  358. PRIMARY_FIELDS_OPTIONS
  359. )}
  360. {generateFieldName(field)}
  361. <CustomSelector
  362. label={collectionTrans('autoId')}
  363. options={AUTO_ID_OPTIONS}
  364. value={autoIdOff}
  365. onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
  366. const autoId = e.target.value === 'true';
  367. setAutoID(autoId);
  368. }}
  369. variant="filled"
  370. wrapperClass={classes.select}
  371. disabled={isVarChar}
  372. />
  373. {isVarChar && generateMaxLength(field)}
  374. {generateDesc(field)}
  375. </div>
  376. );
  377. };
  378. const generateDefaultVectorRow = (field: Field): ReactElement => {
  379. return (
  380. <>
  381. <div className={`${classes.rowWrapper}`}>
  382. {getSelector(
  383. 'vector',
  384. collectionTrans('fieldType'),
  385. field.data_type,
  386. (value: DataTypeEnum) => changeFields(field.id!, 'data_type', value)
  387. )}
  388. {generateFieldName(field)}
  389. {generateDimension(field)}
  390. {generateDesc(field)}
  391. </div>
  392. <CustomButton onClick={handleAddNewField} className={classes.mb2}>
  393. <AddIcon />
  394. <span className={classes.btnTxt}>{collectionTrans('newBtn')}</span>
  395. </CustomButton>
  396. </>
  397. );
  398. };
  399. const generateNumberRow = (field: Field): ReactElement => {
  400. const isVarChar = field.data_type === DataTypeEnum.VarChar;
  401. return (
  402. <div className={`${classes.rowWrapper}`}>
  403. <IconButton
  404. onClick={() => handleRemoveField(field)}
  405. classes={{ root: classes.iconBtn }}
  406. aria-label="delete"
  407. >
  408. <RemoveIcon />
  409. </IconButton>
  410. {generateFieldName(field)}
  411. {getSelector(
  412. 'all',
  413. collectionTrans('fieldType'),
  414. field.data_type,
  415. (value: DataTypeEnum) => changeFields(field.id!, 'data_type', value)
  416. )}
  417. {isVarChar && generateMaxLength(field)}
  418. {generateDesc(field)}
  419. </div>
  420. );
  421. };
  422. const generateVectorRow = (field: Field) => {
  423. return (
  424. <div className={`${classes.rowWrapper}`}>
  425. <IconButton classes={{ root: classes.iconBtn }} aria-label="delete">
  426. <RemoveIcon />
  427. </IconButton>
  428. {generateFieldName(field)}
  429. {getSelector(
  430. 'all',
  431. collectionTrans('fieldType'),
  432. field.data_type,
  433. (value: DataTypeEnum) => changeFields(field.id!, 'data_type', value)
  434. )}
  435. {generateDimension(field)}
  436. {generateDesc(field)}
  437. </div>
  438. );
  439. };
  440. const generateRequiredFieldRow = (field: Field, autoID: boolean) => {
  441. // required type is primaryKey or defaultVector
  442. if (field.createType === 'primaryKey') {
  443. return generatePrimaryKeyRow(field, autoID);
  444. }
  445. // use defaultVector as default return type
  446. return generateDefaultVectorRow(field);
  447. };
  448. const generateOptionalFieldRow = (field: Field) => {
  449. // optional type is vector or number
  450. if (field.createType === 'vector') {
  451. return generateVectorRow(field);
  452. }
  453. // use number as default createType
  454. return generateNumberRow(field);
  455. };
  456. return (
  457. <>
  458. {requiredFields.map((field, index) => (
  459. <Fragment key={index}>
  460. {generateRequiredFieldRow(field, autoID)}
  461. </Fragment>
  462. ))}
  463. <div className={classes.optionalWrapper}>
  464. {optionalFields.map((field, index) => (
  465. <Fragment key={index}>{generateOptionalFieldRow(field)}</Fragment>
  466. ))}
  467. </div>
  468. </>
  469. );
  470. };
  471. export default CreateFields;