add functionality to export and load database

This commit is contained in:
Henry 2023-05-14 20:33:43 +01:00
parent 5d5021bf10
commit 6ab1ff1062
7 changed files with 204 additions and 24 deletions

View File

@ -132,3 +132,9 @@ export interface IOverrideConfig {
name: string name: string
type: string type: string
} }
export interface IDatabaseExport {
chatmessages: IChatMessage[]
chatflows: IChatFlow[]
apikeys: ICommonObject[]
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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'} />
</>
)}
</> </>
) )
} }

View File

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