Merge pull request #1422 from FlowiseAI/feature/env-vars

Feature/Env Variables
This commit is contained in:
Henry Heng
2023-12-27 21:44:17 +00:00
committed by GitHub
36 changed files with 2074 additions and 144 deletions
+16
View File
@@ -0,0 +1,16 @@
import client from './client'
const getAllVariables = () => client.get('/variables')
const createVariable = (body) => client.post(`/variables`, body)
const updateVariable = (id, body) => client.put(`/variables/${id}`, body)
const deleteVariable = (id) => client.delete(`/variables/${id}`)
export default {
getAllVariables,
createVariable,
updateVariable,
deleteVariable
}
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

+10 -2
View File
@@ -1,8 +1,8 @@
// assets
import { IconHierarchy, IconBuildingStore, IconKey, IconTool, IconLock, IconRobot } from '@tabler/icons'
import { IconHierarchy, IconBuildingStore, IconKey, IconTool, IconLock, IconRobot, IconVariable } from '@tabler/icons'
// constant
const icons = { IconHierarchy, IconBuildingStore, IconKey, IconTool, IconLock, IconRobot }
const icons = { IconHierarchy, IconBuildingStore, IconKey, IconTool, IconLock, IconRobot, IconVariable }
// ==============================|| DASHBOARD MENU ITEMS ||============================== //
@@ -51,6 +51,14 @@ const dashboard = {
icon: icons.IconLock,
breadcrumbs: true
},
{
id: 'variables',
title: 'Variables',
type: 'item',
url: '/variables',
icon: icons.IconVariable,
breadcrumbs: true
},
{
id: 'apikey',
title: 'API Keys',
+7
View File
@@ -22,6 +22,9 @@ const Assistants = Loadable(lazy(() => import('views/assistants')))
// credentials routing
const Credentials = Loadable(lazy(() => import('views/credentials')))
// variables routing
const Variables = Loadable(lazy(() => import('views/variables')))
// ==============================|| MAIN ROUTING ||============================== //
const MainRoutes = {
@@ -55,6 +58,10 @@ const MainRoutes = {
{
path: '/credentials',
element: <Credentials />
},
{
path: '/variables',
element: <Variables />
}
]
}
@@ -369,7 +369,12 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA
{inputParam?.acceptVariable && (
<>
<Button
sx={{ borderRadius: 25, width: '100%', mb: 0, mt: 2 }}
sx={{
borderRadius: 25,
width: '100%',
mb: 0,
mt: 2
}}
variant='outlined'
disabled={disabled}
onClick={() => onEditJSONClicked(data.inputs[inputParam.name] ?? '', inputParam)}
@@ -0,0 +1,68 @@
import { createPortal } from 'react-dom'
import PropTypes from 'prop-types'
import { Dialog, DialogContent, DialogTitle } from '@mui/material'
const HowToUseFunctionDialog = ({ show, onCancel }) => {
const portalElement = document.getElementById('portal')
const component = show ? (
<Dialog
onClose={onCancel}
open={show}
fullWidth
maxWidth='sm'
aria-labelledby='alert-dialog-title'
aria-describedby='alert-dialog-description'
>
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
How To Use Function
</DialogTitle>
<DialogContent>
<ul>
<li style={{ marginTop: 10 }}>You can use any libraries imported in Flowise</li>
<li style={{ marginTop: 10 }}>
You can use properties specified in Output Schema as variables with prefix $:
<ul style={{ marginTop: 10 }}>
<li>
Property = <code>userid</code>
</li>
<li>
Variable = <code>$userid</code>
</li>
</ul>
</li>
<li style={{ marginTop: 10 }}>
You can get default flow config:
<ul style={{ marginTop: 10 }}>
<li>
<code>$flow.sessionId</code>
</li>
<li>
<code>$flow.chatId</code>
</li>
<li>
<code>$flow.chatflowId</code>
</li>
<li>
<code>$flow.input</code>
</li>
</ul>
</li>
<li style={{ marginTop: 10 }}>
You can get custom variables:&nbsp;<code>{`$vars.<variable-name>`}</code>
</li>
<li style={{ marginTop: 10 }}>Must return a string value at the end of function</li>
</ul>
</DialogContent>
</Dialog>
) : null
return createPortal(component, portalElement)
}
HowToUseFunctionDialog.propTypes = {
show: PropTypes.bool,
onCancel: PropTypes.func
}
export default HowToUseFunctionDialog
+13
View File
@@ -13,6 +13,7 @@ import { GridActionsCellItem } from '@mui/x-data-grid'
import DeleteIcon from '@mui/icons-material/Delete'
import ConfirmDialog from 'ui-component/dialog/ConfirmDialog'
import { CodeEditor } from 'ui-component/editor/CodeEditor'
import HowToUseFunctionDialog from './HowToUseFunctionDialog'
// Icons
import { IconX, IconFileExport } from '@tabler/icons'
@@ -32,6 +33,8 @@ import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions'
const exampleAPIFunc = `/*
* You can use any libraries imported in Flowise
* You can use properties specified in Output Schema as variables. Ex: Property = userid, Variable = $userid
* You can get default flow config: $flow.sessionId, $flow.chatId, $flow.chatflowId, $flow.input
* You can get custom variables: $vars.<variable-name>
* Must return a string value at the end of function
*/
@@ -74,6 +77,7 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm }) =
const [toolIcon, setToolIcon] = useState('')
const [toolSchema, setToolSchema] = useState([])
const [toolFunc, setToolFunc] = useState('')
const [showHowToDialog, setShowHowToDialog] = useState(false)
const deleteItem = useCallback(
(id) => () => {
@@ -482,6 +486,14 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm }) =
/>
</Typography>
</Stack>
<Button
style={{ marginBottom: 10, marginRight: 10 }}
color='secondary'
variant='outlined'
onClick={() => setShowHowToDialog(true)}
>
How to use Function
</Button>
{dialogProps.type !== 'TEMPLATE' && (
<Button style={{ marginBottom: 10 }} variant='outlined' onClick={() => setToolFunc(exampleAPIFunc)}>
See Example
@@ -519,6 +531,7 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm }) =
)}
</DialogActions>
<ConfirmDialog />
<HowToUseFunctionDialog show={showHowToDialog} onCancel={() => setShowHowToDialog(false)} />
</Dialog>
) : null
@@ -0,0 +1,285 @@
import { createPortal } from 'react-dom'
import PropTypes from 'prop-types'
import { useState, useEffect } from 'react'
import { useDispatch } from 'react-redux'
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from 'store/actions'
// Material
import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Box, Typography, OutlinedInput } from '@mui/material'
// Project imports
import { StyledButton } from 'ui-component/button/StyledButton'
import ConfirmDialog from 'ui-component/dialog/ConfirmDialog'
// Icons
import { IconX, IconVariable } from '@tabler/icons'
// API
import variablesApi from 'api/variables'
// Hooks
// utils
import useNotifier from 'utils/useNotifier'
// const
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions'
import { Dropdown } from '../../ui-component/dropdown/Dropdown'
const variableTypes = [
{
label: 'Static',
name: 'static',
description: 'Variable value will be read from the value entered below'
},
{
label: 'Runtime',
name: 'runtime',
description: 'Variable value will be read from .env file'
}
]
const AddEditVariableDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
const portalElement = document.getElementById('portal')
const dispatch = useDispatch()
// ==============================|| Snackbar ||============================== //
useNotifier()
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
const [variableName, setVariableName] = useState('')
const [variableValue, setVariableValue] = useState('')
const [variableType, setVariableType] = useState('static')
const [dialogType, setDialogType] = useState('ADD')
const [variable, setVariable] = useState({})
useEffect(() => {
if (dialogProps.type === 'EDIT' && dialogProps.data) {
setVariableName(dialogProps.data.name)
setVariableValue(dialogProps.data.value)
setVariableType(dialogProps.data.type)
setDialogType('EDIT')
setVariable(dialogProps.data)
} else if (dialogProps.type === 'ADD') {
setVariableName('')
setVariableValue('')
setVariableType('static')
setDialogType('ADD')
setVariable({})
}
return () => {
setVariableName('')
setVariableValue('')
setVariableType('static')
setDialogType('ADD')
setVariable({})
}
}, [dialogProps])
useEffect(() => {
if (show) dispatch({ type: SHOW_CANVAS_DIALOG })
else dispatch({ type: HIDE_CANVAS_DIALOG })
return () => dispatch({ type: HIDE_CANVAS_DIALOG })
}, [show, dispatch])
const addNewVariable = async () => {
try {
const obj = {
name: variableName,
value: variableValue,
type: variableType
}
const createResp = await variablesApi.createVariable(obj)
if (createResp.data) {
enqueueSnackbar({
message: 'New Variable added',
options: {
key: new Date().getTime() + Math.random(),
variant: 'success',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
onConfirm(createResp.data.id)
}
} catch (err) {
const errorData = typeof err === 'string' ? err : err.response?.data || `${err.response?.status}: ${err.response?.statusText}`
enqueueSnackbar({
message: `Failed to add new Variable: ${errorData}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
persist: true,
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
onCancel()
}
}
const saveVariable = async () => {
try {
const saveObj = {
name: variableName,
value: variableValue,
type: variableType
}
const saveResp = await variablesApi.updateVariable(variable.id, saveObj)
if (saveResp.data) {
enqueueSnackbar({
message: 'Variable saved',
options: {
key: new Date().getTime() + Math.random(),
variant: 'success',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
onConfirm(saveResp.data.id)
}
} catch (error) {
const errorData = error.response?.data || `${error.response?.status}: ${error.response?.statusText}`
enqueueSnackbar({
message: `Failed to save Variable: ${errorData}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
persist: true,
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
onCancel()
}
}
const component = show ? (
<Dialog
fullWidth
maxWidth='sm'
open={show}
onClose={onCancel}
aria-labelledby='alert-dialog-title'
aria-describedby='alert-dialog-description'
>
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<div
style={{
width: 50,
height: 50,
marginRight: 10,
borderRadius: '50%',
backgroundColor: 'white'
}}
>
<IconVariable
style={{
width: '100%',
height: '100%',
padding: 7,
borderRadius: '50%',
objectFit: 'contain'
}}
/>
</div>
{dialogProps.type === 'ADD' ? 'Add Variable' : 'Edit Variable'}
</div>
</DialogTitle>
<DialogContent>
<Box sx={{ p: 2 }}>
<div style={{ display: 'flex', flexDirection: 'row' }}>
<Typography>
Variable Name<span style={{ color: 'red' }}>&nbsp;*</span>
</Typography>
<div style={{ flexGrow: 1 }}></div>
</div>
<OutlinedInput
size='small'
sx={{ mt: 1 }}
type='string'
fullWidth
key='variableName'
onChange={(e) => setVariableName(e.target.value)}
value={variableName ?? ''}
/>
</Box>
<Box sx={{ p: 2 }}>
<div style={{ display: 'flex', flexDirection: 'row' }}>
<Typography>
Type<span style={{ color: 'red' }}>&nbsp;*</span>
</Typography>
<div style={{ flexGrow: 1 }}></div>
</div>
<Dropdown
key={variableType}
name='variableType'
options={variableTypes}
onSelect={(newValue) => setVariableType(newValue)}
value={variableType ?? 'choose an option'}
/>
</Box>
{variableType === 'static' && (
<Box sx={{ p: 2 }}>
<div style={{ display: 'flex', flexDirection: 'row' }}>
<Typography>
Value<span style={{ color: 'red' }}>&nbsp;*</span>
</Typography>
<div style={{ flexGrow: 1 }}></div>
</div>
<OutlinedInput
size='small'
sx={{ mt: 1 }}
type='string'
fullWidth
key='variableValue'
onChange={(e) => setVariableValue(e.target.value)}
value={variableValue ?? ''}
/>
</Box>
)}
</DialogContent>
<DialogActions>
<StyledButton
disabled={!variableName || !variableType || (variableType === 'static' && !variableValue)}
variant='contained'
onClick={() => (dialogType === 'ADD' ? addNewVariable() : saveVariable())}
>
{dialogProps.confirmButtonName}
</StyledButton>
</DialogActions>
<ConfirmDialog />
</Dialog>
) : null
return createPortal(component, portalElement)
}
AddEditVariableDialog.propTypes = {
show: PropTypes.bool,
dialogProps: PropTypes.object,
onCancel: PropTypes.func,
onConfirm: PropTypes.func
}
export default AddEditVariableDialog
@@ -0,0 +1,72 @@
import { createPortal } from 'react-dom'
import PropTypes from 'prop-types'
import { Dialog, DialogContent, DialogTitle } from '@mui/material'
import { CodeEditor } from 'ui-component/editor/CodeEditor'
const overrideConfig = `{
overrideConfig: {
vars: {
var1: 'abc'
}
}
}`
const HowToUseVariablesDialog = ({ show, onCancel }) => {
const portalElement = document.getElementById('portal')
const component = show ? (
<Dialog
onClose={onCancel}
open={show}
fullWidth
maxWidth='sm'
aria-labelledby='alert-dialog-title'
aria-describedby='alert-dialog-description'
>
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
How To Use Variables
</DialogTitle>
<DialogContent>
<p style={{ marginBottom: '10px' }}>Variables can be used in Custom Tool Function with the $ prefix.</p>
<CodeEditor
disabled={true}
value={`$vars.<variable-name>`}
height={'50px'}
theme={'dark'}
lang={'js'}
basicSetup={{ highlightActiveLine: false, highlightActiveLineGutter: false }}
/>
<p style={{ marginBottom: '10px' }}>
If variable type is Static, the value will be retrieved as it is. If variable type is Runtime, the value will be
retrieved from .env file.
</p>
<p style={{ marginBottom: '10px' }}>
You can also override variable values in API overrideConfig using <b>vars</b>:
</p>
<CodeEditor
disabled={true}
value={overrideConfig}
height={'170px'}
theme={'dark'}
lang={'js'}
basicSetup={{ highlightActiveLine: false, highlightActiveLineGutter: false }}
/>
<p>
Read more from{' '}
<a target='_blank' rel='noreferrer' href='https://docs.flowiseai.com/using-flowise/variables'>
docs
</a>
</p>
</DialogContent>
</Dialog>
) : null
return createPortal(component, portalElement)
}
HowToUseVariablesDialog.propTypes = {
show: PropTypes.bool,
onCancel: PropTypes.func
}
export default HowToUseVariablesDialog
+314
View File
@@ -0,0 +1,314 @@
import { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from 'store/actions'
import moment from 'moment'
// material-ui
import {
Button,
Box,
Stack,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
IconButton,
Toolbar,
TextField,
InputAdornment,
ButtonGroup,
Chip
} from '@mui/material'
import { useTheme } from '@mui/material/styles'
// project imports
import MainCard from 'ui-component/cards/MainCard'
import { StyledButton } from 'ui-component/button/StyledButton'
import ConfirmDialog from 'ui-component/dialog/ConfirmDialog'
// API
import variablesApi from 'api/variables'
// Hooks
import useApi from 'hooks/useApi'
import useConfirm from 'hooks/useConfirm'
// utils
import useNotifier from 'utils/useNotifier'
// Icons
import { IconTrash, IconEdit, IconX, IconPlus, IconSearch, IconVariable } from '@tabler/icons'
import VariablesEmptySVG from 'assets/images/variables_empty.svg'
// const
import AddEditVariableDialog from './AddEditVariableDialog'
import HowToUseVariablesDialog from './HowToUseVariablesDialog'
// ==============================|| Credentials ||============================== //
const Variables = () => {
const theme = useTheme()
const customization = useSelector((state) => state.customization)
const dispatch = useDispatch()
useNotifier()
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
const [showVariableDialog, setShowVariableDialog] = useState(false)
const [variableDialogProps, setVariableDialogProps] = useState({})
const [variables, setVariables] = useState([])
const [showHowToDialog, setShowHowToDialog] = useState(false)
const { confirm } = useConfirm()
const getAllVariables = useApi(variablesApi.getAllVariables)
const [search, setSearch] = useState('')
const onSearchChange = (event) => {
setSearch(event.target.value)
}
function filterVariables(data) {
return data.name.toLowerCase().indexOf(search.toLowerCase()) > -1
}
const addNew = () => {
const dialogProp = {
type: 'ADD',
cancelButtonName: 'Cancel',
confirmButtonName: 'Add',
data: {}
}
setVariableDialogProps(dialogProp)
setShowVariableDialog(true)
}
const edit = (variable) => {
const dialogProp = {
type: 'EDIT',
cancelButtonName: 'Cancel',
confirmButtonName: 'Save',
data: variable
}
setVariableDialogProps(dialogProp)
setShowVariableDialog(true)
}
const deleteVariable = async (variable) => {
const confirmPayload = {
title: `Delete`,
description: `Delete variable ${variable.name}?`,
confirmButtonName: 'Delete',
cancelButtonName: 'Cancel'
}
const isConfirmed = await confirm(confirmPayload)
if (isConfirmed) {
try {
const deleteResp = await variablesApi.deleteVariable(variable.id)
if (deleteResp.data) {
enqueueSnackbar({
message: 'Variable deleted',
options: {
key: new Date().getTime() + Math.random(),
variant: 'success',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
onConfirm()
}
} catch (error) {
const errorData = error.response?.data || `${error.response?.status}: ${error.response?.statusText}`
enqueueSnackbar({
message: `Failed to delete Variable: ${errorData}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
persist: true,
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
}
}
}
const onConfirm = () => {
setShowVariableDialog(false)
getAllVariables.request()
}
useEffect(() => {
getAllVariables.request()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
if (getAllVariables.data) {
setVariables(getAllVariables.data)
}
}, [getAllVariables.data])
return (
<>
<MainCard sx={{ background: customization.isDarkMode ? theme.palette.common.black : '' }}>
<Stack flexDirection='row'>
<Box sx={{ flexGrow: 1 }}>
<Toolbar
disableGutters={true}
style={{
margin: 1,
padding: 1,
paddingBottom: 10,
display: 'flex',
justifyContent: 'space-between',
width: '100%'
}}
>
<h1>Variables&nbsp;</h1>
<TextField
size='small'
sx={{ display: { xs: 'none', sm: 'block' }, ml: 3 }}
variant='outlined'
placeholder='Search variable name'
onChange={onSearchChange}
InputProps={{
startAdornment: (
<InputAdornment position='start'>
<IconSearch />
</InputAdornment>
)
}}
/>
<Box sx={{ flexGrow: 1 }} />
<Button variant='outlined' sx={{ mr: 2 }} onClick={() => setShowHowToDialog(true)}>
How To Use
</Button>
<ButtonGroup
sx={{ maxHeight: 40 }}
disableElevation
variant='contained'
aria-label='outlined primary button group'
>
<ButtonGroup disableElevation aria-label='outlined primary button group'>
<StyledButton
variant='contained'
sx={{ color: 'white', mr: 1, height: 37 }}
onClick={addNew}
startIcon={<IconPlus />}
>
Add Variable
</StyledButton>
</ButtonGroup>
</ButtonGroup>
</Toolbar>
</Box>
</Stack>
{variables.length === 0 && (
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
<Box sx={{ p: 2, height: 'auto' }}>
<img
style={{ objectFit: 'cover', height: '30vh', width: 'auto' }}
src={VariablesEmptySVG}
alt='VariablesEmptySVG'
/>
</Box>
<div>No Variables Yet</div>
</Stack>
)}
{variables.length > 0 && (
<TableContainer component={Paper}>
<Table sx={{ minWidth: 650 }} aria-label='simple table'>
<TableHead>
<TableRow>
<TableCell>Name</TableCell>
<TableCell>Value</TableCell>
<TableCell>Type</TableCell>
<TableCell>Last Updated</TableCell>
<TableCell>Created</TableCell>
<TableCell> </TableCell>
<TableCell> </TableCell>
</TableRow>
</TableHead>
<TableBody>
{variables.filter(filterVariables).map((variable, index) => (
<TableRow key={index} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
<TableCell component='th' scope='row'>
<div
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center'
}}
>
<div
style={{
width: 25,
height: 25,
marginRight: 10,
borderRadius: '50%'
}}
>
<IconVariable
style={{
width: '100%',
height: '100%',
borderRadius: '50%',
objectFit: 'contain'
}}
/>
</div>
{variable.name}
</div>
</TableCell>
<TableCell>{variable.value}</TableCell>
<TableCell>
<Chip
color={variable.type === 'static' ? 'info' : 'secondary'}
size='small'
label={variable.type}
/>
</TableCell>
<TableCell>{moment(variable.updatedDate).format('DD-MMM-YY')}</TableCell>
<TableCell>{moment(variable.createdDate).format('DD-MMM-YY')}</TableCell>
<TableCell>
<IconButton title='Edit' color='primary' onClick={() => edit(variable)}>
<IconEdit />
</IconButton>
</TableCell>
<TableCell>
<IconButton title='Delete' color='error' onClick={() => deleteVariable(variable)}>
<IconTrash />
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
</MainCard>
<AddEditVariableDialog
show={showVariableDialog}
dialogProps={variableDialogProps}
onCancel={() => setShowVariableDialog(false)}
onConfirm={onConfirm}
></AddEditVariableDialog>
<HowToUseVariablesDialog show={showHowToDialog} onCancel={() => setShowHowToDialog(false)}></HowToUseVariablesDialog>
<ConfirmDialog />
</>
)
}
export default Variables