diff --git a/packages/server/src/Interface.ts b/packages/server/src/Interface.ts index 803d044df2f..af2a80641ce 100644 --- a/packages/server/src/Interface.ts +++ b/packages/server/src/Interface.ts @@ -1,5 +1,4 @@ -import { IAction } from 'flowise-components' -import { ICommonObject, IFileUpload, INode, INodeData as INodeDataFromComponent, INodeParams } from 'flowise-components' +import { IAction, ICommonObject, IFileUpload, INode, INodeData as INodeDataFromComponent, INodeParams } from 'flowise-components' export type MessageType = 'apiMessage' | 'userMessage' diff --git a/packages/server/src/controllers/export-import/index.ts b/packages/server/src/controllers/export-import/index.ts new file mode 100644 index 00000000000..ba9d853796c --- /dev/null +++ b/packages/server/src/controllers/export-import/index.ts @@ -0,0 +1,26 @@ +import { NextFunction, Request, Response } from 'express' +import exportImportService from '../../services/export-import' + +const exportData = async (req: Request, res: Response, next: NextFunction) => { + try { + const apiResponse = await exportImportService.exportData(exportImportService.convertExportInput(req.body)) + return res.json(apiResponse) + } catch (error) { + next(error) + } +} + +const importData = async (req: Request, res: Response, next: NextFunction) => { + try { + const importData = req.body + await exportImportService.importData(importData) + return res.json({ message: 'success' }) + } catch (error) { + next(error) + } +} + +export default { + exportData, + importData +} diff --git a/packages/server/src/routes/export-import/index.ts b/packages/server/src/routes/export-import/index.ts new file mode 100644 index 00000000000..40c3930d2fd --- /dev/null +++ b/packages/server/src/routes/export-import/index.ts @@ -0,0 +1,9 @@ +import express from 'express' +import exportImportController from '../../controllers/export-import' +const router = express.Router() + +router.post('/export', exportImportController.exportData) + +router.post('/import', exportImportController.importData) + +export default router diff --git a/packages/server/src/routes/index.ts b/packages/server/src/routes/index.ts index 075b28fb963..dd660d16f69 100644 --- a/packages/server/src/routes/index.ts +++ b/packages/server/src/routes/index.ts @@ -9,6 +9,7 @@ import componentsCredentialsRouter from './components-credentials' import componentsCredentialsIconRouter from './components-credentials-icon' import credentialsRouter from './credentials' import documentStoreRouter from './documentstore' +import exportImportRouter from './export-import' import feedbackRouter from './feedback' import fetchLinksRouter from './fetch-links' import flowConfigRouter from './flow-config' @@ -53,6 +54,7 @@ router.use('/components-credentials-icon', componentsCredentialsIconRouter) router.use('/chatflows-uploads', chatflowsUploadsRouter) router.use('/credentials', credentialsRouter) router.use('/document-store', documentStoreRouter) +router.use('/export-import', exportImportRouter) router.use('/feedback', feedbackRouter) router.use('/fetch-links', fetchLinksRouter) router.use('/flow-config', flowConfigRouter) diff --git a/packages/server/src/services/assistants/index.ts b/packages/server/src/services/assistants/index.ts index 48a926d4502..ff7651b32f2 100644 --- a/packages/server/src/services/assistants/index.ts +++ b/packages/server/src/services/assistants/index.ts @@ -292,10 +292,60 @@ const updateAssistant = async (assistantId: string, requestBody: any): Promise[]): Promise => { + try { + const appServer = getRunningExpressApp() + + // step 1 - check whether array is zero + if (newAssistants.length == 0) return + + // step 2 - check whether ids are duplicate in database + let ids = '(' + let count: number = 0 + const lastCount = newAssistants.length - 1 + newAssistants.forEach((newAssistant) => { + ids += `'${newAssistant.id}'` + if (lastCount != count) ids += ',' + if (lastCount == count) ids += ')' + count += 1 + }) + + const selectResponse = await appServer.AppDataSource.getRepository(Assistant) + .createQueryBuilder('assistant') + .select('assistant.id') + .where(`assistant.id IN ${ids}`) + .getMany() + const foundIds = selectResponse.map((response) => { + return response.id + }) + + // step 3 - remove ids that are only duplicate + const prepVariables: Partial[] = newAssistants.map((newAssistant) => { + let id: string = '' + if (newAssistant.id) id = newAssistant.id + if (foundIds.includes(id)) { + newAssistant.id = undefined + } + return newAssistant + }) + + // step 4 - transactional insert array of entities + const insertResponse = await appServer.AppDataSource.getRepository(Assistant).insert(prepVariables) + + return insertResponse + } catch (error) { + throw new InternalFlowiseError( + StatusCodes.INTERNAL_SERVER_ERROR, + `Error: variableService.importVariables - ${getErrorMessage(error)}` + ) + } +} + export default { createAssistant, deleteAssistant, getAllAssistants, getAssistantById, - updateAssistant + updateAssistant, + importAssistants } diff --git a/packages/server/src/services/chatflows/index.ts b/packages/server/src/services/chatflows/index.ts index e32136dd746..4feff1590a6 100644 --- a/packages/server/src/services/chatflows/index.ts +++ b/packages/server/src/services/chatflows/index.ts @@ -1,6 +1,6 @@ import { removeFolderFromStorage } from 'flowise-components' import { StatusCodes } from 'http-status-codes' -import { ChatflowType, IChatFlow, IReactFlowObject } from '../../Interface' +import { ChatflowType, IReactFlowObject } from '../../Interface' import { ChatFlow } from '../../database/entities/ChatFlow' import { ChatMessage } from '../../database/entities/ChatMessage' import { ChatMessageFeedback } from '../../database/entities/ChatMessageFeedback' @@ -103,14 +103,17 @@ const deleteChatflow = async (chatflowId: string): Promise => { } } -const getAllChatflows = async (type?: ChatflowType): Promise => { +const getAllChatflows = async (type?: ChatflowType): Promise => { try { const appServer = getRunningExpressApp() const dbResponse = await appServer.AppDataSource.getRepository(ChatFlow).find() if (type === 'MULTIAGENT') { - return dbResponse.filter((chatflow) => chatflow.type === type) + return dbResponse.filter((chatflow) => chatflow.type === 'MULTIAGENT') + } else if (type === 'CHATFLOW') { + // fetch all chatflows that are not agentflow + return dbResponse.filter((chatflow) => chatflow.type === 'CHATFLOW' || !chatflow.type) } - return dbResponse.filter((chatflow) => chatflow.type === 'CHATFLOW' || !chatflow.type) + return dbResponse } catch (error) { throw new InternalFlowiseError( StatusCodes.INTERNAL_SERVER_ERROR, @@ -202,7 +205,7 @@ const importChatflows = async (newChatflows: Partial[]): Promise const appServer = getRunningExpressApp() // step 1 - check whether file chatflows array is zero - if (newChatflows.length == 0) throw new Error('No chatflows in this file.') + if (newChatflows.length == 0) return // step 2 - check whether ids are duplicate in database let ids = '(' @@ -232,9 +235,8 @@ const importChatflows = async (newChatflows: Partial[]): Promise if (newChatflow.flowData) flowData = newChatflow.flowData if (foundIds.includes(id)) { newChatflow.id = undefined - newChatflow.name += ' with new id' + newChatflow.name += ' (1)' } - newChatflow.type = 'CHATFLOW' newChatflow.flowData = JSON.stringify(JSON.parse(flowData)) return newChatflow }) diff --git a/packages/server/src/services/documentstore/index.ts b/packages/server/src/services/documentstore/index.ts index 05a693429d3..42c6cfd8883 100644 --- a/packages/server/src/services/documentstore/index.ts +++ b/packages/server/src/services/documentstore/index.ts @@ -62,6 +62,19 @@ const getAllDocumentStores = async () => { } } +const getAllDocumentFileChunks = async () => { + try { + const appServer = getRunningExpressApp() + const entities = await appServer.AppDataSource.getRepository(DocumentStoreFileChunk).find() + return entities + } catch (error) { + throw new InternalFlowiseError( + StatusCodes.INTERNAL_SERVER_ERROR, + `Error: documentStoreServices.getAllDocumentFileChunks - ${getErrorMessage(error)}` + ) + } +} + const deleteLoaderFromDocumentStore = async (storeId: string, loaderId: string) => { try { const appServer = getRunningExpressApp() @@ -1208,6 +1221,7 @@ export default { createDocumentStore, deleteLoaderFromDocumentStore, getAllDocumentStores, + getAllDocumentFileChunks, getDocumentStoreById, getUsedChatflowNames, getDocumentStoreFileChunks, diff --git a/packages/server/src/services/export-import/index.ts b/packages/server/src/services/export-import/index.ts new file mode 100644 index 00000000000..9899d24c29b --- /dev/null +++ b/packages/server/src/services/export-import/index.ts @@ -0,0 +1,119 @@ +import { StatusCodes } from 'http-status-codes' +import { ChatFlow } from '../../database/entities/ChatFlow' +import { Tool } from '../../database/entities/Tool' +import { Variable } from '../../database/entities/Variable' +import { Assistant } from '../../database/entities/Assistant' +import { InternalFlowiseError } from '../../errors/internalFlowiseError' +import { getErrorMessage } from '../../errors/utils' +import { getRunningExpressApp } from '../../utils/getRunningExpressApp' +import chatflowService from '../chatflows' +import toolsService from '../tools' +import variableService from '../variables' +import assistantService from '../assistants' + +type ExportInput = { + tool: boolean + chatflow: boolean + agentflow: boolean + variable: boolean + assistant: boolean +} + +type ExportData = { + Tool: Tool[] + ChatFlow: ChatFlow[] + AgentFlow: ChatFlow[] + Variable: Variable[] + Assistant: Assistant[] +} + +const convertExportInput = (body: any): ExportInput => { + try { + if (!body || typeof body !== 'object') throw new Error('Invalid ExportInput object in request body') + if (body.tool && typeof body.tool !== 'boolean') throw new Error('Invalid tool property in ExportInput object') + if (body.chatflow && typeof body.chatflow !== 'boolean') throw new Error('Invalid chatflow property in ExportInput object') + if (body.agentflow && typeof body.agentflow !== 'boolean') throw new Error('Invalid agentflow property in ExportInput object') + if (body.variable && typeof body.variable !== 'boolean') throw new Error('Invalid variable property in ExportInput object') + if (body.assistant && typeof body.assistant !== 'boolean') throw new Error('Invalid assistant property in ExportInput object') + return body as ExportInput + } catch (error) { + throw new InternalFlowiseError( + StatusCodes.INTERNAL_SERVER_ERROR, + `Error: exportImportService.convertExportInput - ${getErrorMessage(error)}` + ) + } +} + +const FileDefaultName = 'ExportData.json' +const exportData = async (exportInput: ExportInput): Promise<{ FileDefaultName: string } & ExportData> => { + try { + // step 1 - get all Tool + let allTool: Tool[] = [] + if (exportInput.tool === true) allTool = await toolsService.getAllTools() + + // step 2 - get all ChatFlow + let allChatflow: ChatFlow[] = [] + if (exportInput.chatflow === true) allChatflow = await chatflowService.getAllChatflows('CHATFLOW') + + // step 3 - get all MultiAgent + let allMultiAgent: ChatFlow[] = [] + if (exportInput.agentflow === true) allMultiAgent = await chatflowService.getAllChatflows('MULTIAGENT') + + let allVars: Variable[] = [] + if (exportInput.variable === true) allVars = await variableService.getAllVariables() + + let allAssistants: Assistant[] = [] + if (exportInput.assistant === true) allAssistants = await assistantService.getAllAssistants() + + return { + FileDefaultName, + Tool: allTool, + ChatFlow: allChatflow, + AgentFlow: allMultiAgent, + Variable: allVars, + Assistant: allAssistants + } + } catch (error) { + throw new InternalFlowiseError( + StatusCodes.INTERNAL_SERVER_ERROR, + `Error: exportImportService.exportData - ${getErrorMessage(error)}` + ) + } +} + +const importData = async (importData: ExportData) => { + try { + const appServer = getRunningExpressApp() + const queryRunner = appServer.AppDataSource.createQueryRunner() + + try { + queryRunner.startTransaction() + + // step 1 - importTools + if (importData.Tool.length > 0) await toolsService.importTools(importData.Tool) + // step 2 - importChatflows + if (importData.ChatFlow.length > 0) await chatflowService.importChatflows(importData.ChatFlow) + // step 3 - importAgentlows + if (importData.AgentFlow.length > 0) await chatflowService.importChatflows(importData.AgentFlow) + if (importData.Variable.length > 0) await variableService.importVariables(importData.Variable) + if (importData.Assistant.length > 0) await assistantService.importAssistants(importData.Assistant) + queryRunner.commitTransaction() + } catch (error) { + queryRunner.rollbackTransaction() + throw error + } finally { + queryRunner.release() + } + } catch (error) { + throw new InternalFlowiseError( + StatusCodes.INTERNAL_SERVER_ERROR, + `Error: exportImportService.importAll - ${getErrorMessage(error)}` + ) + } +} + +export default { + convertExportInput, + exportData, + importData +} diff --git a/packages/server/src/services/tools/index.ts b/packages/server/src/services/tools/index.ts index f2e41400ca6..dcfdc9499d4 100644 --- a/packages/server/src/services/tools/index.ts +++ b/packages/server/src/services/tools/index.ts @@ -1,9 +1,9 @@ import { StatusCodes } from 'http-status-codes' -import { getRunningExpressApp } from '../../utils/getRunningExpressApp' import { Tool } from '../../database/entities/Tool' -import { getAppVersion } from '../../utils' import { InternalFlowiseError } from '../../errors/internalFlowiseError' import { getErrorMessage } from '../../errors/utils' +import { getAppVersion } from '../../utils' +import { getRunningExpressApp } from '../../utils/getRunningExpressApp' const createTool = async (requestBody: any): Promise => { try { @@ -35,7 +35,7 @@ const deleteTool = async (toolId: string): Promise => { } } -const getAllTools = async (): Promise => { +const getAllTools = async (): Promise => { try { const appServer = getRunningExpressApp() const dbResponse = await appServer.AppDataSource.getRepository(Tool).find() @@ -79,10 +79,58 @@ const updateTool = async (toolId: string, toolBody: any): Promise => { } } +const importTools = async (newTools: Partial[]) => { + try { + const appServer = getRunningExpressApp() + + // step 1 - check whether file tools array is zero + if (newTools.length == 0) return + + // step 2 - check whether ids are duplicate in database + let ids = '(' + let count: number = 0 + const lastCount = newTools.length - 1 + newTools.forEach((newTools) => { + ids += `'${newTools.id}'` + if (lastCount != count) ids += ',' + if (lastCount == count) ids += ')' + count += 1 + }) + + const selectResponse = await appServer.AppDataSource.getRepository(Tool) + .createQueryBuilder('t') + .select('t.id') + .where(`t.id IN ${ids}`) + .getMany() + const foundIds = selectResponse.map((response) => { + return response.id + }) + + // step 3 - remove ids that are only duplicate + const prepTools: Partial[] = newTools.map((newTool) => { + let id: string = '' + if (newTool.id) id = newTool.id + if (foundIds.includes(id)) { + newTool.id = undefined + newTool.name += ' (1)' + } + return newTool + }) + + // step 4 - transactional insert array of entities + const insertResponse = await appServer.AppDataSource.getRepository(Tool).insert(prepTools) + + return insertResponse + } catch (error) { + throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Error: toolsService.importTools - ${getErrorMessage(error)}`) + } +} + export default { createTool, deleteTool, getAllTools, getToolById, - updateTool + updateTool, + importTools } diff --git a/packages/server/src/services/variables/index.ts b/packages/server/src/services/variables/index.ts index d2377dc217d..7d81352ea63 100644 --- a/packages/server/src/services/variables/index.ts +++ b/packages/server/src/services/variables/index.ts @@ -73,10 +73,61 @@ const updateVariable = async (variable: Variable, updatedVariable: Variable) => } } +const importVariables = async (newVariables: Partial[]): Promise => { + try { + const appServer = getRunningExpressApp() + + // step 1 - check whether array is zero + if (newVariables.length == 0) return + + // step 2 - check whether ids are duplicate in database + let ids = '(' + let count: number = 0 + const lastCount = newVariables.length - 1 + newVariables.forEach((newVariable) => { + ids += `'${newVariable.id}'` + if (lastCount != count) ids += ',' + if (lastCount == count) ids += ')' + count += 1 + }) + + const selectResponse = await appServer.AppDataSource.getRepository(Variable) + .createQueryBuilder('v') + .select('v.id') + .where(`v.id IN ${ids}`) + .getMany() + const foundIds = selectResponse.map((response) => { + return response.id + }) + + // step 3 - remove ids that are only duplicate + const prepVariables: Partial[] = newVariables.map((newVariable) => { + let id: string = '' + if (newVariable.id) id = newVariable.id + if (foundIds.includes(id)) { + newVariable.id = undefined + newVariable.name += ' (1)' + } + return newVariable + }) + + // step 4 - transactional insert array of entities + const insertResponse = await appServer.AppDataSource.getRepository(Variable).insert(prepVariables) + + return insertResponse + } catch (error) { + throw new InternalFlowiseError( + StatusCodes.INTERNAL_SERVER_ERROR, + `Error: variableService.importVariables - ${getErrorMessage(error)}` + ) + } +} + export default { createVariable, deleteVariable, getAllVariables, getVariableById, - updateVariable + updateVariable, + importVariables } diff --git a/packages/ui/src/api/chatflows.js b/packages/ui/src/api/chatflows.js index fb76d5d3912..6bdd6128599 100644 --- a/packages/ui/src/api/chatflows.js +++ b/packages/ui/src/api/chatflows.js @@ -1,6 +1,6 @@ import client from './client' -const getAllChatflows = () => client.get('/chatflows') +const getAllChatflows = () => client.get('/chatflows?type=CHATFLOW') const getAllAgentflows = () => client.get('/chatflows?type=MULTIAGENT') diff --git a/packages/ui/src/api/exportimport.js b/packages/ui/src/api/exportimport.js new file mode 100644 index 00000000000..7cab1a13997 --- /dev/null +++ b/packages/ui/src/api/exportimport.js @@ -0,0 +1,9 @@ +import client from './client' + +const exportData = (body) => client.post('/export-import/export', body) +const importData = (body) => client.post('/export-import/import', body) + +export default { + exportData, + importData +} diff --git a/packages/ui/src/assets/images/Exporting.gif b/packages/ui/src/assets/images/Exporting.gif new file mode 100644 index 00000000000..be8de05e96e Binary files /dev/null and b/packages/ui/src/assets/images/Exporting.gif differ diff --git a/packages/ui/src/layout/MainLayout/Header/ProfileSection/index.jsx b/packages/ui/src/layout/MainLayout/Header/ProfileSection/index.jsx index a742e312ab9..5caec1463cb 100644 --- a/packages/ui/src/layout/MainLayout/Header/ProfileSection/index.jsx +++ b/packages/ui/src/layout/MainLayout/Header/ProfileSection/index.jsx @@ -1,9 +1,11 @@ -import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction, MENU_OPEN, REMOVE_DIRTY } from '@/store/actions' -import { sanitizeChatflows } from '@/utils/genericHelper' +import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction, REMOVE_DIRTY } from '@/store/actions' +import { exportData, stringify } from '@/utils/exportImport' import useNotifier from '@/utils/useNotifier' import PropTypes from 'prop-types' import { useEffect, useRef, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' +import { createPortal } from 'react-dom' + // material-ui import { Avatar, @@ -18,7 +20,14 @@ import { ListItemText, Paper, Popper, - Typography + Typography, + Dialog, + DialogTitle, + DialogContent, + Stack, + FormControlLabel, + Checkbox, + DialogActions } from '@mui/material' import { useTheme } from '@mui/material/styles' @@ -33,13 +42,114 @@ import Transitions from '@/ui-component/extended/Transitions' // assets import { IconFileExport, IconFileUpload, IconInfoCircle, IconLogout, IconSettings, IconX } from '@tabler/icons-react' import './index.css' +import ExportingGIF from '@/assets/images/Exporting.gif' //API -import chatFlowsApi from '@/api/chatflows' +import exportImportApi from '@/api/exportimport' // Hooks import useApi from '@/hooks/useApi' -import { useLocation, useNavigate } from 'react-router-dom' +import { getErrorMessage } from '@/utils/errorHandler' +import { useNavigate } from 'react-router-dom' + +const dataToExport = ['Chatflows', 'Agentflows', 'Tools', 'Variables', 'Assistants'] + +const ExportDialog = ({ show, onCancel, onExport }) => { + const portalElement = document.getElementById('portal') + + const [selectedData, setSelectedData] = useState(['Chatflows', 'Agentflows', 'Tools', 'Variables', 'Assistants']) + const [isExporting, setIsExporting] = useState(false) + + useEffect(() => { + if (show) setIsExporting(false) + + return () => { + setIsExporting(false) + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [show]) + + const component = show ? ( + + + {!isExporting ? 'Select Data to Export' : 'Exporting..'} + + + {!isExporting && ( + + {dataToExport.map((data, index) => ( + { + setSelectedData( + event.target.checked + ? [...selectedData, data] + : selectedData.filter((item) => item !== data) + ) + }} + /> + } + label={data} + /> + ))} + + )} + {isExporting && ( + +
+ ExportingGIF + Exporting data might takes a while +
+
+ )} +
+ {!isExporting && ( + + + + + )} +
+ ) : null + + return createPortal(component, portalElement) +} + +ExportDialog.propTypes = { + show: PropTypes.bool, + onCancel: PropTypes.func, + onExport: PropTypes.func +} // ==============================|| PROFILE MENU ||============================== // @@ -50,12 +160,16 @@ const ProfileSection = ({ username, handleLogout }) => { const [open, setOpen] = useState(false) const [aboutDialogOpen, setAboutDialogOpen] = useState(false) + const [exportDialogOpen, setExportDialogOpen] = useState(false) const anchorRef = useRef(null) const inputRef = useRef() const navigate = useNavigate() - const location = useLocation() + + const importAllApi = useApi(exportImportApi.importData) + const exportAllApi = useApi(exportImportApi.exportData) + const prevOpen = useRef(open) // ==============================|| Snackbar ||============================== // @@ -90,7 +204,7 @@ const ProfileSection = ({ username, handleLogout }) => { } }) } - const importChatflowsApi = useApi(chatFlowsApi.importChatflows) + const fileChange = (e) => { if (!e.target.files) return @@ -101,16 +215,16 @@ const ProfileSection = ({ username, handleLogout }) => { if (!evt?.target?.result) { return } - const chatflows = JSON.parse(evt.target.result) - importChatflowsApi.request(chatflows) + const body = JSON.parse(evt.target.result) + importAllApi.request(body) } reader.readAsText(file) } - const importChatflowsSuccess = () => { + const importAllSuccess = () => { dispatch({ type: REMOVE_DIRTY }) enqueueSnackbar({ - message: `Import chatflows successful`, + message: `Import All successful`, options: { key: new Date().getTime() + Math.random(), variant: 'success', @@ -122,60 +236,75 @@ const ProfileSection = ({ username, handleLogout }) => { } }) } + + const importAll = () => { + inputRef.current.click() + } + + const onExport = (data) => { + const body = {} + if (data.includes('Chatflows')) body.chatflow = true + if (data.includes('Agentflows')) body.agentflow = true + if (data.includes('Tools')) body.tool = true + if (data.includes('Variables')) body.variable = true + if (data.includes('Assistants')) body.assistant = true + + exportAllApi.request(body) + } + useEffect(() => { - if (importChatflowsApi.error) errorFailed(`Failed to import chatflows: ${importChatflowsApi.error.response.data.message}`) - if (importChatflowsApi.data) { - importChatflowsSuccess() - // if current location is /chatflows, refresh the page - if (location.pathname === '/chatflows') navigate(0) - else { - // if not redirect to /chatflows - dispatch({ type: MENU_OPEN, id: 'chatflows' }) - navigate('/chatflows') + if (importAllApi.data) { + importAllSuccess() + navigate(0) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [importAllApi.data]) + + useEffect(() => { + if (importAllApi.error) { + let errMsg = 'Invalid Imported File' + let error = importAllApi.error + if (error?.response?.data) { + errMsg = typeof error.response.data === 'object' ? error.response.data.message : error.response.data } + errorFailed(`Failed to import: ${errMsg}`) } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [importChatflowsApi.error, importChatflowsApi.data]) - const importAllChatflows = () => { - inputRef.current.click() - } - const getAllChatflowsApi = useApi(chatFlowsApi.getAllChatflows) + }, [importAllApi.error]) - const exportChatflowsSuccess = () => { - dispatch({ type: REMOVE_DIRTY }) - enqueueSnackbar({ - message: `Export chatflows successful`, - options: { - key: new Date().getTime() + Math.random(), - variant: 'success', - action: (key) => ( - - ) + useEffect(() => { + if (exportAllApi.data) { + setExportDialogOpen(false) + try { + const dataStr = stringify(exportData(exportAllApi.data)) + //const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr) + const blob = new Blob([dataStr], { type: 'application/json' }) + const dataUri = URL.createObjectURL(blob) + + const linkElement = document.createElement('a') + linkElement.setAttribute('href', dataUri) + linkElement.setAttribute('download', exportAllApi.data.FileDefaultName) + linkElement.click() + } catch (error) { + errorFailed(`Failed to export all: ${getErrorMessage(error)}`) } - }) - } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [exportAllApi.data]) useEffect(() => { - if (getAllChatflowsApi.error) errorFailed(`Failed to export Chatflows: ${getAllChatflowsApi.error.response.data.message}`) - if (getAllChatflowsApi.data) { - const sanitizedChatflows = sanitizeChatflows(getAllChatflowsApi.data) - const dataStr = JSON.stringify({ Chatflows: sanitizedChatflows }, null, 2) - const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr) - - const exportFileDefaultName = 'AllChatflows.json' - - const linkElement = document.createElement('a') - linkElement.setAttribute('href', dataUri) - linkElement.setAttribute('download', exportFileDefaultName) - linkElement.click() - exportChatflowsSuccess() + if (exportAllApi.error) { + setExportDialogOpen(false) + let errMsg = 'Internal Server Error' + let error = exportAllApi.error + if (error?.response?.data) { + errMsg = typeof error.response.data === 'object' ? error.response.data.message : error.response.data + } + errorFailed(`Failed to export: ${errMsg}`) } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [getAllChatflowsApi.error, getAllChatflowsApi.data]) + }, [exportAllApi.error]) - const prevOpen = useRef(open) useEffect(() => { if (prevOpen.current === true && open === false) { anchorRef.current.focus() @@ -258,26 +387,26 @@ const ProfileSection = ({ username, handleLogout }) => { { - getAllChatflowsApi.request() + setExportDialogOpen(true) }} > - Export Chatflows} /> + Export} /> { - importAllChatflows() + importAll() }} > - Import Chatflows} /> + Import} /> - + { @@ -311,6 +440,7 @@ const ProfileSection = ({ username, handleLogout }) => { )} setAboutDialogOpen(false)} /> + setExportDialogOpen(false)} onExport={(data) => onExport(data)} /> ) } diff --git a/packages/ui/src/utils/errorHandler.js b/packages/ui/src/utils/errorHandler.js new file mode 100644 index 00000000000..17aae836fd1 --- /dev/null +++ b/packages/ui/src/utils/errorHandler.js @@ -0,0 +1,19 @@ +const isErrorWithMessage = (error) => { + return typeof error === 'object' && error !== null && 'message' in error && typeof error.message === 'string' +} + +const toErrorWithMessage = (maybeError) => { + if (isErrorWithMessage(maybeError)) return maybeError + + try { + return new Error(JSON.stringify(maybeError)) + } catch { + // fallback in case there's an error stringifying the maybeError + // like with circular references for example. + return new Error(String(maybeError)) + } +} + +export const getErrorMessage = (error) => { + return toErrorWithMessage(error).message +} diff --git a/packages/ui/src/utils/exportImport.js b/packages/ui/src/utils/exportImport.js new file mode 100644 index 00000000000..b160583d008 --- /dev/null +++ b/packages/ui/src/utils/exportImport.js @@ -0,0 +1,88 @@ +import { getErrorMessage } from './errorHandler' +import { generateExportFlowData } from './genericHelper' + +const sanitizeTool = (Tool) => { + try { + return Tool.map((tool) => { + return { + id: tool.id, + name: tool.name, + description: tool.description, + color: tool.color, + iconSrc: tool.iconSrc, + schema: tool.schema, + func: tool.func + } + }) + } catch (error) { + throw new Error(`exportImport.sanitizeTool ${getErrorMessage(error)}`) + } +} + +const sanitizeChatflow = (ChatFlow) => { + try { + return ChatFlow.map((chatFlow) => { + const sanitizeFlowData = generateExportFlowData(JSON.parse(chatFlow.flowData)) + return { + id: chatFlow.id, + name: chatFlow.name, + flowData: stringify(sanitizeFlowData), + type: chatFlow.type + } + }) + } catch (error) { + throw new Error(`exportImport.sanitizeChatflow ${getErrorMessage(error)}`) + } +} + +const sanitizeVariable = (Variable) => { + try { + return Variable.map((variable) => { + return { + id: variable.id, + name: variable.name, + value: variable.value, + type: variable.type + } + }) + } catch (error) { + throw new Error(`exportImport.sanitizeVariable ${getErrorMessage(error)}`) + } +} + +const sanitizeAssistant = (Assistant) => { + try { + return Assistant.map((assistant) => { + return { + id: assistant.id, + details: assistant.details, + credential: assistant.credential, + iconSrc: assistant.iconSrc + } + }) + } catch (error) { + throw new Error(`exportImport.sanitizeAssistant ${getErrorMessage(error)}`) + } +} + +export const stringify = (object) => { + try { + return JSON.stringify(object, null, 2) + } catch (error) { + throw new Error(`exportImport.stringify ${getErrorMessage(error)}`) + } +} + +export const exportData = (exportAllData) => { + try { + return { + Tool: sanitizeTool(exportAllData.Tool), + ChatFlow: sanitizeChatflow(exportAllData.ChatFlow), + AgentFlow: sanitizeChatflow(exportAllData.AgentFlow), + Variable: sanitizeVariable(exportAllData.Variable), + Assistant: sanitizeAssistant(exportAllData.Assistant) + } + } catch (error) { + throw new Error(`exportImport.exportData ${getErrorMessage(error)}`) + } +} diff --git a/packages/ui/src/utils/genericHelper.js b/packages/ui/src/utils/genericHelper.js index 17330caae22..4243f9a433c 100644 --- a/packages/ui/src/utils/genericHelper.js +++ b/packages/ui/src/utils/genericHelper.js @@ -1,5 +1,5 @@ -import moment from 'moment' import { uniq } from 'lodash' +import moment from 'moment' export const getUniqueNodeId = (nodeData, nodes) => { // Get amount of same nodes @@ -373,18 +373,6 @@ export const getFolderName = (base64ArrayStr) => { } } -export const sanitizeChatflows = (arrayChatflows) => { - const sanitizedChatflows = arrayChatflows.map((chatFlow) => { - const sanitizeFlowData = generateExportFlowData(JSON.parse(chatFlow.flowData)) - return { - id: chatFlow.id, - name: chatFlow.name, - flowData: JSON.stringify(sanitizeFlowData, null, 2) - } - }) - return sanitizedChatflows -} - export const generateExportFlowData = (flowData) => { const nodes = flowData.nodes const edges = flowData.edges diff --git a/packages/ui/src/views/assistants/AssistantDialog.jsx b/packages/ui/src/views/assistants/AssistantDialog.jsx index 68fcec704aa..bee18077573 100644 --- a/packages/ui/src/views/assistants/AssistantDialog.jsx +++ b/packages/ui/src/views/assistants/AssistantDialog.jsx @@ -165,7 +165,8 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) = useEffect(() => { if (getAssistantObjApi.error) { - let errMsg = '' + let errMsg = 'Internal Server Error' + let error = getAssistantObjApi.error if (error?.response?.data) { errMsg = typeof error.response.data === 'object' ? error.response.data.message : error.response.data }