Merge pull request #105 from FlowiseAI/feature/Export-Load-Flows
Feature/Export load flows
This commit is contained in:
commit
80c1768e5c
|
|
@ -132,3 +132,9 @@ export interface IOverrideConfig {
|
||||||
name: string
|
name: string
|
||||||
type: string
|
type: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IDatabaseExport {
|
||||||
|
chatmessages: IChatMessage[]
|
||||||
|
chatflows: IChatFlow[]
|
||||||
|
apikeys: ICommonObject[]
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import http from 'http'
|
||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
import basicAuth from 'express-basic-auth'
|
import basicAuth from 'express-basic-auth'
|
||||||
|
|
||||||
import { IChatFlow, IncomingInput, IReactFlowNode, IReactFlowObject, INodeData } from './Interface'
|
import { IChatFlow, IncomingInput, IReactFlowNode, IReactFlowObject, INodeData, IDatabaseExport } from './Interface'
|
||||||
import {
|
import {
|
||||||
getNodeModulesPackagePath,
|
getNodeModulesPackagePath,
|
||||||
getStartingNodes,
|
getStartingNodes,
|
||||||
|
|
@ -22,7 +22,8 @@ import {
|
||||||
compareKeys,
|
compareKeys,
|
||||||
mapMimeTypeToInputField,
|
mapMimeTypeToInputField,
|
||||||
findAvailableConfigs,
|
findAvailableConfigs,
|
||||||
isSameOverrideConfig
|
isSameOverrideConfig,
|
||||||
|
replaceAllAPIKeys
|
||||||
} from './utils'
|
} from './utils'
|
||||||
import { cloneDeep } from 'lodash'
|
import { cloneDeep } from 'lodash'
|
||||||
import { getDataSource } from './DataSource'
|
import { getDataSource } from './DataSource'
|
||||||
|
|
@ -76,10 +77,12 @@ export class App {
|
||||||
const basicAuthMiddleware = basicAuth({
|
const basicAuthMiddleware = basicAuth({
|
||||||
users: { [username]: password }
|
users: { [username]: password }
|
||||||
})
|
})
|
||||||
const whitelistURLs = ['static', 'favicon', '/api/v1/prediction/', '/api/v1/node-icon/']
|
const whitelistURLs = ['/api/v1/prediction/', '/api/v1/node-icon/']
|
||||||
this.app.use((req, res, next) =>
|
this.app.use((req, res, next) => {
|
||||||
whitelistURLs.some((url) => req.url.includes(url)) || req.url === '/' ? next() : basicAuthMiddleware(req, res, next)
|
if (req.url.includes('/api/v1/')) {
|
||||||
)
|
whitelistURLs.some((url) => req.url.includes(url)) ? next() : basicAuthMiddleware(req, res, next)
|
||||||
|
} else next()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const upload = multer({ dest: `${path.join(__dirname, '..', 'uploads')}/` })
|
const upload = multer({ dest: `${path.join(__dirname, '..', 'uploads')}/` })
|
||||||
|
|
@ -233,6 +236,57 @@ export class App {
|
||||||
return res.json(availableConfigs)
|
return res.json(availableConfigs)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ----------------------------------------
|
||||||
|
// Export Load Chatflow & ChatMessage & Apikeys
|
||||||
|
// ----------------------------------------
|
||||||
|
|
||||||
|
this.app.get('/api/v1/database/export', async (req: Request, res: Response) => {
|
||||||
|
const chatmessages = await this.AppDataSource.getRepository(ChatMessage).find()
|
||||||
|
const chatflows = await this.AppDataSource.getRepository(ChatFlow).find()
|
||||||
|
const apikeys = await getAPIKeys()
|
||||||
|
const result: IDatabaseExport = {
|
||||||
|
chatmessages,
|
||||||
|
chatflows,
|
||||||
|
apikeys
|
||||||
|
}
|
||||||
|
return res.json(result)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.app.post('/api/v1/database/load', async (req: Request, res: Response) => {
|
||||||
|
const databaseItems: IDatabaseExport = req.body
|
||||||
|
|
||||||
|
await this.AppDataSource.getRepository(ChatFlow).delete({})
|
||||||
|
await this.AppDataSource.getRepository(ChatMessage).delete({})
|
||||||
|
|
||||||
|
let error = ''
|
||||||
|
|
||||||
|
// Get a new query runner instance
|
||||||
|
const queryRunner = this.AppDataSource.createQueryRunner()
|
||||||
|
|
||||||
|
// Start a new transaction
|
||||||
|
await queryRunner.startTransaction()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const chatflows: ChatFlow[] = databaseItems.chatflows
|
||||||
|
const chatmessages: ChatMessage[] = databaseItems.chatmessages
|
||||||
|
|
||||||
|
await queryRunner.manager.insert(ChatFlow, chatflows)
|
||||||
|
await queryRunner.manager.insert(ChatMessage, chatmessages)
|
||||||
|
|
||||||
|
await queryRunner.commitTransaction()
|
||||||
|
} catch (err: any) {
|
||||||
|
error = err?.message ?? 'Error loading database'
|
||||||
|
await queryRunner.rollbackTransaction()
|
||||||
|
} finally {
|
||||||
|
await queryRunner.release()
|
||||||
|
}
|
||||||
|
|
||||||
|
await replaceAllAPIKeys(databaseItems.apikeys)
|
||||||
|
|
||||||
|
if (error) return res.status(500).send(error)
|
||||||
|
return res.status(201).send('OK')
|
||||||
|
})
|
||||||
|
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
// Prediction
|
// Prediction
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -539,6 +539,19 @@ export const deleteAPIKey = async (keyIdToDelete: string): Promise<ICommonObject
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace all api keys
|
||||||
|
* @param {ICommonObject[]} content
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
export const replaceAllAPIKeys = async (content: ICommonObject[]): Promise<void> => {
|
||||||
|
try {
|
||||||
|
await fs.promises.writeFile(getAPIKeyPath(), JSON.stringify(content), 'utf8')
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map MimeType to InputField
|
* Map MimeType to InputField
|
||||||
* @param {string} mimeType
|
* @param {string} mimeType
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
import client from './client'
|
||||||
|
|
||||||
|
const getExportDatabase = () => client.get('/database/export')
|
||||||
|
const createLoadDatabase = (body) => client.post('/database/load', body)
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getExportDatabase,
|
||||||
|
createLoadDatabase
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import { useState, useRef, useEffect } from 'react'
|
import { useState, useRef, useEffect } from 'react'
|
||||||
|
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
import { useSelector, useDispatch } from 'react-redux'
|
||||||
import { useSelector } from 'react-redux'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
// material-ui
|
// material-ui
|
||||||
import { useTheme } from '@mui/material/styles'
|
import { useTheme } from '@mui/material/styles'
|
||||||
|
|
@ -27,9 +26,15 @@ import PerfectScrollbar from 'react-perfect-scrollbar'
|
||||||
// project imports
|
// project imports
|
||||||
import MainCard from 'ui-component/cards/MainCard'
|
import MainCard from 'ui-component/cards/MainCard'
|
||||||
import Transitions from 'ui-component/extended/Transitions'
|
import Transitions from 'ui-component/extended/Transitions'
|
||||||
|
import { BackdropLoader } from 'ui-component/loading/BackdropLoader'
|
||||||
|
|
||||||
// assets
|
// assets
|
||||||
import { IconLogout, IconSettings } from '@tabler/icons'
|
import { IconLogout, IconSettings, IconFileExport, IconFileDownload } from '@tabler/icons'
|
||||||
|
|
||||||
|
// API
|
||||||
|
import databaseApi from 'api/database'
|
||||||
|
|
||||||
|
import { SET_MENU } from 'store/actions'
|
||||||
|
|
||||||
import './index.css'
|
import './index.css'
|
||||||
|
|
||||||
|
|
@ -37,12 +42,16 @@ import './index.css'
|
||||||
|
|
||||||
const ProfileSection = ({ username, handleLogout }) => {
|
const ProfileSection = ({ username, handleLogout }) => {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const customization = useSelector((state) => state.customization)
|
const customization = useSelector((state) => state.customization)
|
||||||
|
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
const anchorRef = useRef(null)
|
const anchorRef = useRef(null)
|
||||||
|
const uploadRef = useRef(null)
|
||||||
|
|
||||||
const handleClose = (event) => {
|
const handleClose = (event) => {
|
||||||
if (anchorRef.current && anchorRef.current.contains(event.target)) {
|
if (anchorRef.current && anchorRef.current.contains(event.target)) {
|
||||||
|
|
@ -55,6 +64,56 @@ const ProfileSection = ({ username, handleLogout }) => {
|
||||||
setOpen((prevOpen) => !prevOpen)
|
setOpen((prevOpen) => !prevOpen)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleExportDB = async () => {
|
||||||
|
setOpen(false)
|
||||||
|
try {
|
||||||
|
const response = await databaseApi.getExportDatabase()
|
||||||
|
const exportItems = response.data
|
||||||
|
let dataStr = JSON.stringify(exportItems)
|
||||||
|
let dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr)
|
||||||
|
|
||||||
|
let exportFileDefaultName = `DB.json`
|
||||||
|
|
||||||
|
let linkElement = document.createElement('a')
|
||||||
|
linkElement.setAttribute('href', dataUri)
|
||||||
|
linkElement.setAttribute('download', exportFileDefaultName)
|
||||||
|
linkElement.click()
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFileUpload = (e) => {
|
||||||
|
if (!e.target.files) return
|
||||||
|
|
||||||
|
const file = e.target.files[0]
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = async (evt) => {
|
||||||
|
if (!evt?.target?.result) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const { result } = evt.target
|
||||||
|
|
||||||
|
if (result.includes(`"chatmessages":[`) && result.includes(`"chatflows":[`) && result.includes(`"apikeys":[`)) {
|
||||||
|
dispatch({ type: SET_MENU, opened: false })
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await databaseApi.createLoadDatabase(JSON.parse(result))
|
||||||
|
setLoading(false)
|
||||||
|
navigate('/', { replace: true })
|
||||||
|
navigate(0)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert('Incorrect Flowise Database Format')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.readAsText(file)
|
||||||
|
}
|
||||||
|
|
||||||
const prevOpen = useRef(open)
|
const prevOpen = useRef(open)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (prevOpen.current === true && open === false) {
|
if (prevOpen.current === true && open === false) {
|
||||||
|
|
@ -109,11 +168,13 @@ const ProfileSection = ({ username, handleLogout }) => {
|
||||||
<Paper>
|
<Paper>
|
||||||
<ClickAwayListener onClickAway={handleClose}>
|
<ClickAwayListener onClickAway={handleClose}>
|
||||||
<MainCard border={false} elevation={16} content={false} boxShadow shadow={theme.shadows[16]}>
|
<MainCard border={false} elevation={16} content={false} boxShadow shadow={theme.shadows[16]}>
|
||||||
<Box sx={{ p: 2 }}>
|
{username && (
|
||||||
<Typography component='span' variant='h4'>
|
<Box sx={{ p: 2 }}>
|
||||||
{username}
|
<Typography component='span' variant='h4'>
|
||||||
</Typography>
|
{username}
|
||||||
</Box>
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
<PerfectScrollbar style={{ height: '100%', maxHeight: 'calc(100vh - 250px)', overflowX: 'hidden' }}>
|
<PerfectScrollbar style={{ height: '100%', maxHeight: 'calc(100vh - 250px)', overflowX: 'hidden' }}>
|
||||||
<Box sx={{ p: 2 }}>
|
<Box sx={{ p: 2 }}>
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
@ -135,13 +196,36 @@ const ProfileSection = ({ username, handleLogout }) => {
|
||||||
>
|
>
|
||||||
<ListItemButton
|
<ListItemButton
|
||||||
sx={{ borderRadius: `${customization.borderRadius}px` }}
|
sx={{ borderRadius: `${customization.borderRadius}px` }}
|
||||||
onClick={handleLogout}
|
onClick={() => {
|
||||||
|
setOpen(false)
|
||||||
|
uploadRef.current.click()
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<IconLogout stroke={1.5} size='1.3rem' />
|
<IconFileDownload stroke={1.5} size='1.3rem' />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText primary={<Typography variant='body2'>Logout</Typography>} />
|
<ListItemText primary={<Typography variant='body2'>Load Database</Typography>} />
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
|
<ListItemButton
|
||||||
|
sx={{ borderRadius: `${customization.borderRadius}px` }}
|
||||||
|
onClick={handleExportDB}
|
||||||
|
>
|
||||||
|
<ListItemIcon>
|
||||||
|
<IconFileExport stroke={1.5} size='1.3rem' />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={<Typography variant='body2'>Export Database</Typography>} />
|
||||||
|
</ListItemButton>
|
||||||
|
{localStorage.getItem('username') && localStorage.getItem('password') && (
|
||||||
|
<ListItemButton
|
||||||
|
sx={{ borderRadius: `${customization.borderRadius}px` }}
|
||||||
|
onClick={handleLogout}
|
||||||
|
>
|
||||||
|
<ListItemIcon>
|
||||||
|
<IconLogout stroke={1.5} size='1.3rem' />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={<Typography variant='body2'>Logout</Typography>} />
|
||||||
|
</ListItemButton>
|
||||||
|
)}
|
||||||
</List>
|
</List>
|
||||||
</Box>
|
</Box>
|
||||||
</PerfectScrollbar>
|
</PerfectScrollbar>
|
||||||
|
|
@ -151,6 +235,8 @@ const ProfileSection = ({ username, handleLogout }) => {
|
||||||
</Transitions>
|
</Transitions>
|
||||||
)}
|
)}
|
||||||
</Popper>
|
</Popper>
|
||||||
|
<input ref={uploadRef} type='file' hidden accept='.json' onChange={(e) => handleFileUpload(e)} />
|
||||||
|
<BackdropLoader open={loading} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -127,12 +127,8 @@ const Header = ({ handleLeftDrawerToggle }) => {
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ flexGrow: 1 }} />
|
<Box sx={{ flexGrow: 1 }} />
|
||||||
<MaterialUISwitch checked={isDark} onChange={changeDarkMode} />
|
<MaterialUISwitch checked={isDark} onChange={changeDarkMode} />
|
||||||
{localStorage.getItem('username') && localStorage.getItem('password') && (
|
<Box sx={{ ml: 2 }}></Box>
|
||||||
<>
|
<ProfileSection handleLogout={signOutClicked} username={localStorage.getItem('username') ?? ''} />
|
||||||
<Box sx={{ ml: 2 }}></Box>
|
|
||||||
<ProfileSection handleLogout={signOutClicked} username={localStorage.getItem('username') ?? 'user'} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { Backdrop, CircularProgress } from '@mui/material'
|
||||||
|
|
||||||
|
export const BackdropLoader = ({ open }) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Backdrop sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }} open={open}>
|
||||||
|
<CircularProgress color='inherit' />
|
||||||
|
</Backdrop>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
BackdropLoader.propTypes = {
|
||||||
|
open: PropTypes.bool
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue