CreateCollectionDialog.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  1. import { Box, Typography } from '@mui/material';
  2. import {
  3. FC,
  4. useContext,
  5. useMemo,
  6. useState,
  7. ChangeEvent,
  8. useEffect,
  9. useRef,
  10. } from 'react';
  11. import { useTranslation } from 'react-i18next';
  12. import DialogTemplate from '@/components/customDialog/DialogTemplate';
  13. import CustomInput from '@/components/customInput/CustomInput';
  14. import { ITextfieldConfig } from '@/components/customInput/Types';
  15. import { rootContext, dataContext } from '@/context';
  16. import { useFormValidation } from '@/hooks';
  17. import {
  18. formatForm,
  19. getAnalyzerParams,
  20. TypeEnum,
  21. parseCollectionJson,
  22. } from '@/utils';
  23. import {
  24. DataTypeEnum,
  25. ConsistencyLevelEnum,
  26. DEFAULT_ATTU_DIM,
  27. FunctionType,
  28. } from '@/consts';
  29. import CreateFields from './create/CreateFields';
  30. import ExtraInfoSection from './create/ExtraInfoSection';
  31. import BM25FunctionSection from './create/BM25FunctionSection';
  32. import type {
  33. CollectionCreateParam,
  34. CollectionCreateProps,
  35. CreateField,
  36. } from '../databases/collections/Types';
  37. import { CollectionService } from '@/http';
  38. // Add this type at the top of your file or in a relevant types file
  39. interface BM25Function {
  40. name: string;
  41. description: string;
  42. type: FunctionType;
  43. input_field_names: string[];
  44. output_field_names: string[];
  45. params: Record<string, any>;
  46. }
  47. const CreateCollectionDialog: FC<CollectionCreateProps> = ({ onCreate }) => {
  48. const { fetchCollection } = useContext(dataContext);
  49. const { handleCloseDialog, openSnackBar } = useContext(rootContext);
  50. const { t: collectionTrans } = useTranslation('collection');
  51. const { t: btnTrans } = useTranslation('btn');
  52. const { t: successTrans } = useTranslation('success');
  53. const { t: warningTrans } = useTranslation('warning');
  54. const [form, setForm] = useState<{
  55. collection_name: string;
  56. description: string;
  57. enableDynamicField: boolean;
  58. loadAfterCreate: boolean;
  59. functions: BM25Function[];
  60. }>({
  61. collection_name: '',
  62. description: '',
  63. enableDynamicField: false,
  64. loadAfterCreate: true,
  65. functions: [],
  66. });
  67. const [fieldsValidation, setFieldsValidation] = useState(true);
  68. // State for BM25 selection UI
  69. const [showBm25Selection, setShowBm25Selection] = useState<boolean>(false);
  70. const [selectedBm25Input, setSelectedBm25Input] = useState<string>('');
  71. const [selectedBm25Output, setSelectedBm25Output] = useState<string>('');
  72. const [consistencyLevel, setConsistencyLevel] =
  73. useState<ConsistencyLevelEnum>(ConsistencyLevelEnum.Bounded); // Bounded is the default value of consistency level
  74. const [properties, setProperties] = useState({});
  75. const [fields, setFields] = useState<CreateField[]>([
  76. {
  77. data_type: DataTypeEnum.Int64,
  78. is_primary_key: true,
  79. name: 'id', // we need hide helpertext at first time, so we use null to detect user enter input or not.
  80. description: '',
  81. isDefault: true,
  82. id: '1',
  83. },
  84. {
  85. data_type: DataTypeEnum.FloatVector,
  86. is_primary_key: false,
  87. name: 'vector',
  88. dim: DEFAULT_ATTU_DIM,
  89. description: '',
  90. isDefault: true,
  91. id: '2',
  92. },
  93. ]);
  94. const checkedForm = useMemo(() => {
  95. const { collection_name } = form;
  96. return formatForm({ collection_name });
  97. }, [form]);
  98. const { validation, checkIsValid, disabled, setDisabled } =
  99. useFormValidation(checkedForm);
  100. const updateCheckBox = (
  101. event: ChangeEvent<any>,
  102. key: string,
  103. value: boolean
  104. ) => {
  105. setForm({
  106. ...form,
  107. [key]: value,
  108. });
  109. };
  110. const handleInputChange = (key: string, value: string) => {
  111. setForm(v => ({ ...v, [key]: value }));
  112. };
  113. const generalInfoConfigs: ITextfieldConfig[] = [
  114. {
  115. label: collectionTrans('name'),
  116. key: 'collection_name',
  117. value: form.collection_name,
  118. onChange: (value: string) => handleInputChange('collection_name', value),
  119. variant: 'filled',
  120. validations: [
  121. // cannot be empty
  122. {
  123. rule: 'require',
  124. errorText: warningTrans('requiredOnly'),
  125. },
  126. // length <= 255
  127. {
  128. rule: 'range',
  129. extraParam: {
  130. max: 255,
  131. type: 'string',
  132. },
  133. errorText: collectionTrans('nameLengthWarning'),
  134. },
  135. // name can only be combined with letters, number or underscores
  136. {
  137. rule: 'collectionName',
  138. errorText: collectionTrans('nameContentWarning'),
  139. },
  140. // name can not start with number
  141. {
  142. rule: 'firstCharacter',
  143. extraParam: {
  144. invalidTypes: [TypeEnum.number],
  145. },
  146. errorText: collectionTrans('nameFirstLetterWarning'),
  147. },
  148. ],
  149. InputLabelProps: {
  150. shrink: true,
  151. },
  152. size: 'small',
  153. sx: {
  154. width: '100%',
  155. },
  156. },
  157. {
  158. label: collectionTrans('description'),
  159. key: 'description',
  160. value: form.description,
  161. onChange: (value: string) => handleInputChange('description', value),
  162. variant: 'filled',
  163. validations: [],
  164. size: 'small',
  165. InputLabelProps: {
  166. shrink: true,
  167. },
  168. sx: {
  169. width: '100%',
  170. },
  171. },
  172. ];
  173. const handleCreateCollection = async () => {
  174. const param: CollectionCreateParam = {
  175. ...form,
  176. fields: fields.map(v => {
  177. let data: CreateField = {
  178. ...v,
  179. name: v.name,
  180. description: v.description,
  181. is_primary_key: !!v.is_primary_key,
  182. is_partition_key: !!v.is_partition_key,
  183. data_type: v.data_type,
  184. };
  185. // remove unused id
  186. delete data.id;
  187. // if we need
  188. if (typeof v.dim !== undefined && !isNaN(Number(v.dim))) {
  189. data.dim = Number(v.dim);
  190. }
  191. if (typeof v.max_length === 'number') {
  192. data.max_length = Number(v.max_length);
  193. }
  194. if (typeof v.element_type !== 'undefined') {
  195. data.element_type = Number(v.element_type);
  196. }
  197. if (typeof v.max_capacity !== 'undefined') {
  198. data.max_capacity = Number(v.max_capacity);
  199. }
  200. if (data.analyzer_params) {
  201. // if analyzer_params is string, we need to use default value
  202. data.analyzer_params = getAnalyzerParams(data.analyzer_params);
  203. }
  204. // delete sparse vector dime
  205. if (data.data_type === DataTypeEnum.SparseFloatVector) {
  206. delete data.dim;
  207. }
  208. // delete analyzer if not varchar
  209. if (
  210. data.data_type !== DataTypeEnum.VarChar &&
  211. data.data_type === DataTypeEnum.Array &&
  212. data.element_type !== DataTypeEnum.VarChar
  213. ) {
  214. delete data.enable_analyzer;
  215. delete data.analyzer_params;
  216. delete data.max_length;
  217. }
  218. return data;
  219. }),
  220. functions: form.functions || [],
  221. consistency_level: consistencyLevel,
  222. properties: {
  223. ...properties,
  224. },
  225. };
  226. // create collection
  227. await CollectionService.createCollection({
  228. ...param,
  229. });
  230. // refresh collection
  231. await fetchCollection(param.collection_name);
  232. // show success message
  233. openSnackBar(
  234. successTrans('create', {
  235. name: collectionTrans('collection'),
  236. })
  237. );
  238. onCreate && onCreate(param.collection_name);
  239. handleCloseDialog();
  240. };
  241. // Filter available fields for BM25 selectors
  242. const varcharFields = useMemo(
  243. () => fields.filter(f => f.data_type === DataTypeEnum.VarChar && f.name),
  244. [fields]
  245. );
  246. const sparseFields = useMemo(
  247. () =>
  248. fields.filter(
  249. f => f.data_type === DataTypeEnum.SparseFloatVector && f.name
  250. ),
  251. [fields]
  252. );
  253. const handleAddBm25Click = () => {
  254. setShowBm25Selection(true);
  255. };
  256. const handleConfirmAddBm25 = () => {
  257. if (!selectedBm25Input || !selectedBm25Output) {
  258. openSnackBar(collectionTrans('bm25SelectFieldsWarning'), 'warning');
  259. return;
  260. }
  261. const inputField = fields.find(f => f.name === selectedBm25Input);
  262. const outputField = fields.find(f => f.name === selectedBm25Output);
  263. if (!inputField || !outputField) {
  264. // Should not happen if state is managed correctly, but good to check
  265. console.error('Selected BM25 fields not found');
  266. return;
  267. }
  268. // Generate a unique name for the function
  269. const functionName = `BM25_${inputField.name}_${
  270. outputField.name
  271. }_${Math.floor(Math.random() * 1000)}`;
  272. // Create a new function with the selected fields
  273. const newFunction: BM25Function = {
  274. name: functionName,
  275. description: `BM25 function: ${inputField.name} → ${outputField.name}`,
  276. type: FunctionType.BM25,
  277. input_field_names: [inputField.name],
  278. output_field_names: [outputField.name],
  279. params: {}, // Add default params if needed, e.g., { k1: 1.2, b: 0.75 }
  280. };
  281. // Update form with the new function
  282. setForm(prev => ({
  283. ...prev,
  284. functions: [...(prev.functions || []), newFunction],
  285. }));
  286. // Hide selection UI
  287. setShowBm25Selection(false);
  288. // need to update field.is_function_output to true
  289. const updatedFields = fields.map(field => {
  290. if (field.name === outputField.name) {
  291. return {
  292. ...field,
  293. is_function_output: true,
  294. };
  295. }
  296. return field;
  297. });
  298. setFields(updatedFields);
  299. };
  300. const handleCancelAddBm25 = () => {
  301. setShowBm25Selection(false);
  302. };
  303. // Effect to reset selection when fields change or selection UI is hidden
  304. useEffect(() => {
  305. if (!showBm25Selection) {
  306. setSelectedBm25Input('');
  307. setSelectedBm25Output('');
  308. } else {
  309. // Pre-select first available fields when opening selection
  310. setSelectedBm25Input(varcharFields[0]?.name || '');
  311. setSelectedBm25Output(sparseFields[0]?.name || '');
  312. }
  313. }, [showBm25Selection, varcharFields, sparseFields]);
  314. // Effect to filter out functions with invalid field names
  315. useEffect(() => {
  316. setForm(prevForm => {
  317. const fieldNames = fields.map(f => f.name);
  318. const filteredFunctions = (prevForm.functions || []).filter(
  319. fn =>
  320. fn.input_field_names.every(name => fieldNames.includes(name)) &&
  321. fn.output_field_names.every(name => fieldNames.includes(name))
  322. );
  323. if (filteredFunctions.length !== (prevForm.functions || []).length) {
  324. return { ...prevForm, functions: filteredFunctions };
  325. }
  326. return prevForm;
  327. });
  328. }, [fields]);
  329. // Import from json
  330. const fileInputRef = useRef<HTMLInputElement>(null);
  331. const handleImportClick = () => {
  332. fileInputRef.current?.click();
  333. };
  334. const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  335. const file = e.target.files?.[0];
  336. if (!file) return;
  337. const reader = new FileReader();
  338. reader.onload = evt => {
  339. try {
  340. const json = JSON.parse(evt.target?.result as string);
  341. if (
  342. !json.collection_name ||
  343. !Array.isArray(json.schema?.fields) ||
  344. json.schema.fields.length === 0
  345. ) {
  346. openSnackBar('Invalid JSON file', 'error');
  347. return;
  348. }
  349. const {
  350. form: importedForm,
  351. fields: importedFields,
  352. consistencyLevel: importedConsistencyLevel,
  353. properties: importedProperties,
  354. } = parseCollectionJson(json);
  355. setFields(importedFields);
  356. setConsistencyLevel(importedConsistencyLevel);
  357. setForm(importedForm);
  358. setProperties(importedProperties);
  359. // enable submit
  360. setDisabled(false);
  361. openSnackBar('Import successful', 'success');
  362. } catch (err) {
  363. openSnackBar('Invalid JSON file', 'error');
  364. }
  365. };
  366. reader.readAsText(file);
  367. };
  368. return (
  369. <DialogTemplate
  370. dialogClass="create-collection-dialog"
  371. title={collectionTrans('createTitle', { name: form.collection_name })}
  372. handleClose={() => {
  373. handleCloseDialog();
  374. }}
  375. leftActions={
  376. <>
  377. <button
  378. type="button"
  379. onClick={handleImportClick}
  380. style={{
  381. cursor: 'pointer',
  382. background: 'none',
  383. border: 'none',
  384. color: '#1976d2',
  385. }}
  386. >
  387. {btnTrans('importFromJSON')}
  388. </button>
  389. <input
  390. ref={fileInputRef}
  391. type="file"
  392. accept="application/json"
  393. style={{ display: 'none' }}
  394. onChange={handleFileChange}
  395. />
  396. </>
  397. }
  398. confirmLabel={btnTrans('create')}
  399. handleConfirm={handleCreateCollection}
  400. confirmDisabled={disabled || !fieldsValidation}
  401. sx={{ width: 980 }}
  402. >
  403. <Box sx={{ display: 'flex', gap: 2, flexDirection: 'column' }}>
  404. <Box sx={{ display: 'flex', gap: 2, width: '100%' }}>
  405. {generalInfoConfigs.map(config => (
  406. <CustomInput
  407. key={config.key}
  408. type="text"
  409. textConfig={{ ...config }}
  410. checkValid={checkIsValid}
  411. validInfo={validation}
  412. />
  413. ))}
  414. </Box>
  415. <Box
  416. sx={{
  417. background: theme => theme.palette.background.lightGrey,
  418. padding: '16px',
  419. borderRadius: 0.5,
  420. }}
  421. >
  422. <CreateFields
  423. fields={fields}
  424. setFields={setFields}
  425. onValidationChange={setFieldsValidation}
  426. />
  427. </Box>
  428. <Box sx={{ mt: 2, display: 'flex', flexDirection: 'column', gap: 1 }}>
  429. <Typography variant="h4" sx={{ fontSize: 16 }}>
  430. {collectionTrans('functions')}
  431. </Typography>
  432. <BM25FunctionSection
  433. showBm25Selection={showBm25Selection}
  434. varcharFields={varcharFields}
  435. sparseFields={sparseFields}
  436. selectedBm25Input={selectedBm25Input}
  437. selectedBm25Output={selectedBm25Output}
  438. setSelectedBm25Input={setSelectedBm25Input}
  439. setSelectedBm25Output={setSelectedBm25Output}
  440. handleAddBm25Click={handleAddBm25Click}
  441. handleConfirmAddBm25={handleConfirmAddBm25}
  442. handleCancelAddBm25={handleCancelAddBm25}
  443. formFunctions={form.functions}
  444. setForm={setForm}
  445. collectionTrans={collectionTrans}
  446. btnTrans={btnTrans}
  447. />
  448. </Box>
  449. <ExtraInfoSection
  450. consistencyLevel={consistencyLevel}
  451. setConsistencyLevel={setConsistencyLevel}
  452. form={form}
  453. updateCheckBox={updateCheckBox}
  454. collectionTrans={collectionTrans}
  455. />
  456. </Box>
  457. </DialogTemplate>
  458. );
  459. };
  460. export default CreateCollectionDialog;