123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436 |
- import { Theme } from '@mui/material';
- import {
- FC,
- ReactElement,
- useCallback,
- useContext,
- useEffect,
- useMemo,
- useState,
- } from 'react';
- import { parse } from 'papaparse';
- import { useTranslation } from 'react-i18next';
- import DialogTemplate from '@/components/customDialog/DialogTemplate';
- import icons from '@/components/icons/Icons';
- import { PartitionService } from '@/http';
- import { rootContext } from '@/context';
- import { combineHeadsAndData, convertVectorFields } from '@/utils';
- import { FILE_MIME_TYPE } from '@/consts';
- import InsertImport from './Import';
- import InsertPreview from './Preview';
- import InsertStatus from './Status';
- import { InsertStatusEnum, InsertStepperEnum } from './consts';
- import { DataService } from '@/http';
- import type { InsertContentProps } from './Types';
- import type { Option } from '@/components/customSelector/Types';
- import type { InsertDataParam } from '@/pages/databases/collections/Types';
- /**
- * this component contains processes during insert
- * including import, preview and status
- */
- const InsertContainer: FC<InsertContentProps> = ({
- collections = [],
- defaultSelectedCollection,
- defaultSelectedPartition,
- partitions,
- schema,
- onInsert,
- }) => {
- const { t: insertTrans } = useTranslation('insert');
- const { t: btnTrans } = useTranslation('btn');
- const { handleCloseDialog, openSnackBar } = useContext(rootContext);
- const [activeStep, setActiveStep] = useState<InsertStepperEnum>(
- InsertStepperEnum.import
- );
- const [insertStatus, setInsertStatus] = useState<InsertStatusEnum>(
- InsertStatusEnum.init
- );
- const [insertFailMsg, setInsertFailMsg] = useState<string>('');
- const [nextDisabled, setNextDisabled] = useState<boolean>(false);
- // selected collection name
- const [collectionValue, setCollectionValue] = useState<string>(
- defaultSelectedCollection
- );
- // selected partition name
- const [partitionValue, setPartitionValue] = useState<string>(
- defaultSelectedPartition
- );
- // use contain field names yes as default
- const [isContainFieldNames, setIsContainFieldNames] = useState<number>(1);
- // uploaded file name
- const [fileName, setFileName] = useState<string>('');
- const [file, setFile] = useState<File | null>(null);
- // uploaded csv data (type: string)
- const [csvData, setCsvData] = useState<any[]>([]);
- const [jsonData, setJsonData] = useState<
- Record<string, unknown> | undefined
- >();
- // handle changed table heads
- const [tableHeads, setTableHeads] = useState<string[]>([]);
- const [partitionOptions, setPartitionOptions] = useState<Option[]>([]);
- const previewData = useMemo(() => {
- // we only show top 4 results of uploaded csv data
- const end = isContainFieldNames ? 5 : 4;
- return csvData.slice(0, end);
- }, [csvData, isContainFieldNames]);
- useEffect(() => {
- if (activeStep === InsertStepperEnum.import) {
- /**
- * 1. must choose collection and partition
- * 2. must upload a csv file
- */
- const selectValid = collectionValue !== '' && partitionValue !== '';
- const uploadValid = csvData.length > 0 || typeof jsonData !== 'undefined';
- const condition = selectValid && uploadValid;
- setNextDisabled(!condition);
- }
- if (activeStep === InsertStepperEnum.preview) {
- /**
- * table heads shouldn't be empty
- */
- const headsValid = tableHeads.every(h => h !== '');
- setNextDisabled(!headsValid);
- }
- }, [
- activeStep,
- collectionValue,
- partitionValue,
- csvData,
- tableHeads,
- jsonData,
- ]);
- useEffect(() => {
- const heads = isContainFieldNames
- ? previewData[0]
- : new Array(previewData[0].length).fill('');
- setTableHeads(heads);
- }, [previewData, isContainFieldNames]);
- // every time selected collection value change, partition options and default value will change
- const fetchPartition = useCallback(async () => {
- if (collectionValue) {
- const partitions = await PartitionService.getPartitions(collectionValue);
- const partitionOptions: Option[] = partitions.map(p => ({
- label: p.name === '_default' ? insertTrans('defaultPartition') : p.name,
- value: p.name,
- }));
- setPartitionOptions(partitionOptions);
- if (partitionOptions.length > 0) {
- // set first partition option value as default value
- const [{ value: defaultPartitionValue }] = partitionOptions;
- setPartitionValue(defaultPartitionValue as string);
- }
- }
- }, [collectionValue]);
- useEffect(() => {
- // if not on partitions page, we need to fetch partitions according to selected collection
- if (!partitions || partitions.length === 0) {
- fetchPartition();
- } else {
- const options = partitions
- .map(p => ({
- label: p.name,
- value: p.name,
- }))
- // when there's single selected partition
- // insert dialog partitions shouldn't selectable
- .filter(
- partition =>
- partition.label === defaultSelectedPartition ||
- defaultSelectedPartition === ''
- );
- setPartitionOptions(options);
- }
- }, [partitions, fetchPartition, defaultSelectedPartition]);
- const BackIcon = icons.back;
- // modal actions part, buttons label text or component
- const { confirm, cancel } = useMemo(() => {
- const labelMap: {
- [key in InsertStepperEnum]: {
- confirm: string;
- cancel: string | ReactElement;
- };
- } = {
- [InsertStepperEnum.import]: {
- confirm: btnTrans('next'),
- cancel: btnTrans('cancel'),
- },
- [InsertStepperEnum.preview]: {
- confirm: btnTrans('importFile'),
- cancel: (
- <>
- <BackIcon sx={{ fontSize: '16px' }} />
- {btnTrans('previous')}
- </>
- ),
- },
- [InsertStepperEnum.status]: {
- confirm: btnTrans('done'),
- cancel: '',
- },
- };
- return labelMap[activeStep];
- }, [activeStep, btnTrans, BackIcon]);
- const { showActions, showCancel } = useMemo(() => {
- return {
- showActions: insertStatus !== InsertStatusEnum.loading,
- showCancel: insertStatus === InsertStatusEnum.init,
- };
- }, [insertStatus]);
- // props children component needed:
- const collectionOptions: Option[] = useMemo(
- () =>
- defaultSelectedCollection === ''
- ? collections.map(c => ({
- label: c.collection_name,
- value: c.collection_name,
- }))
- : [
- {
- label: defaultSelectedCollection,
- value: defaultSelectedCollection,
- },
- ],
- [collections, defaultSelectedCollection]
- );
- const {
- schemaOptions,
- autoIdFieldName,
- }: { schemaOptions: Option[]; autoIdFieldName: string } = useMemo(() => {
- /**
- * on collection page, we get schema data from collection
- * on partition page, we pass schema as props
- */
- const list = schema
- ? schema.fields
- : collections.find(c => c.collection_name === collectionValue)?.schema
- ?.fields;
- const autoIdFieldName =
- list?.find(item => item.is_primary_key && item.autoID)?.name || '';
- /**
- * if below conditions all met, this schema shouldn't be selectable as head:
- * 1. this field is primary key
- * 2. this field auto id is true
- */
- const options = (list || [])
- .filter(s => !s.autoID || !s.is_primary_key)
- .map(s => ({
- label: s.name,
- value: s.name,
- }));
- return {
- schemaOptions: options,
- autoIdFieldName,
- };
- }, [schema, collectionValue, collections]);
- const checkUploadFileValidation = (firstRowItems: string[]): boolean => {
- return checkIsAutoIdFieldValid(firstRowItems);
- };
- /**
- * when primary key field auto id is true
- * no need to upload this field data
- * @param firstRowItems uploaded file first row items
- * @returns whether invalid, true means invalid
- */
- const checkIsAutoIdFieldValid = (firstRowItems: string[]): boolean => {
- const isContainAutoIdField = firstRowItems.includes(autoIdFieldName);
- isContainAutoIdField &&
- openSnackBar(
- insertTrans('uploadAutoIdFieldWarning', { fieldName: autoIdFieldName }),
- 'error'
- );
- return isContainAutoIdField;
- };
- const handleUploadedData = (
- content: string,
- uploader: HTMLFormElement,
- type: FILE_MIME_TYPE
- ) => {
- // if json, just parse json to object
- if (type === FILE_MIME_TYPE.JSON) {
- setJsonData(JSON.parse(content));
- setCsvData([]);
- return;
- }
- const { data } = parse(content);
- // if uploaded csv contains heads, firstRowItems is the list of all heads
- const [firstRowItems = []] = data as string[][];
- const invalid = checkUploadFileValidation(firstRowItems);
- if (invalid) {
- // reset uploader value and filename
- setFileName('');
- setFile(null);
- uploader.value = null;
- return;
- }
- setCsvData(data);
- setJsonData(undefined);
- };
- const handleInsertData = async () => {
- const fields = schema
- ? schema.fields
- : collections.find(c => c.collection_name === collectionValue)?.schema
- ?.fields;
- // start loading
- setInsertStatus(InsertStatusEnum.loading);
- // process data
- const data =
- typeof jsonData !== 'undefined'
- ? convertVectorFields(jsonData, fields!)
- : combineHeadsAndData(
- tableHeads,
- isContainFieldNames ? csvData.slice(1) : csvData,
- fields!
- );
- const param: InsertDataParam = {
- partition_name: partitionValue,
- fields_data: data,
- };
- try {
- const inserted = await DataService.insertData(collectionValue, param);
- if (inserted.status.error_code !== 'Success') {
- setInsertFailMsg(inserted.status.reason);
- setInsertStatus(InsertStatusEnum.error);
- } else {
- await DataService.flush(collectionValue);
- // update collections
- await onInsert(collectionValue);
- setInsertStatus(InsertStatusEnum.success);
- }
- } catch (err: any) {
- const {
- response: {
- data: { message },
- },
- } = err;
- setInsertFailMsg(message);
- setInsertStatus(InsertStatusEnum.error);
- }
- };
- const handleCollectionChange = (name: string) => {
- setCollectionValue(name);
- };
- const handleNext = () => {
- const isJSON = typeof jsonData !== 'undefined';
- switch (activeStep) {
- case InsertStepperEnum.import:
- setActiveStep(activeStep => activeStep + (isJSON ? 2 : 1));
- if (isJSON) {
- handleInsertData();
- }
- break;
- case InsertStepperEnum.preview:
- setActiveStep(activeStep => activeStep + 1);
- handleInsertData();
- break;
- // default represent InsertStepperEnum.status
- default:
- handleCloseDialog();
- break;
- }
- };
- const handleUploadFileChange = (file: File, upload: HTMLFormElement) => {
- setFile(file);
- };
- const handleBack = () => {
- switch (activeStep) {
- case InsertStepperEnum.import:
- handleCloseDialog();
- break;
- case InsertStepperEnum.preview:
- setActiveStep(activeStep => activeStep - 1);
- break;
- // default represent InsertStepperEnum.status
- // status don't have cancel button
- default:
- break;
- }
- };
- const generateContent = (activeStep: InsertStepperEnum) => {
- switch (activeStep) {
- case InsertStepperEnum.import:
- return (
- <InsertImport
- collectionOptions={collectionOptions}
- partitionOptions={partitionOptions}
- selectedCollection={collectionValue}
- selectedPartition={partitionValue}
- handleCollectionChange={handleCollectionChange}
- handlePartitionChange={setPartitionValue}
- handleUploadedData={handleUploadedData}
- handleUploadFileChange={handleUploadFileChange}
- fileName={fileName}
- setFileName={setFileName}
- />
- );
- case InsertStepperEnum.preview:
- return (
- <InsertPreview
- schemaOptions={schemaOptions}
- data={previewData}
- tableHeads={tableHeads}
- setTableHeads={setTableHeads}
- isContainFieldNames={isContainFieldNames}
- file={file}
- handleIsContainedChange={setIsContainFieldNames}
- />
- );
- // default represents InsertStepperEnum.status
- default:
- return <InsertStatus status={insertStatus} failMsg={insertFailMsg} />;
- }
- };
- return (
- <DialogTemplate
- title={insertTrans('import')}
- handleClose={handleCloseDialog}
- confirmLabel={confirm}
- cancelLabel={cancel}
- handleCancel={handleBack}
- handleConfirm={handleNext}
- confirmDisabled={nextDisabled}
- showActions={showActions}
- showCancel={showCancel}
- // don't show close icon when insert not finish
- showCloseIcon={insertStatus !== InsertStatusEnum.loading}
- >
- {generateContent(activeStep)}
- </DialogTemplate>
- );
- };
- export default InsertContainer;
|