perf: workflow&plugins json config import and export (#2592)

This commit is contained in:
papapatrick
2024-09-02 15:05:58 +08:00
committed by GitHub
parent 84de95d294
commit 036097243a
11 changed files with 302 additions and 71 deletions

View File

@@ -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;

View File

@@ -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>
);
};