Dialog.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  1. import { Theme } from '@mui/material';
  2. import {
  3. FC,
  4. ReactElement,
  5. useCallback,
  6. useContext,
  7. useEffect,
  8. useMemo,
  9. useState,
  10. } from 'react';
  11. import { parse } from 'papaparse';
  12. import { useTranslation } from 'react-i18next';
  13. import DialogTemplate from '@/components/customDialog/DialogTemplate';
  14. import icons from '@/components/icons/Icons';
  15. import { PartitionService } from '@/http';
  16. import { rootContext } from '@/context';
  17. import { combineHeadsAndData, convertVectorFields } from '@/utils';
  18. import { FILE_MIME_TYPE } from '@/consts';
  19. import InsertImport from './Import';
  20. import InsertPreview from './Preview';
  21. import InsertStatus from './Status';
  22. import { InsertStatusEnum, InsertStepperEnum } from './consts';
  23. import { DataService } from '@/http';
  24. import type { InsertContentProps } from './Types';
  25. import type { Option } from '@/components/customSelector/Types';
  26. import type { InsertDataParam } from '@/pages/databases/collections/Types';
  27. /**
  28. * this component contains processes during insert
  29. * including import, preview and status
  30. */
  31. const InsertContainer: FC<InsertContentProps> = ({
  32. collections = [],
  33. defaultSelectedCollection,
  34. defaultSelectedPartition,
  35. partitions,
  36. schema,
  37. onInsert,
  38. }) => {
  39. const { t: insertTrans } = useTranslation('insert');
  40. const { t: btnTrans } = useTranslation('btn');
  41. const { handleCloseDialog, openSnackBar } = useContext(rootContext);
  42. const [activeStep, setActiveStep] = useState<InsertStepperEnum>(
  43. InsertStepperEnum.import
  44. );
  45. const [insertStatus, setInsertStatus] = useState<InsertStatusEnum>(
  46. InsertStatusEnum.init
  47. );
  48. const [insertFailMsg, setInsertFailMsg] = useState<string>('');
  49. const [nextDisabled, setNextDisabled] = useState<boolean>(false);
  50. // selected collection name
  51. const [collectionValue, setCollectionValue] = useState<string>(
  52. defaultSelectedCollection
  53. );
  54. // selected partition name
  55. const [partitionValue, setPartitionValue] = useState<string>(
  56. defaultSelectedPartition
  57. );
  58. // use contain field names yes as default
  59. const [isContainFieldNames, setIsContainFieldNames] = useState<number>(1);
  60. // uploaded file name
  61. const [fileName, setFileName] = useState<string>('');
  62. const [file, setFile] = useState<File | null>(null);
  63. // uploaded csv data (type: string)
  64. const [csvData, setCsvData] = useState<any[]>([]);
  65. const [jsonData, setJsonData] = useState<
  66. Record<string, unknown> | undefined
  67. >();
  68. // handle changed table heads
  69. const [tableHeads, setTableHeads] = useState<string[]>([]);
  70. const [partitionOptions, setPartitionOptions] = useState<Option[]>([]);
  71. const previewData = useMemo(() => {
  72. // we only show top 4 results of uploaded csv data
  73. const end = isContainFieldNames ? 5 : 4;
  74. return csvData.slice(0, end);
  75. }, [csvData, isContainFieldNames]);
  76. useEffect(() => {
  77. if (activeStep === InsertStepperEnum.import) {
  78. /**
  79. * 1. must choose collection and partition
  80. * 2. must upload a csv file
  81. */
  82. const selectValid = collectionValue !== '' && partitionValue !== '';
  83. const uploadValid = csvData.length > 0 || typeof jsonData !== 'undefined';
  84. const condition = selectValid && uploadValid;
  85. setNextDisabled(!condition);
  86. }
  87. if (activeStep === InsertStepperEnum.preview) {
  88. /**
  89. * table heads shouldn't be empty
  90. */
  91. const headsValid = tableHeads.every(h => h !== '');
  92. setNextDisabled(!headsValid);
  93. }
  94. }, [
  95. activeStep,
  96. collectionValue,
  97. partitionValue,
  98. csvData,
  99. tableHeads,
  100. jsonData,
  101. ]);
  102. useEffect(() => {
  103. const heads = isContainFieldNames
  104. ? previewData[0]
  105. : new Array(previewData[0].length).fill('');
  106. setTableHeads(heads);
  107. }, [previewData, isContainFieldNames]);
  108. // every time selected collection value change, partition options and default value will change
  109. const fetchPartition = useCallback(async () => {
  110. if (collectionValue) {
  111. const partitions = await PartitionService.getPartitions(collectionValue);
  112. const partitionOptions: Option[] = partitions.map(p => ({
  113. label: p.name === '_default' ? insertTrans('defaultPartition') : p.name,
  114. value: p.name,
  115. }));
  116. setPartitionOptions(partitionOptions);
  117. if (partitionOptions.length > 0) {
  118. // set first partition option value as default value
  119. const [{ value: defaultPartitionValue }] = partitionOptions;
  120. setPartitionValue(defaultPartitionValue as string);
  121. }
  122. }
  123. }, [collectionValue]);
  124. useEffect(() => {
  125. // if not on partitions page, we need to fetch partitions according to selected collection
  126. if (!partitions || partitions.length === 0) {
  127. fetchPartition();
  128. } else {
  129. const options = partitions
  130. .map(p => ({
  131. label: p.name,
  132. value: p.name,
  133. }))
  134. // when there's single selected partition
  135. // insert dialog partitions shouldn't selectable
  136. .filter(
  137. partition =>
  138. partition.label === defaultSelectedPartition ||
  139. defaultSelectedPartition === ''
  140. );
  141. setPartitionOptions(options);
  142. }
  143. }, [partitions, fetchPartition, defaultSelectedPartition]);
  144. const BackIcon = icons.back;
  145. // modal actions part, buttons label text or component
  146. const { confirm, cancel } = useMemo(() => {
  147. const labelMap: {
  148. [key in InsertStepperEnum]: {
  149. confirm: string;
  150. cancel: string | ReactElement;
  151. };
  152. } = {
  153. [InsertStepperEnum.import]: {
  154. confirm: btnTrans('next'),
  155. cancel: btnTrans('cancel'),
  156. },
  157. [InsertStepperEnum.preview]: {
  158. confirm: btnTrans('importFile'),
  159. cancel: (
  160. <>
  161. <BackIcon sx={{ fontSize: '16px' }} />
  162. {btnTrans('previous')}
  163. </>
  164. ),
  165. },
  166. [InsertStepperEnum.status]: {
  167. confirm: btnTrans('done'),
  168. cancel: '',
  169. },
  170. };
  171. return labelMap[activeStep];
  172. }, [activeStep, btnTrans, BackIcon]);
  173. const { showActions, showCancel } = useMemo(() => {
  174. return {
  175. showActions: insertStatus !== InsertStatusEnum.loading,
  176. showCancel: insertStatus === InsertStatusEnum.init,
  177. };
  178. }, [insertStatus]);
  179. // props children component needed:
  180. const collectionOptions: Option[] = useMemo(
  181. () =>
  182. defaultSelectedCollection === ''
  183. ? collections.map(c => ({
  184. label: c.collection_name,
  185. value: c.collection_name,
  186. }))
  187. : [
  188. {
  189. label: defaultSelectedCollection,
  190. value: defaultSelectedCollection,
  191. },
  192. ],
  193. [collections, defaultSelectedCollection]
  194. );
  195. const {
  196. schemaOptions,
  197. autoIdFieldName,
  198. }: { schemaOptions: Option[]; autoIdFieldName: string } = useMemo(() => {
  199. /**
  200. * on collection page, we get schema data from collection
  201. * on partition page, we pass schema as props
  202. */
  203. const list = schema
  204. ? schema.fields
  205. : collections.find(c => c.collection_name === collectionValue)?.schema
  206. ?.fields;
  207. const autoIdFieldName =
  208. list?.find(item => item.is_primary_key && item.autoID)?.name || '';
  209. /**
  210. * if below conditions all met, this schema shouldn't be selectable as head:
  211. * 1. this field is primary key
  212. * 2. this field auto id is true
  213. */
  214. const options = (list || [])
  215. .filter(s => !s.autoID || !s.is_primary_key)
  216. .map(s => ({
  217. label: s.name,
  218. value: s.name,
  219. }));
  220. return {
  221. schemaOptions: options,
  222. autoIdFieldName,
  223. };
  224. }, [schema, collectionValue, collections]);
  225. const checkUploadFileValidation = (firstRowItems: string[]): boolean => {
  226. return checkIsAutoIdFieldValid(firstRowItems);
  227. };
  228. /**
  229. * when primary key field auto id is true
  230. * no need to upload this field data
  231. * @param firstRowItems uploaded file first row items
  232. * @returns whether invalid, true means invalid
  233. */
  234. const checkIsAutoIdFieldValid = (firstRowItems: string[]): boolean => {
  235. const isContainAutoIdField = firstRowItems.includes(autoIdFieldName);
  236. isContainAutoIdField &&
  237. openSnackBar(
  238. insertTrans('uploadAutoIdFieldWarning', { fieldName: autoIdFieldName }),
  239. 'error'
  240. );
  241. return isContainAutoIdField;
  242. };
  243. const handleUploadedData = (
  244. content: string,
  245. uploader: HTMLFormElement,
  246. type: FILE_MIME_TYPE
  247. ) => {
  248. // if json, just parse json to object
  249. if (type === FILE_MIME_TYPE.JSON) {
  250. setJsonData(JSON.parse(content));
  251. setCsvData([]);
  252. return;
  253. }
  254. const { data } = parse(content);
  255. // if uploaded csv contains heads, firstRowItems is the list of all heads
  256. const [firstRowItems = []] = data as string[][];
  257. const invalid = checkUploadFileValidation(firstRowItems);
  258. if (invalid) {
  259. // reset uploader value and filename
  260. setFileName('');
  261. setFile(null);
  262. uploader.value = null;
  263. return;
  264. }
  265. setCsvData(data);
  266. setJsonData(undefined);
  267. };
  268. const handleInsertData = async () => {
  269. const fields = schema
  270. ? schema.fields
  271. : collections.find(c => c.collection_name === collectionValue)?.schema
  272. ?.fields;
  273. // start loading
  274. setInsertStatus(InsertStatusEnum.loading);
  275. // process data
  276. const data =
  277. typeof jsonData !== 'undefined'
  278. ? convertVectorFields(jsonData, fields!)
  279. : combineHeadsAndData(
  280. tableHeads,
  281. isContainFieldNames ? csvData.slice(1) : csvData,
  282. fields!
  283. );
  284. const param: InsertDataParam = {
  285. partition_name: partitionValue,
  286. fields_data: data,
  287. };
  288. try {
  289. const inserted = await DataService.insertData(collectionValue, param);
  290. if (inserted.status.error_code !== 'Success') {
  291. setInsertFailMsg(inserted.status.reason);
  292. setInsertStatus(InsertStatusEnum.error);
  293. } else {
  294. await DataService.flush(collectionValue);
  295. // update collections
  296. await onInsert(collectionValue);
  297. setInsertStatus(InsertStatusEnum.success);
  298. }
  299. } catch (err: any) {
  300. const {
  301. response: {
  302. data: { message },
  303. },
  304. } = err;
  305. setInsertFailMsg(message);
  306. setInsertStatus(InsertStatusEnum.error);
  307. }
  308. };
  309. const handleCollectionChange = (name: string) => {
  310. setCollectionValue(name);
  311. };
  312. const handleNext = () => {
  313. const isJSON = typeof jsonData !== 'undefined';
  314. switch (activeStep) {
  315. case InsertStepperEnum.import:
  316. setActiveStep(activeStep => activeStep + (isJSON ? 2 : 1));
  317. if (isJSON) {
  318. handleInsertData();
  319. }
  320. break;
  321. case InsertStepperEnum.preview:
  322. setActiveStep(activeStep => activeStep + 1);
  323. handleInsertData();
  324. break;
  325. // default represent InsertStepperEnum.status
  326. default:
  327. handleCloseDialog();
  328. break;
  329. }
  330. };
  331. const handleUploadFileChange = (file: File, upload: HTMLFormElement) => {
  332. setFile(file);
  333. };
  334. const handleBack = () => {
  335. switch (activeStep) {
  336. case InsertStepperEnum.import:
  337. handleCloseDialog();
  338. break;
  339. case InsertStepperEnum.preview:
  340. setActiveStep(activeStep => activeStep - 1);
  341. break;
  342. // default represent InsertStepperEnum.status
  343. // status don't have cancel button
  344. default:
  345. break;
  346. }
  347. };
  348. const generateContent = (activeStep: InsertStepperEnum) => {
  349. switch (activeStep) {
  350. case InsertStepperEnum.import:
  351. return (
  352. <InsertImport
  353. collectionOptions={collectionOptions}
  354. partitionOptions={partitionOptions}
  355. selectedCollection={collectionValue}
  356. selectedPartition={partitionValue}
  357. handleCollectionChange={handleCollectionChange}
  358. handlePartitionChange={setPartitionValue}
  359. handleUploadedData={handleUploadedData}
  360. handleUploadFileChange={handleUploadFileChange}
  361. fileName={fileName}
  362. setFileName={setFileName}
  363. />
  364. );
  365. case InsertStepperEnum.preview:
  366. return (
  367. <InsertPreview
  368. schemaOptions={schemaOptions}
  369. data={previewData}
  370. tableHeads={tableHeads}
  371. setTableHeads={setTableHeads}
  372. isContainFieldNames={isContainFieldNames}
  373. file={file}
  374. handleIsContainedChange={setIsContainFieldNames}
  375. />
  376. );
  377. // default represents InsertStepperEnum.status
  378. default:
  379. return <InsertStatus status={insertStatus} failMsg={insertFailMsg} />;
  380. }
  381. };
  382. return (
  383. <DialogTemplate
  384. title={insertTrans('import')}
  385. handleClose={handleCloseDialog}
  386. confirmLabel={confirm}
  387. cancelLabel={cancel}
  388. handleCancel={handleBack}
  389. handleConfirm={handleNext}
  390. confirmDisabled={nextDisabled}
  391. showActions={showActions}
  392. showCancel={showCancel}
  393. // don't show close icon when insert not finish
  394. showCloseIcon={insertStatus !== InsertStatusEnum.loading}
  395. >
  396. {generateContent(activeStep)}
  397. </DialogTemplate>
  398. );
  399. };
  400. export default InsertContainer;