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
|
||||
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 basicAuth from 'express-basic-auth'
|
||||
|
||||
import { IChatFlow, IncomingInput, IReactFlowNode, IReactFlowObject, INodeData } from './Interface'
|
||||
import { IChatFlow, IncomingInput, IReactFlowNode, IReactFlowObject, INodeData, IDatabaseExport } from './Interface'
|
||||
import {
|
||||
getNodeModulesPackagePath,
|
||||
getStartingNodes,
|
||||
|
|
@ -22,7 +22,8 @@ import {
|
|||
compareKeys,
|
||||
mapMimeTypeToInputField,
|
||||
findAvailableConfigs,
|
||||
isSameOverrideConfig
|
||||
isSameOverrideConfig,
|
||||
replaceAllAPIKeys
|
||||
} from './utils'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { getDataSource } from './DataSource'
|
||||
|
|
@ -76,10 +77,12 @@ export class App {
|
|||
const basicAuthMiddleware = basicAuth({
|
||||
users: { [username]: password }
|
||||
})
|
||||
const whitelistURLs = ['static', 'favicon', '/api/v1/prediction/', '/api/v1/node-icon/']
|
||||
this.app.use((req, res, next) =>
|
||||
whitelistURLs.some((url) => req.url.includes(url)) || req.url === '/' ? next() : basicAuthMiddleware(req, res, next)
|
||||
)
|
||||
const whitelistURLs = ['/api/v1/prediction/', '/api/v1/node-icon/']
|
||||
this.app.use((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')}/` })
|
||||
|
|
@ -233,6 +236,57 @@ export class App {
|
|||
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
|
||||
// ----------------------------------------
|
||||
|
|
|
|||
|
|
@ -539,6 +539,19 @@ export const deleteAPIKey = async (keyIdToDelete: string): Promise<ICommonObject
|
|||
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
|
||||
* @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 PropTypes from 'prop-types'
|
||||
|
||||
import { useSelector } from 'react-redux'
|
||||
import { useSelector, useDispatch } from 'react-redux'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
// material-ui
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
|
|
@ -27,9 +26,15 @@ import PerfectScrollbar from 'react-perfect-scrollbar'
|
|||
// project imports
|
||||
import MainCard from 'ui-component/cards/MainCard'
|
||||
import Transitions from 'ui-component/extended/Transitions'
|
||||
import { BackdropLoader } from 'ui-component/loading/BackdropLoader'
|
||||
|
||||
// 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'
|
||||
|
||||
|
|
@ -37,12 +42,16 @@ import './index.css'
|
|||
|
||||
const ProfileSection = ({ username, handleLogout }) => {
|
||||
const theme = useTheme()
|
||||
const dispatch = useDispatch()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const customization = useSelector((state) => state.customization)
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const anchorRef = useRef(null)
|
||||
const uploadRef = useRef(null)
|
||||
|
||||
const handleClose = (event) => {
|
||||
if (anchorRef.current && anchorRef.current.contains(event.target)) {
|
||||
|
|
@ -55,6 +64,56 @@ const ProfileSection = ({ username, handleLogout }) => {
|
|||
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)
|
||||
useEffect(() => {
|
||||
if (prevOpen.current === true && open === false) {
|
||||
|
|
@ -109,11 +168,13 @@ const ProfileSection = ({ username, handleLogout }) => {
|
|||
<Paper>
|
||||
<ClickAwayListener onClickAway={handleClose}>
|
||||
<MainCard border={false} elevation={16} content={false} boxShadow shadow={theme.shadows[16]}>
|
||||
{username && (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography component='span' variant='h4'>
|
||||
{username}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
<PerfectScrollbar style={{ height: '100%', maxHeight: 'calc(100vh - 250px)', overflowX: 'hidden' }}>
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Divider />
|
||||
|
|
@ -133,6 +194,28 @@ const ProfileSection = ({ username, handleLogout }) => {
|
|||
}
|
||||
}}
|
||||
>
|
||||
<ListItemButton
|
||||
sx={{ borderRadius: `${customization.borderRadius}px` }}
|
||||
onClick={() => {
|
||||
setOpen(false)
|
||||
uploadRef.current.click()
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<IconFileDownload stroke={1.5} size='1.3rem' />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={<Typography variant='body2'>Load Database</Typography>} />
|
||||
</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}
|
||||
|
|
@ -142,6 +225,7 @@ const ProfileSection = ({ username, handleLogout }) => {
|
|||
</ListItemIcon>
|
||||
<ListItemText primary={<Typography variant='body2'>Logout</Typography>} />
|
||||
</ListItemButton>
|
||||
)}
|
||||
</List>
|
||||
</Box>
|
||||
</PerfectScrollbar>
|
||||
|
|
@ -151,6 +235,8 @@ const ProfileSection = ({ username, handleLogout }) => {
|
|||
</Transitions>
|
||||
)}
|
||||
</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 sx={{ flexGrow: 1 }} />
|
||||
<MaterialUISwitch checked={isDark} onChange={changeDarkMode} />
|
||||
{localStorage.getItem('username') && localStorage.getItem('password') && (
|
||||
<>
|
||||
<Box sx={{ ml: 2 }}></Box>
|
||||
<ProfileSection handleLogout={signOutClicked} username={localStorage.getItem('username') ?? 'user'} />
|
||||
</>
|
||||
)}
|
||||
<ProfileSection handleLogout={signOutClicked} username={localStorage.getItem('username') ?? ''} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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