mirror of
https://github.com/farcasclaudiu/Flowise.git
synced 2026-06-28 15:00:57 +03:00
Feature/export import stage 2 (#3063)
* add export all function * modify exportAll to reuse existing code from other services * modify routes of export-import * add exportAll function into UI * add errorhandler * add importAll Function into UI for ChatFlow * modify importAll Function to import tools * remove appServer variable * modify exportAll to exportData for new requirement in backend * chore modify type camelCase to PascalCase in exportImportService * add import export for variables, assistants, and checkboxes for UI --------- Co-authored-by: Henry <hzj94@hotmail.com>
This commit is contained in:
@@ -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 ? (
|
||||
<Dialog
|
||||
onClose={!isExporting ? onCancel : undefined}
|
||||
open={show}
|
||||
fullWidth
|
||||
maxWidth='sm'
|
||||
aria-labelledby='export-dialog-title'
|
||||
aria-describedby='export-dialog-description'
|
||||
>
|
||||
<DialogTitle sx={{ fontSize: '1rem' }} id='export-dialog-title'>
|
||||
{!isExporting ? 'Select Data to Export' : 'Exporting..'}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
{!isExporting && (
|
||||
<Stack direction='row' sx={{ gap: 1, flexWrap: 'wrap' }}>
|
||||
{dataToExport.map((data, index) => (
|
||||
<FormControlLabel
|
||||
key={index}
|
||||
size='small'
|
||||
control={
|
||||
<Checkbox
|
||||
color='success'
|
||||
checked={selectedData.includes(data)}
|
||||
onChange={(event) => {
|
||||
setSelectedData(
|
||||
event.target.checked
|
||||
? [...selectedData, data]
|
||||
: selectedData.filter((item) => item !== data)
|
||||
)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label={data}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
{isExporting && (
|
||||
<Box sx={{ height: 'auto', display: 'flex', justifyContent: 'center', mb: 3 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<img
|
||||
style={{
|
||||
objectFit: 'cover',
|
||||
height: 'auto',
|
||||
width: 'auto'
|
||||
}}
|
||||
src={ExportingGIF}
|
||||
alt='ExportingGIF'
|
||||
/>
|
||||
<span>Exporting data might takes a while</span>
|
||||
</div>
|
||||
</Box>
|
||||
)}
|
||||
</DialogContent>
|
||||
{!isExporting && (
|
||||
<DialogActions>
|
||||
<Button onClick={onCancel}>Cancel</Button>
|
||||
<Button
|
||||
disabled={selectedData.length === 0}
|
||||
variant='contained'
|
||||
onClick={() => {
|
||||
setIsExporting(true)
|
||||
onExport(selectedData)
|
||||
}}
|
||||
>
|
||||
Export
|
||||
</Button>
|
||||
</DialogActions>
|
||||
)}
|
||||
</Dialog>
|
||||
) : 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 }) => {
|
||||
}
|
||||
})
|
||||
}
|
||||
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')
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [importChatflowsApi.error, importChatflowsApi.data])
|
||||
const importAllChatflows = () => {
|
||||
|
||||
const importAll = () => {
|
||||
inputRef.current.click()
|
||||
}
|
||||
const getAllChatflowsApi = useApi(chatFlowsApi.getAllChatflows)
|
||||
|
||||
const exportChatflowsSuccess = () => {
|
||||
dispatch({ type: REMOVE_DIRTY })
|
||||
enqueueSnackbar({
|
||||
message: `Export chatflows successful`,
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'success',
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
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 (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 (importAllApi.data) {
|
||||
importAllSuccess()
|
||||
navigate(0)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [getAllChatflowsApi.error, getAllChatflowsApi.data])
|
||||
}, [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
|
||||
}, [importAllApi.error])
|
||||
|
||||
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 (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
|
||||
}, [exportAllApi.error])
|
||||
|
||||
const prevOpen = useRef(open)
|
||||
useEffect(() => {
|
||||
if (prevOpen.current === true && open === false) {
|
||||
anchorRef.current.focus()
|
||||
@@ -258,26 +387,26 @@ const ProfileSection = ({ username, handleLogout }) => {
|
||||
<ListItemButton
|
||||
sx={{ borderRadius: `${customization.borderRadius}px` }}
|
||||
onClick={() => {
|
||||
getAllChatflowsApi.request()
|
||||
setExportDialogOpen(true)
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<IconFileExport stroke={1.5} size='1.3rem' />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={<Typography variant='body2'>Export Chatflows</Typography>} />
|
||||
<ListItemText primary={<Typography variant='body2'>Export</Typography>} />
|
||||
</ListItemButton>
|
||||
<ListItemButton
|
||||
sx={{ borderRadius: `${customization.borderRadius}px` }}
|
||||
onClick={() => {
|
||||
importAllChatflows()
|
||||
importAll()
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<IconFileUpload stroke={1.5} size='1.3rem' />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={<Typography variant='body2'>Import Chatflows</Typography>} />
|
||||
<ListItemText primary={<Typography variant='body2'>Import</Typography>} />
|
||||
</ListItemButton>
|
||||
<input ref={inputRef} type='file' hidden onChange={fileChange} />
|
||||
<input ref={inputRef} type='file' hidden onChange={fileChange} accept='.json' />
|
||||
<ListItemButton
|
||||
sx={{ borderRadius: `${customization.borderRadius}px` }}
|
||||
onClick={() => {
|
||||
@@ -311,6 +440,7 @@ const ProfileSection = ({ username, handleLogout }) => {
|
||||
)}
|
||||
</Popper>
|
||||
<AboutDialog show={aboutDialogOpen} onCancel={() => setAboutDialogOpen(false)} />
|
||||
<ExportDialog show={exportDialogOpen} onCancel={() => setExportDialogOpen(false)} onExport={(data) => onExport(data)} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user