Container.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. import { makeStyles, Theme } from '@material-ui/core';
  2. import {
  3. FC,
  4. ReactElement,
  5. useCallback,
  6. useContext,
  7. useEffect,
  8. useMemo,
  9. useState,
  10. } from 'react';
  11. import { useTranslation } from 'react-i18next';
  12. import DialogTemplate from '../customDialog/DialogTemplate';
  13. import icons from '../icons/Icons';
  14. import { rootContext } from '../../context/Root';
  15. import InsertImport from './Import';
  16. import InsertPreview from './Preview';
  17. import InsertStatus from './Status';
  18. import {
  19. InsertContentProps,
  20. InsertStatusEnum,
  21. InsertStepperEnum,
  22. } from './Types';
  23. import { Option } from '../customSelector/Types';
  24. import { parse } from 'papaparse';
  25. import { PartitionHttp } from '../../http/Partition';
  26. import { combineHeadsAndData } from '../../utils/Insert';
  27. const getStyles = makeStyles((theme: Theme) => ({
  28. icon: {
  29. fontSize: '16px',
  30. },
  31. }));
  32. /**
  33. * this component contains processes during insert
  34. * including import, preview and status
  35. */
  36. const InsertContainer: FC<InsertContentProps> = ({
  37. collections = [],
  38. defaultSelectedCollection,
  39. defaultSelectedPartition,
  40. partitions,
  41. schema,
  42. handleInsert,
  43. }) => {
  44. const classes = getStyles();
  45. const { t: insertTrans } = useTranslation('insert');
  46. const { t: btnTrans } = useTranslation('btn');
  47. const { handleCloseDialog, openSnackBar } = useContext(rootContext);
  48. const [activeStep, setActiveStep] = useState<InsertStepperEnum>(
  49. InsertStepperEnum.import
  50. );
  51. const [insertStatus, setInsertStatus] = useState<InsertStatusEnum>(
  52. InsertStatusEnum.init
  53. );
  54. const [insertFailMsg, setInsertFailMsg] = useState<string>('');
  55. const [nextDisabled, setNextDisabled] = useState<boolean>(false);
  56. // selected collection name
  57. const [collectionValue, setCollectionValue] = useState<string>(
  58. defaultSelectedCollection
  59. );
  60. // selected partition name
  61. const [partitionValue, setPartitionValue] = useState<string>(
  62. defaultSelectedPartition
  63. );
  64. // use contain field names yes as default
  65. const [isContainFieldNames, setIsContainFieldNames] = useState<number>(1);
  66. // uploaded file name
  67. const [fileName, setFileName] = useState<string>('');
  68. const [file, setFile] = useState<File | null>(null);
  69. // uploaded csv data (type: string)
  70. const [csvData, setCsvData] = useState<any[]>([]);
  71. // handle changed table heads
  72. const [tableHeads, setTableHeads] = useState<string[]>([]);
  73. const [partitionOptions, setPartitionOptions] = useState<Option[]>([]);
  74. const previewData = useMemo(() => {
  75. // we only show top 4 results of uploaded csv data
  76. const end = isContainFieldNames ? 5 : 4;
  77. return csvData.slice(0, end);
  78. }, [csvData, isContainFieldNames]);
  79. useEffect(() => {
  80. if (activeStep === InsertStepperEnum.import) {
  81. /**
  82. * 1. must choose collection and partition
  83. * 2. must upload a csv file
  84. */
  85. const selectValid = collectionValue !== '' && partitionValue !== '';
  86. const uploadValid = csvData.length > 0;
  87. const condition = selectValid && uploadValid;
  88. setNextDisabled(!condition);
  89. }
  90. if (activeStep === InsertStepperEnum.preview) {
  91. /**
  92. * table heads shouldn't be empty
  93. */
  94. const headsValid = tableHeads.every(h => h !== '');
  95. setNextDisabled(!headsValid);
  96. }
  97. }, [activeStep, collectionValue, partitionValue, csvData, tableHeads]);
  98. useEffect(() => {
  99. const heads = isContainFieldNames
  100. ? previewData[0]
  101. : new Array(previewData[0].length).fill('');
  102. setTableHeads(heads);
  103. }, [previewData, isContainFieldNames]);
  104. const fetchPartition = useCallback(async () => {
  105. if (collectionValue) {
  106. const partitions = await PartitionHttp.getPartitions(collectionValue);
  107. const partitionOptions: Option[] = partitions.map(p => ({
  108. label: p._formatName,
  109. value: p._name,
  110. }));
  111. setPartitionOptions(partitionOptions);
  112. }
  113. }, [collectionValue]);
  114. useEffect(() => {
  115. // if not on partitions page, we need to fetch partitions according to selected collection
  116. if (!partitions || partitions.length === 0) {
  117. fetchPartition();
  118. } else {
  119. const options = partitions
  120. .map(p => ({
  121. label: p._formatName,
  122. value: p._name,
  123. }))
  124. // when there's single selected partition
  125. // insert dialog partitions shouldn't selectable
  126. .filter(
  127. partition =>
  128. partition.label === defaultSelectedPartition ||
  129. defaultSelectedPartition === ''
  130. );
  131. setPartitionOptions(options);
  132. }
  133. }, [partitions, fetchPartition, defaultSelectedPartition]);
  134. const BackIcon = icons.back;
  135. // modal actions part, buttons label text or component
  136. const { confirm, cancel } = useMemo(() => {
  137. const labelMap: {
  138. [key in InsertStepperEnum]: {
  139. confirm: string;
  140. cancel: string | ReactElement;
  141. };
  142. } = {
  143. [InsertStepperEnum.import]: {
  144. confirm: btnTrans('next'),
  145. cancel: btnTrans('cancel'),
  146. },
  147. [InsertStepperEnum.preview]: {
  148. confirm: btnTrans('insert'),
  149. cancel: (
  150. <>
  151. <BackIcon classes={{ root: classes.icon }} />
  152. {btnTrans('previous')}
  153. </>
  154. ),
  155. },
  156. [InsertStepperEnum.status]: {
  157. confirm: btnTrans('done'),
  158. cancel: '',
  159. },
  160. };
  161. return labelMap[activeStep];
  162. }, [activeStep, btnTrans, BackIcon, classes.icon]);
  163. const { showActions, showCancel } = useMemo(() => {
  164. return {
  165. showActions: insertStatus !== InsertStatusEnum.loading,
  166. showCancel: insertStatus === InsertStatusEnum.init,
  167. };
  168. }, [insertStatus]);
  169. // props children component needed:
  170. const collectionOptions: Option[] = useMemo(
  171. () =>
  172. defaultSelectedCollection === ''
  173. ? collections.map(c => ({
  174. label: c._name,
  175. value: c._name,
  176. }))
  177. : [
  178. {
  179. label: defaultSelectedCollection,
  180. value: defaultSelectedCollection,
  181. },
  182. ],
  183. [collections, defaultSelectedCollection]
  184. );
  185. const schemaOptions: Option[] = useMemo(() => {
  186. const list =
  187. schema && schema.length > 0
  188. ? schema
  189. : collections.find(c => c._name === collectionValue)?._fields;
  190. return (list || []).map(s => ({
  191. label: s._fieldName,
  192. value: s._fieldId,
  193. }));
  194. }, [schema, collectionValue, collections]);
  195. const checkUploadFileValidation = (fieldNamesLength: number): boolean => {
  196. return schemaOptions.length === fieldNamesLength;
  197. };
  198. const handleUploadedData = (csv: string, uploader: HTMLFormElement) => {
  199. const { data } = parse(csv);
  200. const uploadFieldNamesLength = (data as string[])[0].length;
  201. const validation = checkUploadFileValidation(uploadFieldNamesLength);
  202. if (!validation) {
  203. // open snackbar
  204. openSnackBar(insertTrans('uploadFieldNamesLenWarning'), 'error');
  205. // reset uploader value and filename
  206. setFileName('');
  207. setFile(null);
  208. uploader.value = null;
  209. return;
  210. }
  211. setCsvData(data);
  212. };
  213. const handleInsertData = async () => {
  214. // start loading
  215. setInsertStatus(InsertStatusEnum.loading);
  216. // combine table heads and data
  217. const tableData = isContainFieldNames ? csvData.slice(1) : csvData;
  218. const data = combineHeadsAndData(tableHeads, tableData);
  219. const { result, msg } = await handleInsert(
  220. collectionValue,
  221. partitionValue,
  222. data
  223. );
  224. if (!result) {
  225. setInsertFailMsg(msg);
  226. }
  227. const status = result ? InsertStatusEnum.success : InsertStatusEnum.error;
  228. setInsertStatus(status);
  229. };
  230. const handleCollectionChange = (name: string) => {
  231. setCollectionValue(name);
  232. // reset partition
  233. setPartitionValue('');
  234. };
  235. const handleNext = () => {
  236. switch (activeStep) {
  237. case InsertStepperEnum.import:
  238. setActiveStep(activeStep => activeStep + 1);
  239. break;
  240. case InsertStepperEnum.preview:
  241. setActiveStep(activeStep => activeStep + 1);
  242. handleInsertData();
  243. break;
  244. // default represent InsertStepperEnum.status
  245. default:
  246. handleCloseDialog();
  247. break;
  248. }
  249. };
  250. const handleUploadFileChange = (file: File, upload: HTMLFormElement) => {
  251. console.log(file);
  252. setFile(file);
  253. };
  254. const handleBack = () => {
  255. switch (activeStep) {
  256. case InsertStepperEnum.import:
  257. handleCloseDialog();
  258. break;
  259. case InsertStepperEnum.preview:
  260. setActiveStep(activeStep => activeStep - 1);
  261. break;
  262. // default represent InsertStepperEnum.status
  263. // status don't have cancel button
  264. default:
  265. break;
  266. }
  267. };
  268. const generateContent = (activeStep: InsertStepperEnum) => {
  269. switch (activeStep) {
  270. case InsertStepperEnum.import:
  271. return (
  272. <InsertImport
  273. collectionOptions={collectionOptions}
  274. partitionOptions={partitionOptions}
  275. selectedCollection={collectionValue}
  276. selectedPartition={partitionValue}
  277. handleCollectionChange={handleCollectionChange}
  278. handlePartitionChange={setPartitionValue}
  279. handleUploadedData={handleUploadedData}
  280. handleUploadFileChange={handleUploadFileChange}
  281. fileName={fileName}
  282. setFileName={setFileName}
  283. />
  284. );
  285. case InsertStepperEnum.preview:
  286. return (
  287. <InsertPreview
  288. schemaOptions={schemaOptions}
  289. data={previewData}
  290. tableHeads={tableHeads}
  291. setTableHeads={setTableHeads}
  292. isContainFieldNames={isContainFieldNames}
  293. file={file}
  294. handleIsContainedChange={setIsContainFieldNames}
  295. />
  296. );
  297. // default represents InsertStepperEnum.status
  298. default:
  299. return <InsertStatus status={insertStatus} failMsg={insertFailMsg} />;
  300. }
  301. };
  302. return (
  303. <DialogTemplate
  304. title={insertTrans('import')}
  305. handleClose={handleCloseDialog}
  306. confirmLabel={confirm}
  307. cancelLabel={cancel}
  308. handleCancel={handleBack}
  309. handleConfirm={handleNext}
  310. confirmDisabled={nextDisabled}
  311. showActions={showActions}
  312. showCancel={showCancel}
  313. // don't show close icon when insert not finish
  314. showCloseIcon={insertStatus !== InsertStatusEnum.loading}
  315. >
  316. {generateContent(activeStep)}
  317. </DialogTemplate>
  318. );
  319. };
  320. export default InsertContainer;