CreateFields.tsx 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750
  1. import {
  2. Theme,
  3. TextField,
  4. IconButton,
  5. Switch,
  6. FormControlLabel,
  7. } from '@mui/material';
  8. import { FC, Fragment, ReactElement, useMemo } from 'react';
  9. import { useTranslation } from 'react-i18next';
  10. import CustomSelector from '@/components/customSelector/CustomSelector';
  11. import icons from '@/components/icons/Icons';
  12. import CustomToolTip from '@/components/customToolTip/CustomToolTip';
  13. import {
  14. generateId,
  15. getCreateFieldType,
  16. checkEmptyValid,
  17. checkRange,
  18. getCheckResult,
  19. } from '@/utils';
  20. import {
  21. ALL_OPTIONS,
  22. PRIMARY_FIELDS_OPTIONS,
  23. VECTOR_FIELDS_OPTIONS,
  24. } from './Constants';
  25. import { CreateFieldsProps, CreateFieldType, FieldType } from './Types';
  26. import { DataTypeEnum, VectorTypes } from '@/consts';
  27. import {
  28. DEFAULT_ATTU_DIM,
  29. DEFAULT_ATTU_MAX_CAPACITY,
  30. DEFAULT_ATTU_VARCHAR_MAX_LENGTH,
  31. DEFAULT_ATTU_ELEMENT_TYPE,
  32. } from '@/consts';
  33. import { makeStyles } from '@mui/styles';
  34. const useStyles = makeStyles((theme: Theme) => ({
  35. optionalWrapper: {
  36. width: '100%',
  37. paddingRight: theme.spacing(1),
  38. overflowY: 'auto',
  39. },
  40. rowWrapper: {
  41. display: 'flex',
  42. flexWrap: 'nowrap',
  43. alignItems: 'center',
  44. gap: '8px',
  45. flex: '1 0 auto',
  46. },
  47. input: {
  48. fontSize: '14px',
  49. },
  50. fieldInput: {
  51. width: '170px',
  52. },
  53. select: {
  54. width: '180px',
  55. marginTop: '-20px',
  56. '&:first-child': {
  57. marginLeft: 0,
  58. },
  59. },
  60. autoIdSelect: {
  61. width: '120px',
  62. marginTop: '-20px',
  63. },
  64. numberBox: {
  65. width: '97px',
  66. },
  67. maxLength: {
  68. maxWidth: '80px',
  69. },
  70. descInput: {
  71. width: '120px',
  72. },
  73. btnTxt: {
  74. textTransform: 'uppercase',
  75. },
  76. iconBtn: {
  77. marginLeft: 0,
  78. padding: 0,
  79. width: '16px',
  80. height: '16px',
  81. position: 'relative',
  82. top: '-8px',
  83. },
  84. helperText: {
  85. lineHeight: '20px',
  86. fontSize: '10px',
  87. margin: theme.spacing(0),
  88. marginLeft: '11px',
  89. },
  90. toggle: {
  91. marginBottom: theme.spacing(2),
  92. marginLeft: theme.spacing(0.5),
  93. marginRight: theme.spacing(0.5),
  94. },
  95. icon: {
  96. fontSize: '14px',
  97. marginLeft: theme.spacing(0.5),
  98. },
  99. }));
  100. type inputType = {
  101. label: string;
  102. value: string | number | null;
  103. handleChange?: (value: string) => void;
  104. className?: string;
  105. inputClassName?: string;
  106. isReadOnly?: boolean;
  107. validate?: (value: string | number | null) => string;
  108. type?: 'number' | 'text';
  109. };
  110. const CreateFields: FC<CreateFieldsProps> = ({
  111. fields,
  112. setFields,
  113. setAutoID,
  114. autoID,
  115. setFieldsValidation,
  116. }) => {
  117. const { t: collectionTrans } = useTranslation('collection');
  118. const { t: warningTrans } = useTranslation('warning');
  119. const classes = useStyles();
  120. const AddIcon = icons.addOutline;
  121. const RemoveIcon = icons.remove;
  122. const { requiredFields, optionalFields } = useMemo(
  123. () =>
  124. fields.reduce(
  125. (acc, field) => {
  126. const createType: CreateFieldType = getCreateFieldType(field);
  127. const requiredTypes: CreateFieldType[] = [
  128. 'primaryKey',
  129. 'defaultVector',
  130. ];
  131. const key = requiredTypes.includes(createType)
  132. ? 'requiredFields'
  133. : 'optionalFields';
  134. acc[key].push({
  135. ...field,
  136. createType,
  137. });
  138. return acc;
  139. },
  140. {
  141. requiredFields: [] as FieldType[],
  142. optionalFields: [] as FieldType[],
  143. }
  144. ),
  145. [fields]
  146. );
  147. const getSelector = (
  148. type: 'all' | 'vector' | 'element' | 'primaryKey',
  149. label: string,
  150. value: number,
  151. onChange: (value: DataTypeEnum) => void
  152. ) => {
  153. let _options = ALL_OPTIONS;
  154. switch (type) {
  155. case 'primaryKey':
  156. _options = PRIMARY_FIELDS_OPTIONS;
  157. break;
  158. case 'all':
  159. _options = ALL_OPTIONS;
  160. break;
  161. case 'vector':
  162. _options = VECTOR_FIELDS_OPTIONS;
  163. break;
  164. case 'element':
  165. _options = ALL_OPTIONS.filter(
  166. d =>
  167. d.label !== 'Array' &&
  168. d.label !== 'JSON' &&
  169. !d.label.includes('Vector')
  170. );
  171. break;
  172. default:
  173. break;
  174. }
  175. return (
  176. <CustomSelector
  177. wrapperClass={classes.select}
  178. options={_options}
  179. size="small"
  180. onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
  181. onChange(e.target.value as DataTypeEnum);
  182. }}
  183. value={value}
  184. variant="filled"
  185. label={label}
  186. />
  187. );
  188. };
  189. const getInput = (data: inputType) => {
  190. const {
  191. label,
  192. value,
  193. handleChange = () => {},
  194. className = '',
  195. inputClassName = '',
  196. isReadOnly = false,
  197. validate = (value: string | number | null) => ' ',
  198. type = 'text',
  199. } = data;
  200. return (
  201. <TextField
  202. label={label}
  203. // value={value}
  204. onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
  205. handleChange(e.target.value as string);
  206. }}
  207. variant="filled"
  208. className={className}
  209. InputProps={{
  210. classes: {
  211. input: inputClassName,
  212. },
  213. }}
  214. InputLabelProps={{
  215. shrink: true,
  216. }}
  217. size="small"
  218. disabled={isReadOnly}
  219. error={validate(value) !== ' '}
  220. helperText={validate(value)}
  221. FormHelperTextProps={{
  222. className: classes.helperText,
  223. }}
  224. defaultValue={value}
  225. type={type}
  226. />
  227. );
  228. };
  229. const generateFieldName = (
  230. field: FieldType,
  231. label?: string,
  232. className?: string
  233. ) => {
  234. const defaultLabal = collectionTrans(
  235. VectorTypes.includes(field.data_type) ? 'vectorFieldName' : 'fieldName'
  236. );
  237. return getInput({
  238. label: label || defaultLabal,
  239. value: field.name,
  240. className: className || classes.fieldInput,
  241. handleChange: (value: string) => {
  242. const isValid = checkEmptyValid(value);
  243. setFieldsValidation(v =>
  244. v.map(item =>
  245. item.id === field.id! ? { ...item, name: isValid } : item
  246. )
  247. );
  248. changeFields(field.id!, 'name', value);
  249. },
  250. validate: (value: any) => {
  251. if (value === null) return ' ';
  252. const isValid = checkEmptyValid(value);
  253. return isValid ? ' ' : warningTrans('requiredOnly');
  254. },
  255. });
  256. };
  257. const generateDesc = (field: FieldType) => {
  258. return getInput({
  259. label: collectionTrans('description'),
  260. value: field.description,
  261. handleChange: (value: string) =>
  262. changeFields(field.id!, 'description', value),
  263. inputClassName: classes.descInput,
  264. });
  265. };
  266. const generateDimension = (field: FieldType) => {
  267. // sparse dont support dimension
  268. if (field.data_type === DataTypeEnum.SparseFloatVector) {
  269. return null;
  270. }
  271. const validateDimension = (value: string) => {
  272. const isPositive = getCheckResult({
  273. value,
  274. rule: 'positiveNumber',
  275. });
  276. const isMultiple = getCheckResult({
  277. value,
  278. rule: 'multiple',
  279. extraParam: {
  280. multipleNumber: 8,
  281. },
  282. });
  283. if (field.data_type === DataTypeEnum.BinaryVector) {
  284. return {
  285. isMultiple,
  286. isPositive,
  287. };
  288. }
  289. return {
  290. isPositive,
  291. };
  292. };
  293. return getInput({
  294. label: collectionTrans('dimension'),
  295. value: field.dimension as number,
  296. inputClassName: classes.numberBox,
  297. handleChange: (value: string) => {
  298. const { isPositive, isMultiple } = validateDimension(value);
  299. const isValid =
  300. field.data_type === DataTypeEnum.BinaryVector
  301. ? !!isMultiple && isPositive
  302. : isPositive;
  303. changeFields(field.id!, 'dimension', `${value}`);
  304. setFieldsValidation(v =>
  305. v.map(item =>
  306. item.id === field.id! ? { ...item, dimension: isValid } : item
  307. )
  308. );
  309. },
  310. type: 'number',
  311. validate: (value: any) => {
  312. const { isPositive, isMultiple } = validateDimension(value);
  313. if (isMultiple === false) {
  314. return collectionTrans('dimensionMultipleWarning');
  315. }
  316. return isPositive ? ' ' : collectionTrans('dimensionPositiveWarning');
  317. },
  318. });
  319. };
  320. const generateMaxLength = (field: FieldType) => {
  321. // update data if needed
  322. if (typeof field.max_length === 'undefined') {
  323. changeFields(field.id!, 'max_length', DEFAULT_ATTU_VARCHAR_MAX_LENGTH);
  324. }
  325. return getInput({
  326. label: 'Max Length',
  327. value: field.max_length! || DEFAULT_ATTU_VARCHAR_MAX_LENGTH,
  328. type: 'number',
  329. inputClassName: classes.maxLength,
  330. handleChange: (value: string) =>
  331. changeFields(field.id!, 'max_length', value),
  332. validate: (value: any) => {
  333. if (value === null) return ' ';
  334. const isEmptyValid = checkEmptyValid(value);
  335. const isRangeValid = checkRange({
  336. value,
  337. min: 1,
  338. max: 65535,
  339. type: 'number',
  340. });
  341. return !isEmptyValid
  342. ? warningTrans('requiredOnly')
  343. : !isRangeValid
  344. ? warningTrans('range', {
  345. min: 1,
  346. max: 65535,
  347. })
  348. : ' ';
  349. },
  350. });
  351. };
  352. const generateMaxCapacity = (field: FieldType) => {
  353. return getInput({
  354. label: 'Max Capacity',
  355. value: field.max_capacity || DEFAULT_ATTU_MAX_CAPACITY,
  356. type: 'number',
  357. inputClassName: classes.maxLength,
  358. handleChange: (value: string) =>
  359. changeFields(field.id!, 'max_capacity', value),
  360. validate: (value: any) => {
  361. if (value === null) return ' ';
  362. const isEmptyValid = checkEmptyValid(value);
  363. const isRangeValid = checkRange({
  364. value,
  365. min: 1,
  366. max: 4096,
  367. type: 'number',
  368. });
  369. return !isEmptyValid
  370. ? warningTrans('requiredOnly')
  371. : !isRangeValid
  372. ? warningTrans('range', {
  373. min: 1,
  374. max: 4096,
  375. })
  376. : ' ';
  377. },
  378. });
  379. };
  380. const generatePartitionKeyToggle = (
  381. field: FieldType,
  382. fields: FieldType[]
  383. ) => {
  384. return (
  385. <FormControlLabel
  386. control={
  387. <Switch
  388. checked={!!field.is_partition_key}
  389. disabled={
  390. fields.some(f => f.is_partition_key) && !field.is_partition_key
  391. }
  392. size="small"
  393. onChange={() => {
  394. changeFields(
  395. field.id!,
  396. 'is_partition_key',
  397. !field.is_partition_key
  398. );
  399. }}
  400. />
  401. }
  402. label={
  403. <CustomToolTip
  404. title={collectionTrans('partitionKeyTooltip')}
  405. placement="top"
  406. >
  407. <>
  408. {collectionTrans('partitionKey')}
  409. {/* <InfoIcon classes={{ root: classes.icon }} /> */}
  410. </>
  411. </CustomToolTip>
  412. }
  413. className={classes.toggle}
  414. />
  415. );
  416. };
  417. const changeFields = (id: string, key: keyof FieldType, value: any) => {
  418. const newFields = fields.map(f => {
  419. if (f.id !== id) {
  420. return f;
  421. }
  422. const updatedField = {
  423. ...f,
  424. [key]: value,
  425. };
  426. // remove array params, if not array
  427. if (updatedField.data_type !== DataTypeEnum.Array) {
  428. delete updatedField.max_capacity;
  429. delete updatedField.element_type;
  430. }
  431. // remove varchar params, if not varchar
  432. if (
  433. updatedField.data_type !== DataTypeEnum.VarChar &&
  434. updatedField.element_type !== DataTypeEnum.VarChar
  435. ) {
  436. delete updatedField.max_length;
  437. }
  438. // remove dimension, if not vector
  439. if (
  440. !VectorTypes.includes(updatedField.data_type) ||
  441. updatedField.data_type === DataTypeEnum.SparseFloatVector
  442. ) {
  443. delete updatedField.dimension;
  444. } else {
  445. // add dimension if not exist
  446. updatedField.dimension = Number(
  447. updatedField.dimension || DEFAULT_ATTU_DIM
  448. );
  449. }
  450. return updatedField;
  451. });
  452. setFields(newFields);
  453. };
  454. const handleAddNewField = (index: number) => {
  455. const id = generateId();
  456. const newDefaultItem: FieldType = {
  457. name: '',
  458. data_type: DataTypeEnum.Int16,
  459. is_primary_key: false,
  460. description: '',
  461. isDefault: false,
  462. dimension: DEFAULT_ATTU_DIM,
  463. id,
  464. };
  465. const newValidation = {
  466. id,
  467. name: false,
  468. dimension: true,
  469. };
  470. fields.splice(index + 1, 0, newDefaultItem);
  471. setFields([...fields]);
  472. setFieldsValidation(v => [...v, newValidation]);
  473. };
  474. const handleRemoveField = (id: string) => {
  475. const newFields = fields.filter(f => f.id !== id);
  476. setFields(newFields);
  477. setFieldsValidation(v => v.filter(item => item.id !== id));
  478. };
  479. const generatePrimaryKeyRow = (
  480. field: FieldType,
  481. autoID: boolean
  482. ): ReactElement => {
  483. const isVarChar = field.data_type === DataTypeEnum.VarChar;
  484. return (
  485. <div className={`${classes.rowWrapper}`}>
  486. {generateFieldName(field, collectionTrans('idFieldName'))}
  487. {getSelector(
  488. 'primaryKey',
  489. `${collectionTrans('idType')} `,
  490. field.data_type,
  491. (value: DataTypeEnum) => {
  492. changeFields(field.id!, 'data_type', value);
  493. if (value === DataTypeEnum.VarChar) {
  494. setAutoID(false);
  495. }
  496. }
  497. )}
  498. {generateDesc(field)}
  499. {isVarChar && generateMaxLength(field)}
  500. <FormControlLabel
  501. control={
  502. <Switch
  503. checked={autoID}
  504. disabled={isVarChar}
  505. size="small"
  506. onChange={() => {
  507. changeFields(field.id!, 'autoID', !autoID);
  508. setAutoID(!autoID);
  509. }}
  510. />
  511. }
  512. label={
  513. <CustomToolTip
  514. title={collectionTrans('autoIdToggleTip')}
  515. placement="top"
  516. >
  517. <>{collectionTrans('autoId')}</>
  518. </CustomToolTip>
  519. }
  520. className={classes.toggle}
  521. />
  522. </div>
  523. );
  524. };
  525. const generateDefaultVectorRow = (
  526. field: FieldType,
  527. index: number
  528. ): ReactElement => {
  529. return (
  530. <div className={`${classes.rowWrapper}`}>
  531. {generateFieldName(field)}
  532. {getSelector(
  533. 'vector',
  534. `${collectionTrans('vectorType')} `,
  535. field.data_type,
  536. (value: DataTypeEnum) => changeFields(field.id!, 'data_type', value)
  537. )}
  538. {generateDimension(field)}
  539. {generateDesc(field)}
  540. <IconButton
  541. onClick={() => handleAddNewField(index)}
  542. classes={{ root: classes.iconBtn }}
  543. aria-label="add"
  544. size="large"
  545. >
  546. <AddIcon />
  547. </IconButton>
  548. </div>
  549. );
  550. };
  551. const generateNonRequiredRow = (
  552. field: FieldType,
  553. index: number,
  554. fields: FieldType[]
  555. ): ReactElement => {
  556. const isVarChar = field.data_type === DataTypeEnum.VarChar;
  557. const isInt64 = field.data_type === DataTypeEnum.Int64;
  558. const isArray = field.data_type === DataTypeEnum.Array;
  559. const isElementVarChar = field.element_type === DataTypeEnum.VarChar;
  560. // handle default values
  561. if (isArray && typeof field.element_type === 'undefined') {
  562. changeFields(field.id!, 'element_type', DEFAULT_ATTU_ELEMENT_TYPE);
  563. }
  564. if (isArray && typeof field.max_capacity === 'undefined') {
  565. changeFields(field.id!, 'max_capacity', DEFAULT_ATTU_MAX_CAPACITY);
  566. }
  567. return (
  568. <div className={`${classes.rowWrapper}`}>
  569. {generateFieldName(field)}
  570. {getSelector(
  571. 'all',
  572. collectionTrans('fieldType'),
  573. field.data_type,
  574. (value: DataTypeEnum) => changeFields(field.id!, 'data_type', value)
  575. )}
  576. {isArray
  577. ? getSelector(
  578. 'element',
  579. collectionTrans('elementType'),
  580. field.element_type || DEFAULT_ATTU_ELEMENT_TYPE,
  581. (value: DataTypeEnum) =>
  582. changeFields(field.id!, 'element_type', value)
  583. )
  584. : null}
  585. {isArray ? generateMaxCapacity(field) : null}
  586. {isVarChar || isElementVarChar ? generateMaxLength(field) : null}
  587. {generateDesc(field)}
  588. {isVarChar || isInt64
  589. ? generatePartitionKeyToggle(field, fields)
  590. : null}
  591. <IconButton
  592. onClick={() => {
  593. handleAddNewField(index);
  594. }}
  595. classes={{ root: classes.iconBtn }}
  596. aria-label="add"
  597. size="large"
  598. >
  599. <AddIcon />
  600. </IconButton>
  601. <IconButton
  602. onClick={() => {
  603. const id = field.id || '';
  604. handleRemoveField(id);
  605. }}
  606. classes={{ root: classes.iconBtn }}
  607. aria-label="delete"
  608. size="large"
  609. >
  610. <RemoveIcon />
  611. </IconButton>
  612. </div>
  613. );
  614. };
  615. const generateVectorRow = (field: FieldType, index: number) => {
  616. return (
  617. <div className={`${classes.rowWrapper}`}>
  618. {generateFieldName(field)}
  619. {getSelector(
  620. 'all',
  621. collectionTrans('fieldType'),
  622. field.data_type,
  623. (value: DataTypeEnum) => changeFields(field.id!, 'data_type', value)
  624. )}
  625. {generateDimension(field)}
  626. {generateDesc(field)}
  627. <IconButton
  628. onClick={() => {
  629. handleAddNewField(index);
  630. }}
  631. classes={{ root: classes.iconBtn }}
  632. aria-label="add"
  633. size="large"
  634. >
  635. <AddIcon />
  636. </IconButton>
  637. <IconButton
  638. onClick={() => {
  639. const id = field.id || '';
  640. handleRemoveField(id);
  641. }}
  642. classes={{ root: classes.iconBtn }}
  643. aria-label="delete"
  644. size="large"
  645. >
  646. <RemoveIcon />
  647. </IconButton>
  648. </div>
  649. );
  650. };
  651. const generateRequiredFieldRow = (
  652. field: FieldType,
  653. autoID: boolean,
  654. index: number
  655. ) => {
  656. // required type is primaryKey or defaultVector
  657. if (field.createType === 'primaryKey') {
  658. return generatePrimaryKeyRow(field, autoID);
  659. }
  660. // use defaultVector as default return type
  661. return generateDefaultVectorRow(field, index);
  662. };
  663. const generateOptionalFieldRow = (
  664. field: FieldType,
  665. index: number,
  666. fields: FieldType[]
  667. ) => {
  668. // optional type is vector or number
  669. if (field.createType === 'vector') {
  670. return generateVectorRow(field, index);
  671. }
  672. // use number as default createType
  673. return generateNonRequiredRow(field, index, fields);
  674. };
  675. return (
  676. <>
  677. {requiredFields.map((field, index) => (
  678. <Fragment key={field.id}>
  679. {generateRequiredFieldRow(field, autoID, index)}
  680. </Fragment>
  681. ))}
  682. <div className={classes.optionalWrapper}>
  683. {optionalFields.map((field, index) => (
  684. <Fragment key={field.id}>
  685. {generateOptionalFieldRow(
  686. field,
  687. index + requiredFields.length,
  688. optionalFields
  689. )}
  690. </Fragment>
  691. ))}
  692. </div>
  693. </>
  694. );
  695. };
  696. export default CreateFields;