Skip to content

Commit

Permalink
add functionality to export and load database
Browse files Browse the repository at this point in the history
  • Loading branch information
HenryHengZJ committed May 14, 2023
1 parent 5d5021b commit 6ab1ff1
Show file tree
Hide file tree
Showing 7 changed files with 204 additions and 24 deletions.
6 changes: 6 additions & 0 deletions packages/server/src/Interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,9 @@ export interface IOverrideConfig {
name: string
type: string
}

export interface IDatabaseExport {
chatmessages: IChatMessage[]
chatflows: IChatFlow[]
apikeys: ICommonObject[]
}
66 changes: 60 additions & 6 deletions packages/server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -22,7 +22,8 @@ import {
compareKeys,
mapMimeTypeToInputField,
findAvailableConfigs,
isSameOverrideConfig
isSameOverrideConfig,
replaceAllAPIKeys
} from './utils'
import { cloneDeep } from 'lodash'
import { getDataSource } from './DataSource'
Expand Down Expand Up @@ -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')}/` })
Expand Down Expand Up @@ -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
// ----------------------------------------
Expand Down
13 changes: 13 additions & 0 deletions packages/server/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions packages/ui/src/api/database.js
Original file line number Diff line number Diff line change
@@ -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
}
110 changes: 98 additions & 12 deletions packages/ui/src/layout/MainLayout/Header/ProfileSection/index.js
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -27,22 +26,32 @@ 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'

// ==============================|| PROFILE MENU ||============================== //

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)) {
Expand All @@ -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) {
Expand Down Expand Up @@ -109,11 +168,13 @@ const ProfileSection = ({ username, handleLogout }) => {
<Paper>
<ClickAwayListener onClickAway={handleClose}>
<MainCard border={false} elevation={16} content={false} boxShadow shadow={theme.shadows[16]}>
<Box sx={{ p: 2 }}>
<Typography component='span' variant='h4'>
{username}
</Typography>
</Box>
{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 />
Expand All @@ -135,13 +196,36 @@ const ProfileSection = ({ username, handleLogout }) => {
>
<ListItemButton
sx={{ borderRadius: `${customization.borderRadius}px` }}
onClick={handleLogout}
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>
<IconLogout stroke={1.5} size='1.3rem' />
<IconFileExport stroke={1.5} size='1.3rem' />
</ListItemIcon>
<ListItemText primary={<Typography variant='body2'>Logout</Typography>} />
<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>
</Box>
</PerfectScrollbar>
Expand All @@ -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} />
</>
)
}
Expand Down
8 changes: 2 additions & 6 deletions packages/ui/src/layout/MainLayout/Header/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'} />
</>
)}
<Box sx={{ ml: 2 }}></Box>
<ProfileSection handleLogout={signOutClicked} username={localStorage.getItem('username') ?? ''} />
</>
)
}
Expand Down
16 changes: 16 additions & 0 deletions packages/ui/src/ui-component/loading/BackdropLoader.js
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit 6ab1ff1

Please sign in to comment.