mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-29 09:44:47 +00:00
perf: workflow&plugins json config import and export (#2592)
This commit is contained in:
@@ -14,6 +14,9 @@ import { useCopyData } from '@/web/common/hooks/useCopyData';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import MyTag from '@fastgpt/web/components/common/Tag/index';
|
||||
import { publishStatusStyle } from '../constants';
|
||||
import MyPopover from '@fastgpt/web/components/common/MyPopover';
|
||||
import { fileDownload } from '@/web/common/file/utils';
|
||||
import { AppChatConfigType } from '@fastgpt/global/core/app/type';
|
||||
|
||||
const ImportSettings = dynamic(() => import('./Flow/ImportSettings'));
|
||||
|
||||
@@ -26,32 +29,16 @@ const AppCard = ({
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { appT } = useI18n();
|
||||
const { copyData } = useCopyData();
|
||||
const { feConfigs } = useSystemStore();
|
||||
|
||||
const { appDetail, onOpenInfoEdit, onOpenTeamTagModal, onDelApp, currentTab } =
|
||||
useContextSelector(AppContext, (v) => v);
|
||||
const { historiesDefaultData, flowData2StoreDataAndCheck, onSaveWorkflow, isSaving } =
|
||||
useContextSelector(WorkflowContext, (v) => v);
|
||||
const { historiesDefaultData, onSaveWorkflow, isSaving } = useContextSelector(
|
||||
WorkflowContext,
|
||||
(v) => v
|
||||
);
|
||||
|
||||
const { isOpen: isOpenImport, onOpen: onOpenImport, onClose: onCloseImport } = useDisclosure();
|
||||
const onExportWorkflow = useCallback(async () => {
|
||||
const data = flowData2StoreDataAndCheck();
|
||||
if (data) {
|
||||
copyData(
|
||||
JSON.stringify(
|
||||
{
|
||||
nodes: filterSensitiveNodesData(data.nodes),
|
||||
edges: data.edges,
|
||||
chatConfig: appDetail.chatConfig
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
appT('export_config_successful')
|
||||
);
|
||||
}
|
||||
}, [appDetail.chatConfig, appT, copyData, flowData2StoreDataAndCheck]);
|
||||
|
||||
const InfoMenu = useCallback(
|
||||
({ children }: { children: React.ReactNode }) => {
|
||||
@@ -84,9 +71,11 @@ const AppCard = ({
|
||||
onClick: onOpenImport
|
||||
},
|
||||
{
|
||||
label: appT('export_configs'),
|
||||
icon: 'export',
|
||||
onClick: onExportWorkflow
|
||||
label: ExportPopover({
|
||||
chatConfig: appDetail.chatConfig,
|
||||
appName: appDetail.name
|
||||
}),
|
||||
onClick: () => {}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -124,6 +113,8 @@ const AppCard = ({
|
||||
);
|
||||
},
|
||||
[
|
||||
appDetail.chatConfig,
|
||||
appDetail.name,
|
||||
appDetail.permission.hasWritePer,
|
||||
appDetail.permission.isOwner,
|
||||
appT,
|
||||
@@ -131,7 +122,6 @@ const AppCard = ({
|
||||
feConfigs?.show_team_chat,
|
||||
historiesDefaultData,
|
||||
onDelApp,
|
||||
onExportWorkflow,
|
||||
onOpenImport,
|
||||
onOpenInfoEdit,
|
||||
onOpenTeamTagModal,
|
||||
@@ -192,4 +182,98 @@ const AppCard = ({
|
||||
return Render;
|
||||
};
|
||||
|
||||
function ExportPopover({
|
||||
chatConfig,
|
||||
appName
|
||||
}: {
|
||||
chatConfig: AppChatConfigType;
|
||||
appName: string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { copyData } = useCopyData();
|
||||
const { flowData2StoreDataAndCheck } = useContextSelector(WorkflowContext, (v) => v);
|
||||
const data = flowData2StoreDataAndCheck();
|
||||
const onExportWorkflow = useCallback(async () => {
|
||||
const data = flowData2StoreDataAndCheck();
|
||||
if (data) {
|
||||
copyData(
|
||||
JSON.stringify(
|
||||
{
|
||||
nodes: filterSensitiveNodesData(data.nodes),
|
||||
edges: data.edges,
|
||||
chatConfig: chatConfig
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
t('app:export_config_successful')
|
||||
);
|
||||
}
|
||||
}, [chatConfig, copyData, flowData2StoreDataAndCheck, t]);
|
||||
|
||||
return (
|
||||
<MyPopover
|
||||
placement={'right-start'}
|
||||
offset={[-5, 20]}
|
||||
hasArrow={false}
|
||||
trigger={'hover'}
|
||||
w={'8.6rem'}
|
||||
Trigger={
|
||||
<Flex align={'center'} w={'100%'}>
|
||||
<Avatar src={'export'} borderRadius={'sm'} w={'1rem'} mr={3} />
|
||||
{t('app:export_configs')}
|
||||
</Flex>
|
||||
}
|
||||
>
|
||||
{({ onClose }) => (
|
||||
<Box p={1}>
|
||||
<Flex
|
||||
py={'0.38rem'}
|
||||
px={1}
|
||||
color={'myGray.600'}
|
||||
_hover={{
|
||||
bg: 'myGray.05',
|
||||
color: 'primary.600'
|
||||
}}
|
||||
borderRadius={'xs'}
|
||||
onClick={onExportWorkflow}
|
||||
>
|
||||
<MyIcon name={'copy'} w={'1rem'} mr={2} />
|
||||
<Box fontSize={'mini'}>{t('common:common.copy_to_clipboard')}</Box>
|
||||
</Flex>
|
||||
<Flex
|
||||
py={'0.38rem'}
|
||||
px={1}
|
||||
color={'myGray.600'}
|
||||
_hover={{
|
||||
bg: 'myGray.05',
|
||||
color: 'primary.600'
|
||||
}}
|
||||
borderRadius={'xs'}
|
||||
onClick={() => {
|
||||
if (!data) return;
|
||||
fileDownload({
|
||||
text: JSON.stringify(
|
||||
{
|
||||
nodes: filterSensitiveNodesData(data.nodes),
|
||||
edges: data.edges,
|
||||
chatConfig: chatConfig
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
type: 'application/json;charset=utf-8',
|
||||
filename: `${appName}.json`
|
||||
});
|
||||
}}
|
||||
>
|
||||
<MyIcon name={'configmap'} w={'1rem'} mr={2} />
|
||||
<Box fontSize={'mini'}>{t('common:common.export_to_json')}</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
)}
|
||||
</MyPopover>
|
||||
);
|
||||
}
|
||||
|
||||
export default AppCard;
|
||||
|
@@ -1,11 +1,14 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Textarea, Button, ModalBody, ModalFooter } from '@chakra-ui/react';
|
||||
import React, { DragEvent, useCallback, useMemo, useState } from 'react';
|
||||
import { Textarea, Button, ModalBody, ModalFooter, Flex, Box } from '@chakra-ui/react';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../context';
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
};
|
||||
@@ -13,46 +16,161 @@ type Props = {
|
||||
const ImportSettings = ({ onClose }: Props) => {
|
||||
const { appT } = useI18n();
|
||||
const { toast } = useToast();
|
||||
const { File, onOpen } = useSelectFile({
|
||||
fileType: 'json',
|
||||
multiple: false
|
||||
});
|
||||
const { isPc } = useSystem();
|
||||
const initData = useContextSelector(WorkflowContext, (v) => v.initData);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [value, setValue] = useState('');
|
||||
const { t } = useTranslation();
|
||||
const handleDragEnter = useCallback((e: DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(true);
|
||||
}, []);
|
||||
|
||||
const handleDragLeave = useCallback((e: DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(false);
|
||||
}, []);
|
||||
const handleDrop = useCallback(async (e: DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
const file = e.dataTransfer.files[0];
|
||||
readJSONFile(file);
|
||||
setIsDragging(false);
|
||||
}, []);
|
||||
|
||||
const readJSONFile = useCallback(
|
||||
(file: File) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
if (!file.name.endsWith('.json')) {
|
||||
toast({
|
||||
title: t('app:not_json_file'),
|
||||
status: 'error'
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (e.target) {
|
||||
const res = JSON.parse(e.target.result as string);
|
||||
setValue(JSON.stringify(res, null, 2));
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
},
|
||||
[t, toast]
|
||||
);
|
||||
|
||||
const onSelectFile = useCallback(
|
||||
async (e: File[]) => {
|
||||
const file = e[0];
|
||||
readJSONFile(file);
|
||||
console.log(file);
|
||||
},
|
||||
[readJSONFile]
|
||||
);
|
||||
return (
|
||||
<MyModal
|
||||
isOpen
|
||||
w={'600px'}
|
||||
onClose={onClose}
|
||||
iconSrc="/imgs/modal/params.svg"
|
||||
title={appT('import_configs')}
|
||||
title={
|
||||
<Flex align={'center'} ml={-3}>
|
||||
<MyIcon name={'common/importLight'} color={'primary.600'} w={'1.25rem'} mr={'0.62rem'} />
|
||||
<Box lineHeight={'1.25rem'}>{appT('import_configs')}</Box>
|
||||
</Flex>
|
||||
}
|
||||
>
|
||||
<ModalBody>
|
||||
<Textarea
|
||||
placeholder={appT('paste_config')}
|
||||
defaultValue={value}
|
||||
rows={16}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
/>
|
||||
<ModalBody py={'2rem'} px={'3.25rem'}>
|
||||
<File onSelect={onSelectFile} />
|
||||
{isDragging ? (
|
||||
<Flex
|
||||
align={'center'}
|
||||
justify={'center'}
|
||||
w={'31rem'}
|
||||
h={'21.25rem'}
|
||||
borderRadius={'md'}
|
||||
border={'1px dashed'}
|
||||
borderColor={'myGray.400'}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
onDrop={handleDrop}
|
||||
onDragLeave={handleDragLeave}
|
||||
>
|
||||
<Flex align={'center'} justify={'center'} flexDir={'column'} gap={'0.62rem'}>
|
||||
<MyIcon name={'configmap'} w={'1.5rem'} color={'myGray.500'} />
|
||||
<Box color={'myGray.600'} fontSize={'mini'}>
|
||||
{t('app:file_recover')}
|
||||
</Box>
|
||||
</Flex>
|
||||
</Flex>
|
||||
) : (
|
||||
<Box w={'31rem'} minH={'21.25rem'}>
|
||||
<Flex justify={'space-between'} align={'center'} pb={2}>
|
||||
<Box fontSize={'sm'} color={'myGray.900'} fontWeight={'500'}>
|
||||
{t('common:common.json_config')}
|
||||
</Box>
|
||||
<Button onClick={onOpen} variant={'whiteBase'} p={0}>
|
||||
<Flex px={'0.88rem'} py={'0.44rem'} color={'myGray.600'} fontSize={'mini'}>
|
||||
<MyIcon name={'file/uploadFile'} w={'1rem'} mr={'0.38rem'} />
|
||||
{t('common:common.upload_file')}
|
||||
</Flex>
|
||||
</Button>
|
||||
</Flex>
|
||||
<Box
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
onDrop={handleDrop}
|
||||
onDragLeave={handleDragLeave}
|
||||
>
|
||||
<Textarea
|
||||
bg={'myGray.50'}
|
||||
border={'1px solid'}
|
||||
borderRadius={'md'}
|
||||
borderColor={'myGray.200'}
|
||||
h={'15.125rem'}
|
||||
value={value}
|
||||
placeholder={
|
||||
isPc
|
||||
? t('app:paste_config') + '\n' + t('app:or_drag_JSON')
|
||||
: t('app:paste_config')
|
||||
}
|
||||
defaultValue={value}
|
||||
rows={16}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
/>
|
||||
</Box>
|
||||
<Flex justify={'flex-end'} pt={5}>
|
||||
<Button
|
||||
p={0}
|
||||
onClick={() => {
|
||||
if (!value) {
|
||||
return onClose();
|
||||
}
|
||||
try {
|
||||
const data = JSON.parse(value);
|
||||
initData(data);
|
||||
onClose();
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: appT('import_configs_failed')
|
||||
});
|
||||
}
|
||||
toast({
|
||||
title: t('app:import_configs_success'),
|
||||
status: 'success'
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Flex px={5} py={2} fontWeight={'500'}>
|
||||
{t('common:common.Save')}
|
||||
</Flex>
|
||||
</Button>
|
||||
</Flex>
|
||||
</Box>
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
variant="whiteBase"
|
||||
onClick={() => {
|
||||
if (!value) {
|
||||
return onClose();
|
||||
}
|
||||
try {
|
||||
const data = JSON.parse(value);
|
||||
initData(data);
|
||||
onClose();
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: appT('import_configs_failed')
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('common:common.Confirm')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
Reference in New Issue
Block a user