UI Improvements (#1935)

* Update styles for dashboard page

* Fix grid in chatflows and marketplaces pages

* Update styles for main routes

* Create ViewHeader component and use it in chatflows and marketplace

* Use viewheader in all main routes views and make the styles consistent

* Update table styles for chatflow and marketplace views

* Update table and grid styles in all main routes views

* Make backgrounds, borders, and colors everywhere

* Apply text ellipsis for titles in cards and tables

* Update credentials list dialog styles

* Update tools dialog styles

* Update styles for inputs and dialogs

* Show skeleton loaders for main routes

* Apply text ellipsis to chatflow title in canvas page

* Update icons for load and export buttons in tools and assistants

* Fix issue where table header is shown when number of elements is zero

* Add error boundary component to main routes

* Capture errors from all requests in main routes

* Fix id for add api key and add variable buttons

* Fix missing th tag in variables table body
This commit is contained in:
Ilango
2024-04-08 11:15:42 +05:30
committed by GitHub
parent 19e14c4798
commit 19bb23440a
32 changed files with 2267 additions and 1662 deletions
+51
View File
@@ -0,0 +1,51 @@
import PropTypes from 'prop-types'
import { Box, Card, IconButton, Stack, Typography, useTheme } from '@mui/material'
import { IconCopy } from '@tabler/icons'
const ErrorBoundary = ({ error }) => {
const theme = useTheme()
const copyToClipboard = () => {
const errorMessage = `Status: ${error.response.status}\n${error.response.data.message}`
navigator.clipboard.writeText(errorMessage)
}
return (
<Box sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2, padding: '20px', maxWidth: '1280px' }}>
<Stack flexDirection='column' sx={{ alignItems: 'center', gap: 3 }}>
<Stack flexDirection='column' sx={{ alignItems: 'center', gap: 1 }}>
<Typography variant='h2'>Oh snap!</Typography>
<Typography variant='h3'>The following error occured when loading this page.</Typography>
</Stack>
<Card variant='outlined'>
<Box sx={{ position: 'relative', px: 2, py: 3 }}>
<IconButton
onClick={copyToClipboard}
size='small'
sx={{ position: 'absolute', top: 1, right: 1, color: theme.palette.grey[900] + 25 }}
>
<IconCopy />
</IconButton>
<pre style={{ margin: 0 }}>
<code>{`Status: ${error.response.status}`}</code>
<br />
<code>{error.response.data.message}</code>
</pre>
</Box>
</Card>
<Typography variant='body1' sx={{ fontSize: '1.1rem', textAlign: 'center', lineHeight: '1.5' }}>
Please retry after some time. If the issue persists, reach out to us on our Discord server.
<br />
Alternatively, you can raise an issue on Github.
</Typography>
</Stack>
</Box>
)
}
ErrorBoundary.propTypes = {
error: PropTypes.object
}
export default ErrorBoundary
@@ -44,6 +44,7 @@ const NavGroup = ({ item }) => {
</Typography> </Typography>
) )
} }
sx={{ py: '20px' }}
> >
{items} {items}
</List> </List>
@@ -11,7 +11,7 @@ import { BrowserView, MobileView } from 'react-device-detect'
// project imports // project imports
import MenuList from './MenuList' import MenuList from './MenuList'
import LogoSection from '../LogoSection' import LogoSection from '../LogoSection'
import { drawerWidth } from '@/store/constant' import { drawerWidth, headerHeight } from '@/store/constant'
// ==============================|| SIDEBAR DRAWER ||============================== // // ==============================|| SIDEBAR DRAWER ||============================== //
@@ -21,7 +21,12 @@ const Sidebar = ({ drawerOpen, drawerToggle, window }) => {
const drawer = ( const drawer = (
<> <>
<Box sx={{ display: { xs: 'block', md: 'none' } }}> <Box
sx={{
display: { xs: 'block', md: 'none' },
height: '80px'
}}
>
<Box sx={{ display: 'flex', p: 2, mx: 'auto' }}> <Box sx={{ display: 'flex', p: 2, mx: 'auto' }}>
<LogoSection /> <LogoSection />
</Box> </Box>
@@ -30,7 +35,7 @@ const Sidebar = ({ drawerOpen, drawerToggle, window }) => {
<PerfectScrollbar <PerfectScrollbar
component='div' component='div'
style={{ style={{
height: !matchUpMd ? 'calc(100vh - 56px)' : 'calc(100vh - 88px)', height: !matchUpMd ? 'calc(100vh - 56px)' : `calc(100vh - ${headerHeight}px)`,
paddingLeft: '16px', paddingLeft: '16px',
paddingRight: '16px' paddingRight: '16px'
}} }}
@@ -49,7 +54,14 @@ const Sidebar = ({ drawerOpen, drawerToggle, window }) => {
const container = window !== undefined ? () => window.document.body : undefined const container = window !== undefined ? () => window.document.body : undefined
return ( return (
<Box component='nav' sx={{ flexShrink: { md: 0 }, width: matchUpMd ? drawerWidth : 'auto' }} aria-label='mailbox folders'> <Box
component='nav'
sx={{
flexShrink: { md: 0 },
width: matchUpMd ? drawerWidth : 'auto'
}}
aria-label='mailbox folders'
>
<Drawer <Drawer
container={container} container={container}
variant={matchUpMd ? 'persistent' : 'temporary'} variant={matchUpMd ? 'persistent' : 'temporary'}
@@ -61,10 +73,11 @@ const Sidebar = ({ drawerOpen, drawerToggle, window }) => {
width: drawerWidth, width: drawerWidth,
background: theme.palette.background.default, background: theme.palette.background.default,
color: theme.palette.text.primary, color: theme.palette.text.primary,
borderRight: 'none',
[theme.breakpoints.up('md')]: { [theme.breakpoints.up('md')]: {
top: '66px' top: `${headerHeight}px`
} },
borderRight: drawerOpen ? '1px solid' : 'none',
borderColor: drawerOpen ? theme.palette.primary[200] + 75 : 'transparent'
} }
}} }}
ModalProps={{ keepMounted: true }} ModalProps={{ keepMounted: true }}
@@ -0,0 +1,83 @@
import PropTypes from 'prop-types'
// material-ui
import { Box, OutlinedInput, Toolbar, Typography } from '@mui/material'
import { useTheme } from '@mui/material/styles'
// icons
import { IconSearch } from '@tabler/icons'
const ViewHeader = ({ children, filters = null, onSearchChange, search, searchPlaceholder = 'Search', title }) => {
const theme = useTheme()
return (
<Box sx={{ flexGrow: 1, py: 1.25, width: '100%' }}>
<Toolbar
disableGutters={true}
sx={{
p: 0,
display: 'flex',
justifyContent: 'space-between',
width: '100%'
}}
>
<Typography
sx={{
fontSize: '2rem',
fontWeight: 600
}}
variant='h1'
>
{title}
</Typography>
<Box sx={{ height: 40, display: 'flex', alignItems: 'center', gap: 1 }}>
{search && (
<OutlinedInput
size='small'
sx={{
width: '280px',
height: '100%',
display: { xs: 'none', sm: 'flex' },
borderRadius: 2,
'& .MuiOutlinedInput-notchedOutline': {
borderRadius: 2
}
}}
variant='outlined'
placeholder={searchPlaceholder}
onChange={onSearchChange}
startAdornment={
<Box
sx={{
color: theme.palette.grey[400],
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
mr: 1
}}
>
<IconSearch style={{ color: 'inherit', width: 16, height: 16 }} />
</Box>
}
type='search'
/>
)}
{filters}
{children}
</Box>
</Toolbar>
</Box>
)
}
ViewHeader.propTypes = {
children: PropTypes.node,
filters: PropTypes.node,
onSearchChange: PropTypes.func,
search: PropTypes.bool,
searchPlaceholder: PropTypes.string,
title: PropTypes.string
}
export default ViewHeader
+10 -12
View File
@@ -9,21 +9,23 @@ import { AppBar, Box, CssBaseline, Toolbar, useMediaQuery } from '@mui/material'
// project imports // project imports
import Header from './Header' import Header from './Header'
import Sidebar from './Sidebar' import Sidebar from './Sidebar'
import { drawerWidth } from '@/store/constant' import { drawerWidth, headerHeight } from '@/store/constant'
import { SET_MENU } from '@/store/actions' import { SET_MENU } from '@/store/actions'
// styles // styles
const Main = styled('main', { shouldForwardProp: (prop) => prop !== 'open' })(({ theme, open }) => ({ const Main = styled('main', { shouldForwardProp: (prop) => prop !== 'open' })(({ theme, open }) => ({
...theme.typography.mainContent, ...theme.typography.mainContent,
...(!open && { ...(!open && {
backgroundColor: 'transparent',
borderBottomLeftRadius: 0, borderBottomLeftRadius: 0,
borderBottomRightRadius: 0, borderBottomRightRadius: 0,
transition: theme.transitions.create('margin', { transition: theme.transitions.create('all', {
easing: theme.transitions.easing.sharp, easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen duration: theme.transitions.duration.leavingScreen
}), }),
marginRight: 0,
[theme.breakpoints.up('md')]: { [theme.breakpoints.up('md')]: {
marginLeft: -(drawerWidth - 20), marginLeft: -drawerWidth,
width: `calc(100% - ${drawerWidth}px)` width: `calc(100% - ${drawerWidth}px)`
}, },
[theme.breakpoints.down('md')]: { [theme.breakpoints.down('md')]: {
@@ -39,20 +41,16 @@ const Main = styled('main', { shouldForwardProp: (prop) => prop !== 'open' })(({
} }
}), }),
...(open && { ...(open && {
transition: theme.transitions.create('margin', { backgroundColor: 'transparent',
transition: theme.transitions.create('all', {
easing: theme.transitions.easing.easeOut, easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen duration: theme.transitions.duration.enteringScreen
}), }),
marginLeft: 0, marginLeft: 0,
marginRight: 0,
borderBottomLeftRadius: 0, borderBottomLeftRadius: 0,
borderBottomRightRadius: 0, borderBottomRightRadius: 0,
width: `calc(100% - ${drawerWidth}px)`, width: `calc(100% - ${drawerWidth}px)`
[theme.breakpoints.down('md')]: {
marginLeft: '20px'
},
[theme.breakpoints.down('sm')]: {
marginLeft: '10px'
}
}) })
})) }))
@@ -88,7 +86,7 @@ const MainLayout = () => {
transition: leftDrawerOpened ? theme.transitions.create('width') : 'none' transition: leftDrawerOpened ? theme.transitions.create('width') : 'none'
}} }}
> >
<Toolbar> <Toolbar sx={{ height: `${headerHeight}px`, borderBottom: '1px solid', borderColor: theme.palette.primary[200] + 75 }}>
<Header handleLeftDrawerToggle={handleLeftDrawerToggle} /> <Header handleLeftDrawerToggle={handleLeftDrawerToggle} />
</Toolbar> </Toolbar>
</AppBar> </AppBar>
+1
View File
@@ -2,6 +2,7 @@
export const gridSpacing = 3 export const gridSpacing = 3
export const drawerWidth = 260 export const drawerWidth = 260
export const appDrawerWidth = 320 export const appDrawerWidth = 320
export const headerHeight = 80
export const maxScroll = 100000 export const maxScroll = 100000
export const baseURL = export const baseURL =
import.meta.env.PROD === true import.meta.env.PROD === true
@@ -72,7 +72,7 @@ const StyledMenu = styled((props) => (
} }
})) }))
export default function FlowListMenu({ chatflow, updateFlowsApi }) { export default function FlowListMenu({ chatflow, setError, updateFlowsApi }) {
const { confirm } = useConfirm() const { confirm } = useConfirm()
const dispatch = useDispatch() const dispatch = useDispatch()
const updateChatflowApi = useApi(chatflowsApi.updateChatflow) const updateChatflowApi = useApi(chatflowsApi.updateChatflow)
@@ -153,6 +153,7 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) {
await updateChatflowApi.request(chatflow.id, updateBody) await updateChatflowApi.request(chatflow.id, updateBody)
await updateFlowsApi.request() await updateFlowsApi.request()
} catch (error) { } catch (error) {
setError(error)
enqueueSnackbar({ enqueueSnackbar({
message: error.response.data.message, message: error.response.data.message,
options: { options: {
@@ -191,6 +192,7 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) {
await updateChatflowApi.request(chatflow.id, updateBody) await updateChatflowApi.request(chatflow.id, updateBody)
await updateFlowsApi.request() await updateFlowsApi.request()
} catch (error) { } catch (error) {
setError(error)
enqueueSnackbar({ enqueueSnackbar({
message: error.response.data.message, message: error.response.data.message,
options: { options: {
@@ -222,6 +224,7 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) {
await chatflowsApi.deleteChatflow(chatflow.id) await chatflowsApi.deleteChatflow(chatflow.id)
await updateFlowsApi.request() await updateFlowsApi.request()
} catch (error) { } catch (error) {
setError(error)
enqueueSnackbar({ enqueueSnackbar({
message: error.response.data.message, message: error.response.data.message,
options: { options: {
@@ -370,5 +373,6 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) {
FlowListMenu.propTypes = { FlowListMenu.propTypes = {
chatflow: PropTypes.object, chatflow: PropTypes.object,
setError: PropTypes.func,
updateFlowsApi: PropTypes.object updateFlowsApi: PropTypes.object
} }
+97 -83
View File
@@ -1,12 +1,12 @@
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { useSelector } from 'react-redux'
// material-ui // material-ui
import { styled } from '@mui/material/styles' import { styled } from '@mui/material/styles'
import { Box, Grid, Typography } from '@mui/material' import { Box, Grid, Typography, useTheme } from '@mui/material'
// project imports // project imports
import MainCard from '@/ui-component/cards/MainCard' import MainCard from '@/ui-component/cards/MainCard'
import SkeletonChatflowCard from '@/ui-component/cards/Skeleton/ChatflowCard'
const CardWrapper = styled(MainCard)(({ theme }) => ({ const CardWrapper = styled(MainCard)(({ theme }) => ({
background: theme.palette.card.main, background: theme.palette.card.main,
@@ -19,101 +19,115 @@ const CardWrapper = styled(MainCard)(({ theme }) => ({
background: theme.palette.card.hover, background: theme.palette.card.hover,
boxShadow: '0 2px 14px 0 rgb(32 40 45 / 20%)' boxShadow: '0 2px 14px 0 rgb(32 40 45 / 20%)'
}, },
height: '100%',
minHeight: '160px',
maxHeight: '300px', maxHeight: '300px',
maxWidth: '300px', width: '100%',
overflowWrap: 'break-word', overflowWrap: 'break-word',
whiteSpace: 'pre-line' whiteSpace: 'pre-line'
})) }))
// ===========================|| CONTRACT CARD ||=========================== // // ===========================|| CONTRACT CARD ||=========================== //
const ItemCard = ({ isLoading, data, images, onClick }) => { const ItemCard = ({ data, images, onClick }) => {
const theme = useTheme()
const customization = useSelector((state) => state.customization)
return ( return (
<> <CardWrapper content={false} onClick={onClick} sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2 }}>
{isLoading ? ( <Box sx={{ height: '100%', p: 2.25 }}>
<SkeletonChatflowCard /> <Grid container justifyContent='space-between' direction='column' sx={{ height: '100%', gap: 3 }}>
) : ( <Box display='flex' flexDirection='column' sx={{ width: '100%' }}>
<CardWrapper border={false} content={false} onClick={onClick}> <div
<Box sx={{ p: 2.25 }}> style={{
<Grid container direction='column'> width: '100%',
<div display: 'flex',
style={{ flexDirection: 'row',
display: 'flex', alignItems: 'center',
flexDirection: 'row', overflow: 'hidden'
alignItems: 'center' }}
}} >
> {data.iconSrc && (
{data.iconSrc && (
<div
style={{
width: 35,
height: 35,
marginRight: 10,
borderRadius: '50%',
background: `url(${data.iconSrc})`,
backgroundSize: 'contain',
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center center'
}}
></div>
)}
{!data.iconSrc && data.color && (
<div
style={{
width: 35,
height: 35,
marginRight: 10,
borderRadius: '50%',
background: data.color
}}
></div>
)}
<Typography
sx={{ fontSize: '1.5rem', fontWeight: 500, overflowWrap: 'break-word', whiteSpace: 'pre-line' }}
>
{data.templateName || data.name}
</Typography>
</div>
{data.description && (
<span style={{ marginTop: 10, overflowWrap: 'break-word', whiteSpace: 'pre-line' }}>
{data.description}
</span>
)}
{images && (
<div <div
style={{ style={{
width: 35,
height: 35,
display: 'flex', display: 'flex',
flexDirection: 'row', flexShrink: 0,
flexWrap: 'wrap', marginRight: 10,
marginTop: 5 borderRadius: '50%',
background: `url(${data.iconSrc})`,
backgroundSize: 'contain',
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center center'
}}
></div>
)}
{!data.iconSrc && data.color && (
<div
style={{
width: 35,
height: 35,
display: 'flex',
flexShrink: 0,
marginRight: 10,
borderRadius: '50%',
background: data.color
}}
></div>
)}
<Typography
sx={{
display: '-webkit-box',
fontSize: '1.25rem',
fontWeight: 500,
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
textOverflow: 'ellipsis',
overflow: 'hidden'
}}
>
{data.templateName || data.name}
</Typography>
</div>
{data.description && (
<span style={{ marginTop: 10, overflowWrap: 'break-word', whiteSpace: 'pre-line' }}>{data.description}</span>
)}
</Box>
{images && (
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'start',
gap: 1
}}
>
{images.slice(0, images.length > 3 ? 3 : images.length).map((img) => (
<Box
key={img}
sx={{
width: 30,
height: 30,
borderRadius: '50%',
backgroundColor: customization.isDarkMode
? theme.palette.common.white
: theme.palette.grey[300] + 75
}} }}
> >
{images.map((img) => ( <img style={{ width: '100%', height: '100%', padding: 5, objectFit: 'contain' }} alt='' src={img} />
<div </Box>
key={img} ))}
style={{ {images.length > 3 && (
width: 35, <Typography sx={{ alignItems: 'center', display: 'flex', fontSize: '.9rem', fontWeight: 200 }}>
height: 35, + {images.length - 3} More
marginRight: 5, </Typography>
borderRadius: '50%',
backgroundColor: 'white',
marginTop: 5
}}
>
<img
style={{ width: '100%', height: '100%', padding: 5, objectFit: 'contain' }}
alt=''
src={img}
/>
</div>
))}
</div>
)} )}
</Grid> </Box>
</Box> )}
</CardWrapper> </Grid>
)} </Box>
</> </CardWrapper>
) )
} }
@@ -2,7 +2,6 @@ import PropTypes from 'prop-types'
import { forwardRef } from 'react' import { forwardRef } from 'react'
// material-ui // material-ui
import { useTheme } from '@mui/material/styles'
import { Card, CardContent, CardHeader, Divider, Typography } from '@mui/material' import { Card, CardContent, CardHeader, Divider, Typography } from '@mui/material'
// constant // constant
@@ -14,12 +13,14 @@ const headerSX = {
const MainCard = forwardRef(function MainCard( const MainCard = forwardRef(function MainCard(
{ {
border = true,
boxShadow, boxShadow,
children, children,
content = true, content = true,
contentClass = '', contentClass = '',
contentSX = {}, contentSX = {
px: 2,
py: 0
},
darkTitle, darkTitle,
secondary, secondary,
shadow, shadow,
@@ -29,18 +30,17 @@ const MainCard = forwardRef(function MainCard(
}, },
ref ref
) { ) {
const theme = useTheme()
return ( return (
<Card <Card
ref={ref} ref={ref}
{...others} {...others}
sx={{ sx={{
border: border ? '1px solid' : 'none', background: 'transparent',
borderColor: theme.palette.primary[200] + 75,
':hover': { ':hover': {
boxShadow: boxShadow ? shadow || '0 2px 14px 0 rgb(32 40 45 / 8%)' : 'inherit' boxShadow: boxShadow ? shadow || '0 2px 14px 0 rgb(32 40 45 / 8%)' : 'inherit'
}, },
maxWidth: '1280px',
mx: 'auto',
...sx ...sx
}} }}
> >
@@ -114,7 +114,7 @@ export const AsyncDropdown = ({
disabled={disabled} disabled={disabled}
disableClearable={disableClearable} disableClearable={disableClearable}
size='small' size='small'
sx={{ width: '100%' }} sx={{ width: '100%', height: '52px' }}
open={open} open={open}
onOpen={() => { onOpen={() => {
setOpen(true) setOpen(true)
@@ -148,6 +148,7 @@ export const AsyncDropdown = ({
</Fragment> </Fragment>
) )
}} }}
sx={{ height: '100%', '& .MuiInputBase-root': { height: '100%' } }}
/> />
)} )}
renderOption={(props, option) => ( renderOption={(props, option) => (
@@ -25,7 +25,7 @@ export const Dropdown = ({ name, value, options, onSelect, disabled = false, dis
let [internalValue, setInternalValue] = useState(value ?? 'choose an option') let [internalValue, setInternalValue] = useState(value ?? 'choose an option')
return ( return (
<FormControl sx={{ mt: 1, width: '100%' }} size='small'> <FormControl sx={{ width: '100%', height: '52px' }} size='small'>
<Autocomplete <Autocomplete
id={name} id={name}
disabled={disabled} disabled={disabled}
@@ -39,7 +39,9 @@ export const Dropdown = ({ name, value, options, onSelect, disabled = false, dis
onSelect(value) onSelect(value)
}} }}
PopperComponent={StyledPopper} PopperComponent={StyledPopper}
renderInput={(params) => <TextField {...params} value={internalValue} />} renderInput={(params) => (
<TextField {...params} value={internalValue} sx={{ height: '100%', '& .MuiInputBase-root': { height: '100%' } }} />
)}
renderOption={(props, option) => ( renderOption={(props, option) => (
<Box component='li' {...props}> <Box component='li' {...props}>
<div style={{ display: 'flex', flexDirection: 'column' }}> <div style={{ display: 'flex', flexDirection: 'column' }}>
@@ -50,6 +52,7 @@ export const Dropdown = ({ name, value, options, onSelect, disabled = false, dis
</div> </div>
</Box> </Box>
)} )}
sx={{ height: '100%' }}
/> />
</FormControl> </FormControl>
) )
@@ -30,7 +30,7 @@ export const MultiDropdown = ({ name, value, options, onSelect, formControlSx =
let [internalValue, setInternalValue] = useState(value ?? []) let [internalValue, setInternalValue] = useState(value ?? [])
return ( return (
<FormControl sx={{ mt: 1, width: '100%', ...formControlSx }} size='small'> <FormControl sx={{ width: '100%', height: '52px', ...formControlSx }} size='small'>
<Autocomplete <Autocomplete
id={name} id={name}
disabled={disabled} disabled={disabled}
@@ -53,7 +53,9 @@ export const MultiDropdown = ({ name, value, options, onSelect, formControlSx =
onSelect(value) onSelect(value)
}} }}
PopperComponent={StyledPopper} PopperComponent={StyledPopper}
renderInput={(params) => <TextField {...params} value={internalValue} />} renderInput={(params) => (
<TextField {...params} value={internalValue} sx={{ height: '100%', '& .MuiInputBase-root': { height: '100%' } }} />
)}
renderOption={(props, option) => ( renderOption={(props, option) => (
<Box component='li' {...props}> <Box component='li' {...props}>
<div style={{ display: 'flex', flexDirection: 'column' }}> <div style={{ display: 'flex', flexDirection: 'column' }}>
@@ -64,6 +66,7 @@ export const MultiDropdown = ({ name, value, options, onSelect, formControlSx =
</div> </div>
</Box> </Box>
)} )}
sx={{ height: '100%' }}
/> />
</FormControl> </FormControl>
) )
+1 -9
View File
@@ -1,9 +1,7 @@
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { DataGrid } from '@mui/x-data-grid' import { DataGrid } from '@mui/x-data-grid'
import { IconPlus } from '@tabler/icons'
import { Button } from '@mui/material'
export const Grid = ({ columns, rows, style, disabled = false, onRowUpdate, addNewRow }) => { export const Grid = ({ columns, rows, style, disabled = false, onRowUpdate }) => {
const handleProcessRowUpdate = (newRow) => { const handleProcessRowUpdate = (newRow) => {
onRowUpdate(newRow) onRowUpdate(newRow)
return newRow return newRow
@@ -11,11 +9,6 @@ export const Grid = ({ columns, rows, style, disabled = false, onRowUpdate, addN
return ( return (
<> <>
{!disabled && (
<Button variant='outlined' onClick={addNewRow} startIcon={<IconPlus />}>
Add Item
</Button>
)}
{rows && columns && ( {rows && columns && (
<div style={{ marginTop: 10, height: 300, width: '100%', ...style }}> <div style={{ marginTop: 10, height: 300, width: '100%', ...style }}>
<DataGrid <DataGrid
@@ -38,6 +31,5 @@ Grid.propTypes = {
columns: PropTypes.array, columns: PropTypes.array,
style: PropTypes.any, style: PropTypes.any,
disabled: PropTypes.bool, disabled: PropTypes.bool,
addNewRow: PropTypes.func,
onRowUpdate: PropTypes.func onRowUpdate: PropTypes.func
} }
@@ -1,50 +1,61 @@
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { useNavigate } from 'react-router-dom' import { useSelector } from 'react-redux'
import moment from 'moment' import moment from 'moment'
import { styled } from '@mui/material/styles' import { styled } from '@mui/material/styles'
import Table from '@mui/material/Table' import {
import TableBody from '@mui/material/TableBody' Box,
import TableCell, { tableCellClasses } from '@mui/material/TableCell' Chip,
import TableContainer from '@mui/material/TableContainer' Paper,
import TableHead from '@mui/material/TableHead' Skeleton,
import TableRow from '@mui/material/TableRow' Stack,
import Paper from '@mui/material/Paper' Table,
import Chip from '@mui/material/Chip' TableBody,
import { Button, Stack, Typography } from '@mui/material' TableCell,
TableContainer,
TableHead,
TableRow,
Tooltip,
Typography,
useTheme
} from '@mui/material'
import { tableCellClasses } from '@mui/material/TableCell'
import FlowListMenu from '../button/FlowListMenu' import FlowListMenu from '../button/FlowListMenu'
import { Link } from 'react-router-dom'
const StyledTableCell = styled(TableCell)(({ theme }) => ({ const StyledTableCell = styled(TableCell)(({ theme }) => ({
borderColor: theme.palette.grey[900] + 25,
[`&.${tableCellClasses.head}`]: { [`&.${tableCellClasses.head}`]: {
backgroundColor: theme.palette.common.black, color: theme.palette.grey[900]
color: theme.palette.common.white
}, },
[`&.${tableCellClasses.body}`]: { [`&.${tableCellClasses.body}`]: {
fontSize: 14 fontSize: 14,
height: 64
} }
})) }))
const StyledTableRow = styled(TableRow)(({ theme }) => ({ const StyledTableRow = styled(TableRow)(() => ({
'&:nth-of-type(odd)': {
backgroundColor: theme.palette.action.hover
},
// hide last border // hide last border
'&:last-child td, &:last-child th': { '&:last-child td, &:last-child th': {
border: 0 border: 0
} }
})) }))
export const FlowListTable = ({ data, images, filterFunction, updateFlowsApi }) => { export const FlowListTable = ({ data, images, isLoading, filterFunction, updateFlowsApi, setError }) => {
const navigate = useNavigate() const theme = useTheme()
const goToCanvas = (selectedChatflow) => { const customization = useSelector((state) => state.customization)
navigate(`/canvas/${selectedChatflow.id}`)
}
return ( return (
<> <>
<TableContainer style={{ marginTop: '30', border: 1 }} component={Paper}> <TableContainer sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2 }} component={Paper}>
<Table sx={{ minWidth: 650 }} size='small' aria-label='a dense table'> <Table sx={{ minWidth: 650 }} size='small' aria-label='a dense table'>
<TableHead> <TableHead
<TableRow sx={{ marginTop: '10', backgroundColor: 'primary' }}> sx={{
backgroundColor: customization.isDarkMode ? theme.palette.common.black : theme.palette.grey[100],
height: 56
}}
>
<TableRow>
<StyledTableCell component='th' scope='row' style={{ width: '20%' }} key='0'> <StyledTableCell component='th' scope='row' style={{ width: '20%' }} key='0'>
Name Name
</StyledTableCell> </StyledTableCell>
@@ -63,82 +74,150 @@ export const FlowListTable = ({ data, images, filterFunction, updateFlowsApi })
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{data.filter(filterFunction).map((row, index) => ( {isLoading ? (
<StyledTableRow key={index}> <>
<TableCell key='0'> <StyledTableRow>
<Typography <StyledTableCell>
sx={{ fontSize: '1.2rem', fontWeight: 500, overflowWrap: 'break-word', whiteSpace: 'pre-line' }} <Skeleton variant='text' />
> </StyledTableCell>
<Button onClick={() => goToCanvas(row)} sx={{ textAlign: 'left' }}> <StyledTableCell>
{row.templateName || row.name} <Skeleton variant='text' />
</Button> </StyledTableCell>
</Typography> <StyledTableCell>
</TableCell> <Skeleton variant='text' />
<TableCell key='1'> </StyledTableCell>
<div <StyledTableCell>
style={{ <Skeleton variant='text' />
display: 'flex', </StyledTableCell>
flexDirection: 'row', <StyledTableCell>
flexWrap: 'wrap', <Skeleton variant='text' />
marginTop: 5 </StyledTableCell>
}} </StyledTableRow>
> <StyledTableRow>
&nbsp; <StyledTableCell>
{row.category && <Skeleton variant='text' />
row.category </StyledTableCell>
.split(';') <StyledTableCell>
.map((tag, index) => ( <Skeleton variant='text' />
<Chip key={index} label={tag} style={{ marginRight: 5, marginBottom: 5 }} /> </StyledTableCell>
))} <StyledTableCell>
</div> <Skeleton variant='text' />
</TableCell> </StyledTableCell>
<TableCell key='2'> <StyledTableCell>
{images[row.id] && ( <Skeleton variant='text' />
<div </StyledTableCell>
style={{ <StyledTableCell>
display: 'flex', <Skeleton variant='text' />
flexDirection: 'row', </StyledTableCell>
flexWrap: 'wrap', </StyledTableRow>
marginTop: 5 </>
}} ) : (
> <>
{images[row.id].slice(0, images[row.id].length > 5 ? 5 : images[row.id].length).map((img) => ( {data?.filter(filterFunction).map((row, index) => (
<div <StyledTableRow key={index}>
key={img} <StyledTableCell key='0'>
style={{ <Tooltip title={row.templateName || row.name}>
width: 35, <Typography
height: 35, sx={{
marginRight: 5, display: '-webkit-box',
borderRadius: '50%', fontSize: 14,
backgroundColor: 'white', fontWeight: 500,
marginTop: 5 WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
textOverflow: 'ellipsis',
overflow: 'hidden'
}} }}
> >
<img <Link to={`/canvas/${row.id}`} style={{ color: '#2196f3', textDecoration: 'none' }}>
style={{ width: '100%', height: '100%', padding: 5, objectFit: 'contain' }} {row.templateName || row.name}
alt='' </Link>
src={img}
/>
</div>
))}
{images[row.id].length > 5 && (
<Typography
sx={{ alignItems: 'center', display: 'flex', fontSize: '.8rem', fontWeight: 200 }}
>
+ {images[row.id].length - 5} More
</Typography> </Typography>
</Tooltip>
</StyledTableCell>
<StyledTableCell key='1'>
<div
style={{
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
marginTop: 5
}}
>
&nbsp;
{row.category &&
row.category
.split(';')
.map((tag, index) => (
<Chip key={index} label={tag} style={{ marginRight: 5, marginBottom: 5 }} />
))}
</div>
</StyledTableCell>
<StyledTableCell key='2'>
{images[row.id] && (
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'start',
gap: 1
}}
>
{images[row.id]
.slice(0, images[row.id].length > 5 ? 5 : images[row.id].length)
.map((img) => (
<Box
key={img}
sx={{
width: 30,
height: 30,
borderRadius: '50%',
backgroundColor: customization.isDarkMode
? theme.palette.common.white
: theme.palette.grey[300] + 75
}}
>
<img
style={{
width: '100%',
height: '100%',
padding: 5,
objectFit: 'contain'
}}
alt=''
src={img}
/>
</Box>
))}
{images[row.id].length > 5 && (
<Typography
sx={{
alignItems: 'center',
display: 'flex',
fontSize: '.9rem',
fontWeight: 200
}}
>
+ {images[row.id].length - 5} More
</Typography>
)}
</Box>
)} )}
</div> </StyledTableCell>
)} <StyledTableCell key='3'>{moment(row.updatedDate).format('MMMM Do, YYYY')}</StyledTableCell>
</TableCell> <StyledTableCell key='4'>
<TableCell key='3'>{moment(row.updatedDate).format('MMMM Do, YYYY')}</TableCell> <Stack
<TableCell key='4'> direction={{ xs: 'column', sm: 'row' }}
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={1} justifyContent='center' alignItems='center'> spacing={1}
<FlowListMenu chatflow={row} updateFlowsApi={updateFlowsApi} /> justifyContent='center'
</Stack> alignItems='center'
</TableCell> >
</StyledTableRow> <FlowListMenu chatflow={row} setError={setError} updateFlowsApi={updateFlowsApi} />
))} </Stack>
</StyledTableCell>
</StyledTableRow>
))}
</>
)}
</TableBody> </TableBody>
</Table> </Table>
</TableContainer> </TableContainer>
@@ -149,6 +228,8 @@ export const FlowListTable = ({ data, images, filterFunction, updateFlowsApi })
FlowListTable.propTypes = { FlowListTable.propTypes = {
data: PropTypes.array, data: PropTypes.array,
images: PropTypes.object, images: PropTypes.object,
isLoading: PropTypes.bool,
filterFunction: PropTypes.func, filterFunction: PropTypes.func,
updateFlowsApi: PropTypes.object updateFlowsApi: PropTypes.object,
setError: PropTypes.func
} }
@@ -1,36 +1,54 @@
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { useSelector } from 'react-redux'
import { styled } from '@mui/material/styles' import { styled } from '@mui/material/styles'
import Table from '@mui/material/Table' import { tableCellClasses } from '@mui/material/TableCell'
import TableBody from '@mui/material/TableBody' import {
import TableCell, { tableCellClasses } from '@mui/material/TableCell' Button,
import TableContainer from '@mui/material/TableContainer' Chip,
import TableHead from '@mui/material/TableHead' Paper,
import TableRow from '@mui/material/TableRow' Skeleton,
import Paper from '@mui/material/Paper' Table,
import Chip from '@mui/material/Chip' TableBody,
import { Button, Typography } from '@mui/material' TableCell,
TableContainer,
TableHead,
TableRow,
Typography,
useTheme
} from '@mui/material'
const StyledTableCell = styled(TableCell)(({ theme }) => ({ const StyledTableCell = styled(TableCell)(({ theme }) => ({
borderColor: theme.palette.grey[900] + 25,
[`&.${tableCellClasses.head}`]: { [`&.${tableCellClasses.head}`]: {
backgroundColor: theme.palette.common.black, color: theme.palette.grey[900]
color: theme.palette.common.white
}, },
[`&.${tableCellClasses.body}`]: { [`&.${tableCellClasses.body}`]: {
fontSize: 14 fontSize: 14,
height: 64
} }
})) }))
const StyledTableRow = styled(TableRow)(({ theme }) => ({ const StyledTableRow = styled(TableRow)(() => ({
'&:nth-of-type(odd)': {
backgroundColor: theme.palette.action.hover
},
// hide last border // hide last border
'&:last-child td, &:last-child th': { '&:last-child td, &:last-child th': {
border: 0 border: 0
} }
})) }))
export const MarketplaceTable = ({ data, filterFunction, filterByBadge, filterByType, filterByFramework, goToCanvas, goToTool }) => { export const MarketplaceTable = ({
data,
filterFunction,
filterByBadge,
filterByType,
filterByFramework,
goToCanvas,
goToTool,
isLoading
}) => {
const theme = useTheme()
const customization = useSelector((state) => state.customization)
const openTemplate = (selectedTemplate) => { const openTemplate = (selectedTemplate) => {
if (selectedTemplate.flowData) { if (selectedTemplate.flowData) {
goToCanvas(selectedTemplate) goToCanvas(selectedTemplate)
@@ -41,10 +59,15 @@ export const MarketplaceTable = ({ data, filterFunction, filterByBadge, filterBy
return ( return (
<> <>
<TableContainer style={{ marginTop: '30', border: 1 }} component={Paper}> <TableContainer sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2 }} component={Paper}>
<Table sx={{ minWidth: 650 }} size='small' aria-label='a dense table'> <Table sx={{ minWidth: 650 }} size='small' aria-label='a dense table'>
<TableHead> <TableHead
<TableRow sx={{ marginTop: '10', backgroundColor: 'primary' }}> sx={{
backgroundColor: customization.isDarkMode ? theme.palette.common.black : theme.palette.grey[100],
height: 56
}}
>
<TableRow>
<StyledTableCell component='th' scope='row' style={{ width: '15%' }} key='0'> <StyledTableCell component='th' scope='row' style={{ width: '15%' }} key='0'>
Name Name
</StyledTableCell> </StyledTableCell>
@@ -63,71 +86,120 @@ export const MarketplaceTable = ({ data, filterFunction, filterByBadge, filterBy
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{data {isLoading ? (
.filter(filterByBadge) <>
.filter(filterByType) <StyledTableRow>
.filter(filterFunction) <StyledTableCell>
.filter(filterByFramework) <Skeleton variant='text' />
.map((row, index) => ( </StyledTableCell>
<StyledTableRow key={index}> <StyledTableCell>
<TableCell key='0'> <Skeleton variant='text' />
<Typography </StyledTableCell>
sx={{ fontSize: '1.2rem', fontWeight: 500, overflowWrap: 'break-word', whiteSpace: 'pre-line' }} <StyledTableCell>
> <Skeleton variant='text' />
<Button onClick={() => openTemplate(row)} sx={{ textAlign: 'left' }}> </StyledTableCell>
{row.templateName || row.name} <StyledTableCell>
</Button> <Skeleton variant='text' />
</Typography> </StyledTableCell>
</TableCell> <StyledTableCell>
<TableCell key='1'> <Skeleton variant='text' />
<Typography>{row.type}</Typography> </StyledTableCell>
</TableCell>
<TableCell key='2'>
<Typography sx={{ overflowWrap: 'break-word', whiteSpace: 'pre-line' }}>
{row.description || ''}
</Typography>
</TableCell>
<TableCell key='3'>
<div
style={{
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
marginTop: 5
}}
>
{row.categories &&
row.categories
.split(',')
.map((tag, index) => (
<Chip
variant='outlined'
key={index}
size='small'
label={tag.toUpperCase()}
style={{ marginRight: 3, marginBottom: 3 }}
/>
))}
</div>
</TableCell>
<TableCell key='4'>
<Typography>
{row.badge &&
row.badge
.split(';')
.map((tag, index) => (
<Chip
color={tag === 'POPULAR' ? 'primary' : 'error'}
key={index}
size='small'
label={tag.toUpperCase()}
style={{ marginRight: 5, marginBottom: 5 }}
/>
))}
</Typography>
</TableCell>
</StyledTableRow> </StyledTableRow>
))} <StyledTableRow>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
</StyledTableRow>
</>
) : (
<>
{data
?.filter(filterByBadge)
.filter(filterByType)
.filter(filterFunction)
.filter(filterByFramework)
.map((row, index) => (
<StyledTableRow key={index}>
<StyledTableCell key='0'>
<Typography
sx={{
display: '-webkit-box',
fontSize: 14,
fontWeight: 500,
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
textOverflow: 'ellipsis',
overflow: 'hidden'
}}
>
<Button onClick={() => openTemplate(row)} sx={{ textAlign: 'left' }}>
{row.templateName || row.name}
</Button>
</Typography>
</StyledTableCell>
<StyledTableCell key='1'>
<Typography>{row.type}</Typography>
</StyledTableCell>
<StyledTableCell key='2'>
<Typography sx={{ overflowWrap: 'break-word', whiteSpace: 'pre-line' }}>
{row.description || ''}
</Typography>
</StyledTableCell>
<StyledTableCell key='3'>
<div
style={{
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
marginTop: 5
}}
>
{row.categories &&
row.categories
.split(',')
.map((tag, index) => (
<Chip
variant='outlined'
key={index}
size='small'
label={tag.toUpperCase()}
style={{ marginRight: 3, marginBottom: 3 }}
/>
))}
</div>
</StyledTableCell>
<StyledTableCell key='4'>
<Typography>
{row.badge &&
row.badge
.split(';')
.map((tag, index) => (
<Chip
color={tag === 'POPULAR' ? 'primary' : 'error'}
key={index}
size='small'
label={tag.toUpperCase()}
style={{ marginRight: 5, marginBottom: 5 }}
/>
))}
</Typography>
</StyledTableCell>
</StyledTableRow>
))}
</>
)}
</TableBody> </TableBody>
</Table> </Table>
</TableContainer> </TableContainer>
@@ -142,5 +214,6 @@ MarketplaceTable.propTypes = {
filterByType: PropTypes.func, filterByType: PropTypes.func,
filterByFramework: PropTypes.func, filterByFramework: PropTypes.func,
goToTool: PropTypes.func, goToTool: PropTypes.func,
goToCanvas: PropTypes.func goToCanvas: PropTypes.func,
isLoading: PropTypes.bool
} }
@@ -4,15 +4,15 @@ import parser from 'html-react-parser'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
export const TooltipWithParser = ({ title, style }) => { export const TooltipWithParser = ({ title, sx }) => {
const customization = useSelector((state) => state.customization) const customization = useSelector((state) => state.customization)
return ( return (
<Tooltip title={parser(title)} placement='right'> <Tooltip title={parser(title)} placement='right'>
<IconButton sx={{ height: 15, width: 15 }}> <IconButton sx={{ height: 15, width: 15, ml: 2, mt: -0.5 }}>
<Info <Info
style={{ sx={{
...style, ...sx,
background: 'transparent', background: 'transparent',
color: customization.isDarkMode ? 'white' : 'inherit', color: customization.isDarkMode ? 'white' : 'inherit',
height: 15, height: 15,
@@ -26,5 +26,5 @@ export const TooltipWithParser = ({ title, style }) => {
TooltipWithParser.propTypes = { TooltipWithParser.propTypes = {
title: PropTypes.node, title: PropTypes.node,
style: PropTypes.any sx: PropTypes.any
} }
@@ -29,7 +29,7 @@ import apikeyApi from '@/api/apikey'
// utils // utils
import useNotifier from '@/utils/useNotifier' import useNotifier from '@/utils/useNotifier'
const APIKeyDialog = ({ show, dialogProps, onCancel, onConfirm }) => { const APIKeyDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) => {
const portalElement = document.getElementById('portal') const portalElement = document.getElementById('portal')
const theme = useTheme() const theme = useTheme()
@@ -77,6 +77,7 @@ const APIKeyDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
onConfirm() onConfirm()
} }
} catch (error) { } catch (error) {
setError(error)
enqueueSnackbar({ enqueueSnackbar({
message: `Failed to add new API key: ${error.response.data.message}`, message: `Failed to add new API key: ${error.response.data.message}`,
options: { options: {
@@ -113,6 +114,7 @@ const APIKeyDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
onConfirm() onConfirm()
} }
} catch (error) { } catch (error) {
setError(error)
enqueueSnackbar({ enqueueSnackbar({
message: `Failed to save API key: ${error.response.data.message}`, message: `Failed to save API key: ${error.response.data.message}`,
options: { options: {
@@ -227,7 +229,8 @@ APIKeyDialog.propTypes = {
show: PropTypes.bool, show: PropTypes.bool,
dialogProps: PropTypes.object, dialogProps: PropTypes.object,
onCancel: PropTypes.func, onCancel: PropTypes.func,
onConfirm: PropTypes.func onConfirm: PropTypes.func,
setError: PropTypes.func
} }
export default APIKeyDialog export default APIKeyDialog
+173 -136
View File
@@ -7,6 +7,7 @@ import {
Button, Button,
Box, Box,
Chip, Chip,
Skeleton,
Stack, Stack,
Table, Table,
TableBody, TableBody,
@@ -17,11 +18,7 @@ import {
IconButton, IconButton,
Popover, Popover,
Collapse, Collapse,
Typography, Typography
Toolbar,
TextField,
InputAdornment,
ButtonGroup
} from '@mui/material' } from '@mui/material'
import TableCell, { tableCellClasses } from '@mui/material/TableCell' import TableCell, { tableCellClasses } from '@mui/material/TableCell'
import { useTheme, styled } from '@mui/material/styles' import { useTheme, styled } from '@mui/material/styles'
@@ -43,26 +40,24 @@ import useConfirm from '@/hooks/useConfirm'
import useNotifier from '@/utils/useNotifier' import useNotifier from '@/utils/useNotifier'
// Icons // Icons
import { import { IconTrash, IconEdit, IconCopy, IconChevronsUp, IconChevronsDown, IconX, IconPlus, IconEye, IconEyeOff } from '@tabler/icons'
IconTrash,
IconEdit,
IconCopy,
IconChevronsUp,
IconChevronsDown,
IconX,
IconSearch,
IconPlus,
IconEye,
IconEyeOff
} from '@tabler/icons'
import APIEmptySVG from '@/assets/images/api_empty.svg' import APIEmptySVG from '@/assets/images/api_empty.svg'
import * as PropTypes from 'prop-types' import * as PropTypes from 'prop-types'
import moment from 'moment/moment' import moment from 'moment/moment'
import ViewHeader from '@/layout/MainLayout/ViewHeader'
import ErrorBoundary from '@/ErrorBoundary'
// ==============================|| APIKey ||============================== // // ==============================|| APIKey ||============================== //
const StyledTableCell = styled(TableCell)(({ theme }) => ({ const StyledTableCell = styled(TableCell)(({ theme }) => ({
borderColor: theme.palette.grey[900] + 25,
padding: '6px 16px',
[`&.${tableCellClasses.head}`]: { [`&.${tableCellClasses.head}`]: {
backgroundColor: theme.palette.action.hover color: theme.palette.grey[900]
},
[`&.${tableCellClasses.body}`]: {
fontSize: 14,
height: 64
} }
})) }))
@@ -75,11 +70,15 @@ const StyledTableRow = styled(TableRow)(() => ({
function APIKeyRow(props) { function APIKeyRow(props) {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const theme = useTheme()
return ( return (
<> <>
<TableRow sx={{ '&:last-child td, &:last-child th': { border: 0 } }}> <TableRow sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
<TableCell scope='row'>{props.apiKey.keyName}</TableCell> <StyledTableCell scope='row' style={{ width: '15%' }}>
<TableCell> {props.apiKey.keyName}
</StyledTableCell>
<StyledTableCell style={{ width: '40%' }}>
{props.showApiKeys.includes(props.apiKey.apiKey) {props.showApiKeys.includes(props.apiKey.apiKey)
? props.apiKey.apiKey ? props.apiKey.apiKey
: `${props.apiKey.apiKey.substring(0, 2)}${'•'.repeat(18)}${props.apiKey.apiKey.substring( : `${props.apiKey.apiKey.substring(0, 2)}${'•'.repeat(18)}${props.apiKey.apiKey.substring(
@@ -108,48 +107,46 @@ function APIKeyRow(props) {
Copied! Copied!
</Typography> </Typography>
</Popover> </Popover>
</TableCell> </StyledTableCell>
<TableCell> <StyledTableCell>
{props.apiKey.chatFlows.length}{' '} {props.apiKey.chatFlows.length}{' '}
{props.apiKey.chatFlows.length > 0 && ( {props.apiKey.chatFlows.length > 0 && (
<IconButton aria-label='expand row' size='small' color='inherit' onClick={() => setOpen(!open)}> <IconButton aria-label='expand row' size='small' color='inherit' onClick={() => setOpen(!open)}>
{props.apiKey.chatFlows.length > 0 && open ? <IconChevronsUp /> : <IconChevronsDown />} {props.apiKey.chatFlows.length > 0 && open ? <IconChevronsUp /> : <IconChevronsDown />}
</IconButton> </IconButton>
)} )}
</TableCell> </StyledTableCell>
<TableCell>{props.apiKey.createdAt}</TableCell> <StyledTableCell>{moment(props.apiKey.createdAt).format('MMMM Do, YYYY')}</StyledTableCell>
<TableCell> <StyledTableCell>
<IconButton title='Edit' color='primary' onClick={props.onEditClick}> <IconButton title='Edit' color='primary' onClick={props.onEditClick}>
<IconEdit /> <IconEdit />
</IconButton> </IconButton>
</TableCell> </StyledTableCell>
<TableCell> <StyledTableCell>
<IconButton title='Delete' color='error' onClick={props.onDeleteClick}> <IconButton title='Delete' color='error' onClick={props.onDeleteClick}>
<IconTrash /> <IconTrash />
</IconButton> </IconButton>
</TableCell> </StyledTableCell>
</TableRow> </TableRow>
{open && ( {open && (
<TableRow sx={{ '& td': { border: 0 } }}> <TableRow sx={{ '& td': { border: 0 } }}>
<TableCell sx={{ pb: 0, pt: 0 }} colSpan={6}> <StyledTableCell sx={{ p: 2 }} colSpan={6}>
<Collapse in={open} timeout='auto' unmountOnExit> <Collapse in={open} timeout='auto' unmountOnExit>
<Box sx={{ mt: 1, mb: 2, borderRadius: '15px', border: '1px solid' }}> <Box sx={{ borderRadius: 2, border: 1, borderColor: theme.palette.grey[900] + 25, overflow: 'hidden' }}>
<Table aria-label='chatflow table'> <Table aria-label='chatflow table'>
<TableHead> <TableHead sx={{ height: 48 }}>
<TableRow> <TableRow>
<StyledTableCell sx={{ width: '30%', borderTopLeftRadius: '15px' }}> <StyledTableCell sx={{ width: '30%' }}>Chatflow Name</StyledTableCell>
Chatflow Name
</StyledTableCell>
<StyledTableCell sx={{ width: '20%' }}>Modified On</StyledTableCell> <StyledTableCell sx={{ width: '20%' }}>Modified On</StyledTableCell>
<StyledTableCell sx={{ width: '50%', borderTopRightRadius: '15px' }}>Category</StyledTableCell> <StyledTableCell sx={{ width: '50%' }}>Category</StyledTableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{props.apiKey.chatFlows.map((flow, index) => ( {props.apiKey.chatFlows.map((flow, index) => (
<StyledTableRow key={index}> <TableRow key={index}>
<TableCell>{flow.flowName}</TableCell> <StyledTableCell>{flow.flowName}</StyledTableCell>
<TableCell>{moment(flow.updatedDate).format('DD-MMM-YY')}</TableCell> <StyledTableCell>{moment(flow.updatedDate).format('MMMM Do, YYYY')}</StyledTableCell>
<TableCell> <StyledTableCell>
&nbsp; &nbsp;
{flow.category && {flow.category &&
flow.category flow.category
@@ -157,14 +154,14 @@ function APIKeyRow(props) {
.map((tag, index) => ( .map((tag, index) => (
<Chip key={index} label={tag} style={{ marginRight: 5, marginBottom: 5 }} /> <Chip key={index} label={tag} style={{ marginRight: 5, marginBottom: 5 }} />
))} ))}
</TableCell> </StyledTableCell>
</StyledTableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
</Table> </Table>
</Box> </Box>
</Collapse> </Collapse>
</TableCell> </StyledTableCell>
</TableRow> </TableRow>
)} )}
</> </>
@@ -193,6 +190,8 @@ const APIKey = () => {
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
const [isLoading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [showDialog, setShowDialog] = useState(false) const [showDialog, setShowDialog] = useState(false)
const [dialogProps, setDialogProps] = useState({}) const [dialogProps, setDialogProps] = useState({})
const [apiKeys, setAPIKeys] = useState([]) const [apiKeys, setAPIKeys] = useState([])
@@ -315,111 +314,148 @@ const APIKey = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])
useEffect(() => {
setLoading(getAllAPIKeysApi.loading)
}, [getAllAPIKeysApi.loading])
useEffect(() => { useEffect(() => {
if (getAllAPIKeysApi.data) { if (getAllAPIKeysApi.data) {
setAPIKeys(getAllAPIKeysApi.data) setAPIKeys(getAllAPIKeysApi.data)
} }
}, [getAllAPIKeysApi.data]) }, [getAllAPIKeysApi.data])
useEffect(() => {
if (getAllAPIKeysApi.error) {
setError(getAllAPIKeysApi.error)
}
}, [getAllAPIKeysApi.error])
return ( return (
<> <>
<MainCard sx={{ background: customization.isDarkMode ? theme.palette.common.black : '' }}> <MainCard>
<Stack flexDirection='row'> {error ? (
<Box sx={{ flexGrow: 1 }}> <ErrorBoundary error={error} />
<Toolbar ) : (
disableGutters={true} <Stack flexDirection='column' sx={{ gap: 3 }}>
style={{ <ViewHeader onSearchChange={onSearchChange} search={true} searchPlaceholder='Search API Keys' title='API Keys'>
margin: 1, <StyledButton
padding: 1,
paddingBottom: 10,
display: 'flex',
justifyContent: 'space-between',
width: '100%'
}}
>
<h1>API Keys&nbsp;</h1>
<TextField
size='small'
sx={{ display: { xs: 'none', sm: 'block' }, ml: 3 }}
variant='outlined'
placeholder='Search key name'
onChange={onSearchChange}
InputProps={{
startAdornment: (
<InputAdornment position='start'>
<IconSearch />
</InputAdornment>
)
}}
/>
<Box sx={{ flexGrow: 1 }} />
<ButtonGroup
sx={{ maxHeight: 40 }}
disableElevation
variant='contained' variant='contained'
aria-label='outlined primary button group' sx={{ borderRadius: 2, height: '100%' }}
onClick={addNew}
startIcon={<IconPlus />}
id='btn_createApiKey'
> >
<ButtonGroup disableElevation aria-label='outlined primary button group'> Create Key
<StyledButton </StyledButton>
variant='contained' </ViewHeader>
sx={{ color: 'white', mr: 1, height: 37 }} {!isLoading && apiKeys.length <= 0 ? (
onClick={addNew} <Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
startIcon={<IconPlus />} <Box sx={{ p: 2, height: 'auto' }}>
id='btn_createApiKey' <img
> style={{ objectFit: 'cover', height: '16vh', width: 'auto' }}
Create Key src={APIEmptySVG}
</StyledButton> alt='APIEmptySVG'
</ButtonGroup>
</ButtonGroup>
</Toolbar>
</Box>
</Stack>
{apiKeys.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={APIEmptySVG} alt='APIEmptySVG' />
</Box>
<div>No API Keys Yet</div>
</Stack>
)}
{apiKeys.length > 0 && (
<TableContainer component={Paper}>
<Table sx={{ minWidth: 650 }} aria-label='simple table'>
<TableHead>
<TableRow>
<TableCell>Key Name</TableCell>
<TableCell>API Key</TableCell>
<TableCell>Usage</TableCell>
<TableCell>Created</TableCell>
<TableCell> </TableCell>
<TableCell> </TableCell>
</TableRow>
</TableHead>
<TableBody>
{apiKeys.filter(filterKeys).map((key, index) => (
<APIKeyRow
key={index}
apiKey={key}
showApiKeys={showApiKeys}
onCopyClick={(event) => {
navigator.clipboard.writeText(key.apiKey)
setAnchorEl(event.currentTarget)
setTimeout(() => {
handleClosePopOver()
}, 1500)
}}
onShowAPIClick={() => onShowApiKeyClick(key.apiKey)}
open={openPopOver}
anchorEl={anchorEl}
onClose={handleClosePopOver}
theme={theme}
onEditClick={() => edit(key)}
onDeleteClick={() => deleteKey(key)}
/> />
))} </Box>
</TableBody> <div>No API Keys Yet</div>
</Table> </Stack>
</TableContainer> ) : (
<TableContainer
sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2 }}
component={Paper}
>
<Table sx={{ minWidth: 650 }} aria-label='simple table'>
<TableHead
sx={{
backgroundColor: customization.isDarkMode
? theme.palette.common.black
: theme.palette.grey[100],
height: 56
}}
>
<TableRow>
<StyledTableCell>Key Name</StyledTableCell>
<StyledTableCell>API Key</StyledTableCell>
<StyledTableCell>Usage</StyledTableCell>
<StyledTableCell>Created</StyledTableCell>
<StyledTableCell> </StyledTableCell>
<StyledTableCell> </StyledTableCell>
</TableRow>
</TableHead>
<TableBody>
{isLoading ? (
<>
<StyledTableRow>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
</StyledTableRow>
<StyledTableRow>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
</StyledTableRow>
</>
) : (
<>
{apiKeys.filter(filterKeys).map((key, index) => (
<APIKeyRow
key={index}
apiKey={key}
showApiKeys={showApiKeys}
onCopyClick={(event) => {
navigator.clipboard.writeText(key.apiKey)
setAnchorEl(event.currentTarget)
setTimeout(() => {
handleClosePopOver()
}, 1500)
}}
onShowAPIClick={() => onShowApiKeyClick(key.apiKey)}
open={openPopOver}
anchorEl={anchorEl}
onClose={handleClosePopOver}
theme={theme}
onEditClick={() => edit(key)}
onDeleteClick={() => deleteKey(key)}
/>
))}
</>
)}
</TableBody>
</Table>
</TableContainer>
)}
</Stack>
)} )}
</MainCard> </MainCard>
<APIKeyDialog <APIKeyDialog
@@ -427,6 +463,7 @@ const APIKey = () => {
dialogProps={dialogProps} dialogProps={dialogProps}
onCancel={() => setShowDialog(false)} onCancel={() => setShowDialog(false)}
onConfirm={onConfirm} onConfirm={onConfirm}
setError={setError}
></APIKeyDialog> ></APIKeyDialog>
<ConfirmDialog /> <ConfirmDialog />
</> </>
@@ -68,7 +68,7 @@ const assistantAvailableModels = [
} }
] ]
const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => { const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) => {
const portalElement = document.getElementById('portal') const portalElement = document.getElementById('portal')
useNotifier() useNotifier()
const dispatch = useDispatch() const dispatch = useDispatch()
@@ -122,6 +122,18 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
} }
}, [getAssistantObjApi.data]) }, [getAssistantObjApi.data])
useEffect(() => {
if (getAssistantObjApi.error) {
syncData(getAssistantObjApi.error)
}
}, [getAssistantObjApi.error])
useEffect(() => {
if (getSpecificAssistantApi.error) {
syncData(getSpecificAssistantApi.error)
}
}, [getSpecificAssistantApi.error])
useEffect(() => { useEffect(() => {
if (dialogProps.type === 'EDIT' && dialogProps.data) { if (dialogProps.type === 'EDIT' && dialogProps.data) {
// When assistant dialog is opened from Assistants dashboard // When assistant dialog is opened from Assistants dashboard
@@ -235,6 +247,7 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
} }
setLoading(false) setLoading(false)
} catch (error) { } catch (error) {
setError(error)
enqueueSnackbar({ enqueueSnackbar({
message: `Failed to add new Assistant: ${error.response.data.message}`, message: `Failed to add new Assistant: ${error.response.data.message}`,
options: { options: {
@@ -288,6 +301,7 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
} }
setLoading(false) setLoading(false)
} catch (error) { } catch (error) {
setError(error)
enqueueSnackbar({ enqueueSnackbar({
message: `Failed to save Assistant: ${error.response.data.message}`, message: `Failed to save Assistant: ${error.response.data.message}`,
options: { options: {
@@ -327,6 +341,7 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
} }
setLoading(false) setLoading(false)
} catch (error) { } catch (error) {
setError(error)
enqueueSnackbar({ enqueueSnackbar({
message: `Failed to sync Assistant: ${error.response.data.message}`, message: `Failed to sync Assistant: ${error.response.data.message}`,
options: { options: {
@@ -373,6 +388,7 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
onConfirm() onConfirm()
} }
} catch (error) { } catch (error) {
setError(error)
enqueueSnackbar({ enqueueSnackbar({
message: `Failed to delete Assistant: ${error.response.data.message}`, message: `Failed to delete Assistant: ${error.response.data.message}`,
options: { options: {
@@ -403,214 +419,193 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
aria-labelledby='alert-dialog-title' aria-labelledby='alert-dialog-title'
aria-describedby='alert-dialog-description' aria-describedby='alert-dialog-description'
> >
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'> <DialogTitle sx={{ fontSize: '1rem', p: 3, pb: 0 }} id='alert-dialog-title'>
{dialogProps.title} {dialogProps.title}
</DialogTitle> </DialogTitle>
<DialogContent> <DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2, maxHeight: '75vh', position: 'relative', px: 3, pb: 3 }}>
<Box sx={{ p: 2 }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: 2 }}>
<Stack sx={{ position: 'relative' }} direction='row'> <Box>
<Typography variant='overline'> <Stack sx={{ position: 'relative', alignItems: 'center' }} direction='row'>
Assistant Name <Typography variant='overline'>Assistant Name</Typography>
<TooltipWithParser <TooltipWithParser title={'The name of the assistant. The maximum length is 256 characters.'} />
style={{ marginLeft: 10 }} </Stack>
title={'The name of the assistant. The maximum length is 256 characters.'} <OutlinedInput
/> id='assistantName'
</Typography> type='string'
</Stack> fullWidth
<OutlinedInput placeholder='My New Assistant'
id='assistantName' value={assistantName}
type='string' name='assistantName'
fullWidth onChange={(e) => setAssistantName(e.target.value)}
placeholder='My New Assistant'
value={assistantName}
name='assistantName'
onChange={(e) => setAssistantName(e.target.value)}
/>
</Box>
<Box sx={{ p: 2 }}>
<Stack sx={{ position: 'relative' }} direction='row'>
<Typography variant='overline'>
Assistant Description
<TooltipWithParser
style={{ marginLeft: 10 }}
title={'The description of the assistant. The maximum length is 512 characters.'}
/>
</Typography>
</Stack>
<OutlinedInput
id='assistantDesc'
type='string'
fullWidth
placeholder='Description of what the Assistant does'
multiline={true}
rows={3}
value={assistantDesc}
name='assistantDesc'
onChange={(e) => setAssistantDesc(e.target.value)}
/>
</Box>
<Box sx={{ p: 2 }}>
<Stack sx={{ position: 'relative' }} direction='row'>
<Typography variant='overline'>Assistant Icon Src</Typography>
</Stack>
<div
style={{
width: 100,
height: 100,
borderRadius: '50%',
backgroundColor: 'white'
}}
>
<img
style={{
width: '100%',
height: '100%',
padding: 5,
borderRadius: '50%',
objectFit: 'contain'
}}
alt={assistantName}
src={assistantIcon}
/> />
</div> </Box>
<OutlinedInput <Box>
id='assistantIcon' <Stack sx={{ position: 'relative', alignItems: 'center' }} direction='row'>
type='string' <Typography variant='overline'>Assistant Description</Typography>
fullWidth <TooltipWithParser title={'The description of the assistant. The maximum length is 512 characters.'} />
placeholder={`https://api.dicebear.com/7.x/bottts/svg?seed=${uuidv4()}`} </Stack>
value={assistantIcon} <OutlinedInput
name='assistantIcon' id='assistantDesc'
onChange={(e) => setAssistantIcon(e.target.value)} type='string'
/> fullWidth
</Box> placeholder='Description of what the Assistant does'
<Box sx={{ p: 2 }}> multiline={true}
<Stack sx={{ position: 'relative' }} direction='row'> rows={3}
<Typography variant='overline'> value={assistantDesc}
Assistant Model name='assistantDesc'
<span style={{ color: 'red' }}>&nbsp;*</span> onChange={(e) => setAssistantDesc(e.target.value)}
</Typography> />
</Stack> </Box>
<Dropdown <Box>
key={assistantModel} <Stack sx={{ position: 'relative' }} direction='row'>
name={assistantModel} <Typography variant='overline'>Assistant Icon Src</Typography>
options={assistantAvailableModels} </Stack>
onSelect={(newValue) => setAssistantModel(newValue)} <div
value={assistantModel ?? 'choose an option'} style={{
/> width: 100,
</Box> height: 100,
<Box sx={{ p: 2 }}> borderRadius: '50%',
<Stack sx={{ position: 'relative' }} direction='row'> backgroundColor: 'white'
<Typography variant='overline'> }}
OpenAI Credential >
<span style={{ color: 'red' }}>&nbsp;*</span> <img
</Typography> style={{
</Stack> width: '100%',
<CredentialInputHandler height: '100%',
key={assistantCredential} padding: 5,
data={assistantCredential ? { credential: assistantCredential } : {}} borderRadius: '50%',
inputParam={{ objectFit: 'contain'
label: 'Connect Credential', }}
name: 'credential', alt={assistantName}
type: 'credential', src={assistantIcon}
credentialNames: ['openAIApi'] />
}} </div>
onSelect={(newValue) => setAssistantCredential(newValue)} <OutlinedInput
/> id='assistantIcon'
</Box> type='string'
<Box sx={{ p: 2 }}> fullWidth
<Stack sx={{ position: 'relative' }} direction='row'> placeholder={`https://api.dicebear.com/7.x/bottts/svg?seed=${uuidv4()}`}
<Typography variant='overline'> value={assistantIcon}
Assistant Instruction name='assistantIcon'
onChange={(e) => setAssistantIcon(e.target.value)}
/>
</Box>
<Box>
<Stack sx={{ position: 'relative' }} direction='row'>
<Typography variant='overline'>
Assistant Model
<span style={{ color: 'red' }}>&nbsp;*</span>
</Typography>
</Stack>
<Dropdown
key={assistantModel}
name={assistantModel}
options={assistantAvailableModels}
onSelect={(newValue) => setAssistantModel(newValue)}
value={assistantModel ?? 'choose an option'}
/>
</Box>
<Box>
<Stack sx={{ position: 'relative' }} direction='row'>
<Typography variant='overline'>
OpenAI Credential
<span style={{ color: 'red' }}>&nbsp;*</span>
</Typography>
</Stack>
<CredentialInputHandler
key={assistantCredential}
data={assistantCredential ? { credential: assistantCredential } : {}}
inputParam={{
label: 'Connect Credential',
name: 'credential',
type: 'credential',
credentialNames: ['openAIApi']
}}
onSelect={(newValue) => setAssistantCredential(newValue)}
/>
</Box>
<Box>
<Stack sx={{ position: 'relative', alignItems: 'center' }} direction='row'>
<Typography variant='overline'>Assistant Instruction</Typography>
<TooltipWithParser <TooltipWithParser
style={{ marginLeft: 10 }}
title={'The system instructions that the assistant uses. The maximum length is 32768 characters.'} title={'The system instructions that the assistant uses. The maximum length is 32768 characters.'}
/> />
</Typography> </Stack>
</Stack> <OutlinedInput
<OutlinedInput id='assistantInstructions'
id='assistantInstructions' type='string'
type='string' fullWidth
fullWidth placeholder='You are a personal math tutor. When asked a question, write and run Python code to answer the question.'
placeholder='You are a personal math tutor. When asked a question, write and run Python code to answer the question.' multiline={true}
multiline={true} rows={3}
rows={3} value={assistantInstructions}
value={assistantInstructions} name='assistantInstructions'
name='assistantInstructions' onChange={(e) => setAssistantInstructions(e.target.value)}
onChange={(e) => setAssistantInstructions(e.target.value)} />
/> </Box>
</Box> <Box>
<Box sx={{ p: 2 }}> <Stack sx={{ position: 'relative', alignItems: 'center' }} direction='row'>
<Stack sx={{ position: 'relative' }} direction='row'> <Typography variant='overline'>Assistant Tools</Typography>
<Typography variant='overline'> <TooltipWithParser title='A list of tool enabled on the assistant. There can be a maximum of 128 tools per assistant.' />
Assistant Tools </Stack>
<TooltipWithParser <MultiDropdown
style={{ marginLeft: 10 }} key={JSON.stringify(assistantTools)}
title='A list of tool enabled on the assistant. There can be a maximum of 128 tools per assistant.' name={JSON.stringify(assistantTools)}
/> options={[
</Typography> {
</Stack> label: 'Code Interpreter',
<MultiDropdown name: 'code_interpreter'
key={JSON.stringify(assistantTools)} },
name={JSON.stringify(assistantTools)} {
options={[ label: 'Retrieval',
{ name: 'retrieval'
label: 'Code Interpreter', }
name: 'code_interpreter' ]}
}, onSelect={(newValue) => (newValue ? setAssistantTools(JSON.parse(newValue)) : setAssistantTools([]))}
{ value={assistantTools ?? 'choose an option'}
label: 'Retrieval', />
name: 'retrieval' </Box>
} <Box>
]} <Stack sx={{ position: 'relative', alignItems: 'center' }} direction='row'>
onSelect={(newValue) => (newValue ? setAssistantTools(JSON.parse(newValue)) : setAssistantTools([]))} <Typography variant='overline'>Knowledge Files</Typography>
value={assistantTools ?? 'choose an option'} <TooltipWithParser title='Allow assistant to use the content from uploaded files for retrieval and code interpreter. MAX: 20 files' />
/> </Stack>
</Box> <div style={{ display: 'flex', flexDirection: 'row' }}>
<Box sx={{ p: 2 }}> {assistantFiles.map((file, index) => (
<Stack sx={{ position: 'relative' }} direction='row'> <div
<Typography variant='overline'> key={index}
Knowledge Files style={{
<TooltipWithParser display: 'flex',
style={{ marginLeft: 10 }} flexDirection: 'row',
title='Allow assistant to use the content from uploaded files for retrieval and code interpreter. MAX: 20 files' alignItems: 'center',
/> width: 'max-content',
</Typography> height: 'max-content',
</Stack> borderRadius: 15,
<div style={{ display: 'flex', flexDirection: 'row' }}> background: 'rgb(254,252,191)',
{assistantFiles.map((file, index) => ( paddingLeft: 15,
<div paddingRight: 15,
key={index} paddingTop: 5,
style={{ paddingBottom: 5,
display: 'flex', marginRight: 10
flexDirection: 'row', }}
alignItems: 'center', >
width: 'max-content', <span style={{ color: 'rgb(116,66,16)', marginRight: 10 }}>{file.filename}</span>
height: 'max-content', <IconButton sx={{ height: 15, width: 15, p: 0 }} onClick={() => onFileDeleteClick(file.id)}>
borderRadius: 15, <IconX />
background: 'rgb(254,252,191)', </IconButton>
paddingLeft: 15, </div>
paddingRight: 15, ))}
paddingTop: 5, </div>
paddingBottom: 5, <File
marginRight: 10 key={uploadAssistantFiles}
}} fileType='*'
> onChange={(newValue) => setUploadAssistantFiles(newValue)}
<span style={{ color: 'rgb(116,66,16)', marginRight: 10 }}>{file.filename}</span> value={uploadAssistantFiles ?? 'Choose a file to upload'}
<IconButton sx={{ height: 15, width: 15, p: 0 }} onClick={() => onFileDeleteClick(file.id)}> />
<IconX /> </Box>
</IconButton>
</div>
))}
</div>
<File
key={uploadAssistantFiles}
fileType='*'
onChange={(newValue) => setUploadAssistantFiles(newValue)}
value={uploadAssistantFiles ?? 'Choose a file to upload'}
/>
</Box> </Box>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions sx={{ p: 3, pt: 0 }}>
{dialogProps.type === 'EDIT' && ( {dialogProps.type === 'EDIT' && (
<StyledButton color='secondary' variant='contained' onClick={() => onSyncClick()}> <StyledButton color='secondary' variant='contained' onClick={() => onSyncClick()}>
Sync Sync
@@ -8,7 +8,7 @@ import { StyledButton } from '@/ui-component/button/StyledButton'
import assistantsApi from '@/api/assistants' import assistantsApi from '@/api/assistants'
import useApi from '@/hooks/useApi' import useApi from '@/hooks/useApi'
const LoadAssistantDialog = ({ show, dialogProps, onCancel, onAssistantSelected }) => { const LoadAssistantDialog = ({ show, dialogProps, onCancel, onAssistantSelected, setError }) => {
const portalElement = document.getElementById('portal') const portalElement = document.getElementById('portal')
const getAllAvailableAssistantsApi = useApi(assistantsApi.getAllAvailableAssistants) const getAllAvailableAssistantsApi = useApi(assistantsApi.getAllAvailableAssistants)
@@ -39,6 +39,13 @@ const LoadAssistantDialog = ({ show, dialogProps, onCancel, onAssistantSelected
} }
}, [getAllAvailableAssistantsApi.data]) }, [getAllAvailableAssistantsApi.data])
useEffect(() => {
if (getAllAvailableAssistantsApi.error) {
setError(getAllAvailableAssistantsApi.error)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getAllAvailableAssistantsApi.error])
const component = show ? ( const component = show ? (
<Dialog <Dialog
fullWidth fullWidth
@@ -108,7 +115,8 @@ LoadAssistantDialog.propTypes = {
show: PropTypes.bool, show: PropTypes.bool,
dialogProps: PropTypes.object, dialogProps: PropTypes.object,
onCancel: PropTypes.func, onCancel: PropTypes.func,
onAssistantSelected: PropTypes.func onAssistantSelected: PropTypes.func,
setError: PropTypes.func
} }
export default LoadAssistantDialog export default LoadAssistantDialog
+71 -40
View File
@@ -1,9 +1,7 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useSelector } from 'react-redux'
// material-ui // material-ui
import { Grid, Box, Stack, Button } from '@mui/material' import { Box, Stack, Button, Skeleton } from '@mui/material'
import { useTheme } from '@mui/material/styles'
// project imports // project imports
import MainCard from '@/ui-component/cards/MainCard' import MainCard from '@/ui-component/cards/MainCard'
@@ -21,16 +19,17 @@ import assistantsApi from '@/api/assistants'
import useApi from '@/hooks/useApi' import useApi from '@/hooks/useApi'
// icons // icons
import { IconPlus, IconFileImport } from '@tabler/icons' import { IconPlus, IconFileUpload } from '@tabler/icons'
import ViewHeader from '@/layout/MainLayout/ViewHeader'
import ErrorBoundary from '@/ErrorBoundary'
// ==============================|| CHATFLOWS ||============================== // // ==============================|| CHATFLOWS ||============================== //
const Assistants = () => { const Assistants = () => {
const theme = useTheme()
const customization = useSelector((state) => state.customization)
const getAllAssistantsApi = useApi(assistantsApi.getAllAssistants) const getAllAssistantsApi = useApi(assistantsApi.getAllAssistants)
const [isLoading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [showDialog, setShowDialog] = useState(false) const [showDialog, setShowDialog] = useState(false)
const [dialogProps, setDialogProps] = useState({}) const [dialogProps, setDialogProps] = useState({})
const [showLoadDialog, setShowLoadDialog] = useState(false) const [showLoadDialog, setShowLoadDialog] = useState(false)
@@ -85,45 +84,75 @@ const Assistants = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])
useEffect(() => {
setLoading(getAllAssistantsApi.loading)
}, [getAllAssistantsApi.loading])
useEffect(() => {
if (getAllAssistantsApi.error) {
setError(getAllAssistantsApi.error)
}
}, [getAllAssistantsApi.error])
return ( return (
<> <>
<MainCard sx={{ background: customization.isDarkMode ? theme.palette.common.black : '' }}> <MainCard>
<Stack flexDirection='row'> {error ? (
<Grid sx={{ mb: 1.25 }} container direction='row'> <ErrorBoundary error={error} />
<h1>OpenAI Assistants</h1> ) : (
<Box sx={{ flexGrow: 1 }} /> <Stack flexDirection='column' sx={{ gap: 3 }}>
<Grid item> <ViewHeader title='OpenAI Assistants'>
<Button variant='outlined' sx={{ mr: 2 }} onClick={loadExisting} startIcon={<IconFileImport />}> <Button
variant='outlined'
onClick={loadExisting}
startIcon={<IconFileUpload />}
sx={{ borderRadius: 2, height: 40 }}
>
Load Load
</Button> </Button>
<StyledButton variant='contained' sx={{ color: 'white' }} onClick={addNew} startIcon={<IconPlus />}> <StyledButton
variant='contained'
sx={{ borderRadius: 2, height: 40 }}
onClick={addNew}
startIcon={<IconPlus />}
>
Add Add
</StyledButton> </StyledButton>
</Grid> </ViewHeader>
</Grid> {isLoading ? (
</Stack> <Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
<Grid container spacing={gridSpacing}> <Skeleton variant='rounded' height={160} />
{!getAllAssistantsApi.loading && <Skeleton variant='rounded' height={160} />
getAllAssistantsApi.data && <Skeleton variant='rounded' height={160} />
getAllAssistantsApi.data.map((data, index) => ( </Box>
<Grid key={index} item lg={3} md={4} sm={6} xs={12}> ) : (
<ItemCard <Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
data={{ {getAllAssistantsApi.data &&
name: JSON.parse(data.details)?.name, getAllAssistantsApi.data.map((data, index) => (
description: JSON.parse(data.details)?.instructions, <ItemCard
iconSrc: data.iconSrc data={{
}} name: JSON.parse(data.details)?.name,
onClick={() => edit(data)} description: JSON.parse(data.details)?.instructions,
/> iconSrc: data.iconSrc
</Grid> }}
))} key={index}
</Grid> onClick={() => edit(data)}
{!getAllAssistantsApi.loading && (!getAllAssistantsApi.data || getAllAssistantsApi.data.length === 0) && ( />
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'> ))}
<Box sx={{ p: 2, height: 'auto' }}> </Box>
<img style={{ objectFit: 'cover', height: '30vh', width: 'auto' }} src={ToolEmptySVG} alt='ToolEmptySVG' /> )}
</Box> {!isLoading && (!getAllAssistantsApi.data || getAllAssistantsApi.data.length === 0) && (
<div>No Assistants Added Yet</div> <Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
<Box sx={{ p: 2, height: 'auto' }}>
<img
style={{ objectFit: 'cover', height: '16vh', width: 'auto' }}
src={ToolEmptySVG}
alt='ToolEmptySVG'
/>
</Box>
<div>No Assistants Added Yet</div>
</Stack>
)}
</Stack> </Stack>
)} )}
</MainCard> </MainCard>
@@ -132,12 +161,14 @@ const Assistants = () => {
dialogProps={loadDialogProps} dialogProps={loadDialogProps}
onCancel={() => setShowLoadDialog(false)} onCancel={() => setShowLoadDialog(false)}
onAssistantSelected={onAssistantSelected} onAssistantSelected={onAssistantSelected}
setError={setError}
></LoadAssistantDialog> ></LoadAssistantDialog>
<AssistantDialog <AssistantDialog
show={showDialog} show={showDialog}
dialogProps={dialogProps} dialogProps={dialogProps}
onCancel={() => setShowDialog(false)} onCancel={() => setShowDialog(false)}
onConfirm={onConfirm} onConfirm={onConfirm}
setError={setError}
></AssistantDialog> ></AssistantDialog>
</> </>
) )
+156 -150
View File
@@ -192,186 +192,192 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
return ( return (
<> <>
<Box> <Stack flexDirection='row' justifyContent='space-between' sx={{ width: '100%' }}>
<ButtonBase title='Back' sx={{ borderRadius: '50%' }}> <Stack flexDirection='row' sx={{ width: '100%', maxWidth: '50%' }}>
<Avatar <Box>
variant='rounded' <ButtonBase title='Back' sx={{ borderRadius: '50%' }}>
sx={{ <Avatar
...theme.typography.commonAvatar, variant='rounded'
...theme.typography.mediumAvatar, sx={{
transition: 'all .2s ease-in-out', ...theme.typography.commonAvatar,
background: theme.palette.secondary.light, ...theme.typography.mediumAvatar,
color: theme.palette.secondary.dark, transition: 'all .2s ease-in-out',
'&:hover': { background: theme.palette.secondary.light,
background: theme.palette.secondary.dark, color: theme.palette.secondary.dark,
color: theme.palette.secondary.light '&:hover': {
} background: theme.palette.secondary.dark,
}} color: theme.palette.secondary.light
color='inherit' }
onClick={() => }}
window.history.state && window.history.state.idx > 0 ? navigate(-1) : navigate('/', { replace: true }) color='inherit'
} onClick={() =>
> window.history.state && window.history.state.idx > 0 ? navigate(-1) : navigate('/', { replace: true })
<IconChevronLeft stroke={1.5} size='1.3rem' /> }
</Avatar> >
</ButtonBase> <IconChevronLeft stroke={1.5} size='1.3rem' />
</Box> </Avatar>
<Box sx={{ flexGrow: 1 }}> </ButtonBase>
{!isEditingFlowName && ( </Box>
<Stack flexDirection='row'> <Box sx={{ width: '100%' }}>
<Typography {!isEditingFlowName ? (
sx={{ <Stack flexDirection='row'>
fontSize: '1.5rem', <Typography
fontWeight: 600,
ml: 2
}}
>
{canvas.isDirty && <strong style={{ color: theme.palette.orange.main }}>*</strong>} {flowName}
</Typography>
{chatflow?.id && (
<ButtonBase title='Edit Name' sx={{ borderRadius: '50%' }}>
<Avatar
variant='rounded'
sx={{ sx={{
...theme.typography.commonAvatar, fontSize: '1.5rem',
...theme.typography.mediumAvatar, fontWeight: 600,
transition: 'all .2s ease-in-out', ml: 2,
ml: 1, textOverflow: 'ellipsis',
background: theme.palette.secondary.light, overflow: 'hidden',
color: theme.palette.secondary.dark, whiteSpace: 'nowrap'
'&:hover': {
background: theme.palette.secondary.dark,
color: theme.palette.secondary.light
}
}} }}
color='inherit'
onClick={() => setEditingFlowName(true)}
> >
<IconPencil stroke={1.5} size='1.3rem' /> {canvas.isDirty && <strong style={{ color: theme.palette.orange.main }}>*</strong>} {flowName}
</Avatar> </Typography>
</ButtonBase> {chatflow?.id && (
<ButtonBase title='Edit Name' sx={{ borderRadius: '50%' }}>
<Avatar
variant='rounded'
sx={{
...theme.typography.commonAvatar,
...theme.typography.mediumAvatar,
transition: 'all .2s ease-in-out',
ml: 1,
background: theme.palette.secondary.light,
color: theme.palette.secondary.dark,
'&:hover': {
background: theme.palette.secondary.dark,
color: theme.palette.secondary.light
}
}}
color='inherit'
onClick={() => setEditingFlowName(true)}
>
<IconPencil stroke={1.5} size='1.3rem' />
</Avatar>
</ButtonBase>
)}
</Stack>
) : (
<Stack flexDirection='row' sx={{ width: '100%' }}>
<TextField
size='small'
inputRef={flowNameRef}
sx={{
width: '100%',
ml: 2
}}
defaultValue={flowName}
/>
<ButtonBase title='Save Name' sx={{ borderRadius: '50%' }}>
<Avatar
variant='rounded'
sx={{
...theme.typography.commonAvatar,
...theme.typography.mediumAvatar,
transition: 'all .2s ease-in-out',
background: theme.palette.success.light,
color: theme.palette.success.dark,
ml: 1,
'&:hover': {
background: theme.palette.success.dark,
color: theme.palette.success.light
}
}}
color='inherit'
onClick={submitFlowName}
>
<IconCheck stroke={1.5} size='1.3rem' />
</Avatar>
</ButtonBase>
<ButtonBase title='Cancel' sx={{ borderRadius: '50%' }}>
<Avatar
variant='rounded'
sx={{
...theme.typography.commonAvatar,
...theme.typography.mediumAvatar,
transition: 'all .2s ease-in-out',
background: theme.palette.error.light,
color: theme.palette.error.dark,
ml: 1,
'&:hover': {
background: theme.palette.error.dark,
color: theme.palette.error.light
}
}}
color='inherit'
onClick={() => setEditingFlowName(false)}
>
<IconX stroke={1.5} size='1.3rem' />
</Avatar>
</ButtonBase>
</Stack>
)} )}
</Stack> </Box>
)} </Stack>
{isEditingFlowName && ( <Box>
<Stack flexDirection='row'> {chatflow?.id && (
<TextField <ButtonBase title='API Endpoint' sx={{ borderRadius: '50%', mr: 2 }}>
size='small'
inputRef={flowNameRef}
sx={{
width: '50%',
ml: 2
}}
defaultValue={flowName}
/>
<ButtonBase title='Save Name' sx={{ borderRadius: '50%' }}>
<Avatar <Avatar
variant='rounded' variant='rounded'
sx={{ sx={{
...theme.typography.commonAvatar, ...theme.typography.commonAvatar,
...theme.typography.mediumAvatar, ...theme.typography.mediumAvatar,
transition: 'all .2s ease-in-out', transition: 'all .2s ease-in-out',
background: theme.palette.success.light, background: theme.palette.canvasHeader.deployLight,
color: theme.palette.success.dark, color: theme.palette.canvasHeader.deployDark,
ml: 1,
'&:hover': { '&:hover': {
background: theme.palette.success.dark, background: theme.palette.canvasHeader.deployDark,
color: theme.palette.success.light color: theme.palette.canvasHeader.deployLight
} }
}} }}
color='inherit' color='inherit'
onClick={submitFlowName} onClick={onAPIDialogClick}
> >
<IconCheck stroke={1.5} size='1.3rem' /> <IconCode stroke={1.5} size='1.3rem' />
</Avatar> </Avatar>
</ButtonBase> </ButtonBase>
<ButtonBase title='Cancel' sx={{ borderRadius: '50%' }}> )}
<Avatar <ButtonBase title='Save Chatflow' sx={{ borderRadius: '50%', mr: 2 }}>
variant='rounded'
sx={{
...theme.typography.commonAvatar,
...theme.typography.mediumAvatar,
transition: 'all .2s ease-in-out',
background: theme.palette.error.light,
color: theme.palette.error.dark,
ml: 1,
'&:hover': {
background: theme.palette.error.dark,
color: theme.palette.error.light
}
}}
color='inherit'
onClick={() => setEditingFlowName(false)}
>
<IconX stroke={1.5} size='1.3rem' />
</Avatar>
</ButtonBase>
</Stack>
)}
</Box>
<Box>
{chatflow?.id && (
<ButtonBase title='API Endpoint' sx={{ borderRadius: '50%', mr: 2 }}>
<Avatar <Avatar
variant='rounded' variant='rounded'
sx={{ sx={{
...theme.typography.commonAvatar, ...theme.typography.commonAvatar,
...theme.typography.mediumAvatar, ...theme.typography.mediumAvatar,
transition: 'all .2s ease-in-out', transition: 'all .2s ease-in-out',
background: theme.palette.canvasHeader.deployLight, background: theme.palette.canvasHeader.saveLight,
color: theme.palette.canvasHeader.deployDark, color: theme.palette.canvasHeader.saveDark,
'&:hover': { '&:hover': {
background: theme.palette.canvasHeader.deployDark, background: theme.palette.canvasHeader.saveDark,
color: theme.palette.canvasHeader.deployLight color: theme.palette.canvasHeader.saveLight
} }
}} }}
color='inherit' color='inherit'
onClick={onAPIDialogClick} onClick={onSaveChatflowClick}
> >
<IconCode stroke={1.5} size='1.3rem' /> <IconDeviceFloppy stroke={1.5} size='1.3rem' />
</Avatar> </Avatar>
</ButtonBase> </ButtonBase>
)} <ButtonBase ref={settingsRef} title='Settings' sx={{ borderRadius: '50%' }}>
<ButtonBase title='Save Chatflow' sx={{ borderRadius: '50%', mr: 2 }}> <Avatar
<Avatar variant='rounded'
variant='rounded' sx={{
sx={{ ...theme.typography.commonAvatar,
...theme.typography.commonAvatar, ...theme.typography.mediumAvatar,
...theme.typography.mediumAvatar, transition: 'all .2s ease-in-out',
transition: 'all .2s ease-in-out', background: theme.palette.canvasHeader.settingsLight,
background: theme.palette.canvasHeader.saveLight, color: theme.palette.canvasHeader.settingsDark,
color: theme.palette.canvasHeader.saveDark, '&:hover': {
'&:hover': { background: theme.palette.canvasHeader.settingsDark,
background: theme.palette.canvasHeader.saveDark, color: theme.palette.canvasHeader.settingsLight
color: theme.palette.canvasHeader.saveLight }
} }}
}} onClick={() => setSettingsOpen(!isSettingsOpen)}
color='inherit' >
onClick={onSaveChatflowClick} <IconSettings stroke={1.5} size='1.3rem' />
> </Avatar>
<IconDeviceFloppy stroke={1.5} size='1.3rem' /> </ButtonBase>
</Avatar> </Box>
</ButtonBase> </Stack>
<ButtonBase ref={settingsRef} title='Settings' sx={{ borderRadius: '50%' }}>
<Avatar
variant='rounded'
sx={{
...theme.typography.commonAvatar,
...theme.typography.mediumAvatar,
transition: 'all .2s ease-in-out',
background: theme.palette.canvasHeader.settingsLight,
color: theme.palette.canvasHeader.settingsDark,
'&:hover': {
background: theme.palette.canvasHeader.settingsDark,
color: theme.palette.canvasHeader.settingsLight
}
}}
onClick={() => setSettingsOpen(!isSettingsOpen)}
>
<IconSettings stroke={1.5} size='1.3rem' />
</Avatar>
</ButtonBase>
</Box>
<Settings <Settings
chatflow={chatflow} chatflow={chatflow}
isSettingsOpen={isSettingsOpen} isSettingsOpen={isSettingsOpen}
@@ -97,29 +97,26 @@ const CredentialInputHandler = ({ inputParam, data, onSelect, disabled = false }
{inputParam && ( {inputParam && (
<> <>
{inputParam.type === 'credential' && ( {inputParam.type === 'credential' && (
<> <div key={reloadTimestamp} style={{ display: 'flex', flexDirection: 'row' }}>
<div style={{ marginTop: 10 }} /> <AsyncDropdown
<div key={reloadTimestamp} style={{ display: 'flex', flexDirection: 'row' }}> disabled={disabled}
<AsyncDropdown name={inputParam.name}
disabled={disabled} nodeData={data}
name={inputParam.name} value={credentialId ?? 'choose an option'}
nodeData={data} isCreateNewOption={true}
value={credentialId ?? 'choose an option'} credentialNames={inputParam.credentialNames}
isCreateNewOption={true} onSelect={(newValue) => {
credentialNames={inputParam.credentialNames} setCredentialId(newValue)
onSelect={(newValue) => { onSelect(newValue)
setCredentialId(newValue) }}
onSelect(newValue) onCreateNew={() => addAsyncOption(inputParam.name)}
}} />
onCreateNew={() => addAsyncOption(inputParam.name)} {credentialId && (
/> <IconButton title='Edit' color='primary' size='small' onClick={() => editCredential(credentialId)}>
{credentialId && ( <IconEdit />
<IconButton title='Edit' color='primary' size='small' onClick={() => editCredential(credentialId)}> </IconButton>
<IconEdit /> )}
</IconButton> </div>
)}
</div>
</>
)} )}
</> </>
)} )}
+87 -89
View File
@@ -1,9 +1,8 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { useSelector } from 'react-redux'
// material-ui // material-ui
import { Grid, Box, Stack, Toolbar, ToggleButton, ButtonGroup, InputAdornment, TextField } from '@mui/material' import { Box, Skeleton, Stack, ToggleButton } from '@mui/material'
import { useTheme } from '@mui/material/styles' import { useTheme } from '@mui/material/styles'
// project imports // project imports
@@ -24,20 +23,22 @@ import useApi from '@/hooks/useApi'
import { baseURL } from '@/store/constant' import { baseURL } from '@/store/constant'
// icons // icons
import { IconPlus, IconSearch, IconLayoutGrid, IconList } from '@tabler/icons' import { IconPlus, IconLayoutGrid, IconList } from '@tabler/icons'
import * as React from 'react' import * as React from 'react'
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup' import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'
import { FlowListTable } from '@/ui-component/table/FlowListTable' import { FlowListTable } from '@/ui-component/table/FlowListTable'
import { StyledButton } from '@/ui-component/button/StyledButton' import { StyledButton } from '@/ui-component/button/StyledButton'
import ViewHeader from '@/layout/MainLayout/ViewHeader'
import ErrorBoundary from '@/ErrorBoundary'
// ==============================|| CHATFLOWS ||============================== // // ==============================|| CHATFLOWS ||============================== //
const Chatflows = () => { const Chatflows = () => {
const navigate = useNavigate() const navigate = useNavigate()
const theme = useTheme() const theme = useTheme()
const customization = useSelector((state) => state.customization)
const [isLoading, setLoading] = useState(true) const [isLoading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [images, setImages] = useState({}) const [images, setImages] = useState({})
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
const [loginDialogOpen, setLoginDialogOpen] = useState(false) const [loginDialogOpen, setLoginDialogOpen] = useState(false)
@@ -91,6 +92,8 @@ const Chatflows = () => {
confirmButtonName: 'Login' confirmButtonName: 'Login'
}) })
setLoginDialogOpen(true) setLoginDialogOpen(true)
} else {
setError(getAllChatflowsApi.error)
} }
} }
}, [getAllChatflowsApi.error]) }, [getAllChatflowsApi.error])
@@ -124,94 +127,89 @@ const Chatflows = () => {
}, [getAllChatflowsApi.data]) }, [getAllChatflowsApi.data])
return ( return (
<MainCard sx={{ background: customization.isDarkMode ? theme.palette.common.black : '' }}> <MainCard>
<Stack flexDirection='column'> {error ? (
<Box sx={{ flexGrow: 1 }}> <ErrorBoundary error={error} />
<Toolbar ) : (
disableGutters={true} <Stack flexDirection='column' sx={{ gap: 3 }}>
style={{ <ViewHeader onSearchChange={onSearchChange} search={true} searchPlaceholder='Search Name or Category' title='Chatflows'>
margin: 1, <ToggleButtonGroup
padding: 1, sx={{ borderRadius: 2, maxHeight: 40 }}
paddingBottom: 10, value={view}
display: 'flex', color='primary'
justifyContent: 'space-between', exclusive
width: '100%' onChange={handleChange}
}} >
> <ToggleButton
<h1>Chatflows</h1> sx={{
<TextField borderColor: theme.palette.grey[900] + 25,
size='small' borderRadius: 2,
sx={{ display: { xs: 'none', sm: 'block' }, ml: 3 }} color: theme?.customization?.isDarkMode ? 'white' : 'inherit'
variant='outlined' }}
placeholder='Search name or category' variant='contained'
onChange={onSearchChange} value='card'
InputProps={{ title='Card View'
startAdornment: ( >
<InputAdornment position='start'> <IconLayoutGrid />
<IconSearch /> </ToggleButton>
</InputAdornment> <ToggleButton
) sx={{
}} borderColor: theme.palette.grey[900] + 25,
borderRadius: 2,
color: theme?.customization?.isDarkMode ? 'white' : 'inherit'
}}
variant='contained'
value='list'
title='List View'
>
<IconList />
</ToggleButton>
</ToggleButtonGroup>
<StyledButton variant='contained' onClick={addNew} startIcon={<IconPlus />} sx={{ borderRadius: 2, height: 40 }}>
Add New
</StyledButton>
</ViewHeader>
{!view || view === 'card' ? (
<>
{isLoading && !getAllChatflowsApi.data ? (
<Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
<Skeleton variant='rounded' height={160} />
<Skeleton variant='rounded' height={160} />
<Skeleton variant='rounded' height={160} />
</Box>
) : (
<Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
{getAllChatflowsApi.data?.filter(filterFlows).map((data, index) => (
<ItemCard key={index} onClick={() => goToCanvas(data)} data={data} images={images[data.id]} />
))}
</Box>
)}
</>
) : (
<FlowListTable
data={getAllChatflowsApi.data}
images={images}
isLoading={isLoading}
filterFunction={filterFlows}
updateFlowsApi={getAllChatflowsApi}
setError={setError}
/> />
<Box sx={{ flexGrow: 1 }} /> )}
<ButtonGroup sx={{ maxHeight: 40 }} disableElevation variant='contained' aria-label='outlined primary button group'> {!isLoading && (!getAllChatflowsApi.data || getAllChatflowsApi.data.length === 0) && (
<ButtonGroup disableElevation variant='contained' aria-label='outlined primary button group'> <Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
<ToggleButtonGroup sx={{ maxHeight: 40 }} value={view} color='primary' exclusive onChange={handleChange}> <Box sx={{ p: 2, height: 'auto' }}>
<ToggleButton <img
sx={{ color: theme?.customization?.isDarkMode ? 'white' : 'inherit' }} style={{ objectFit: 'cover', height: '16vh', width: 'auto' }}
variant='contained' src={WorkflowEmptySVG}
value='card' alt='WorkflowEmptySVG'
title='Card View' />
> </Box>
<IconLayoutGrid /> <div>No Chatflows Yet</div>
</ToggleButton> </Stack>
<ToggleButton )}
sx={{ color: theme?.customization?.isDarkMode ? 'white' : 'inherit' }}
variant='contained'
value='list'
title='List View'
>
<IconList />
</ToggleButton>
</ToggleButtonGroup>
</ButtonGroup>
<Box sx={{ width: 5 }} />
<ButtonGroup disableElevation aria-label='outlined primary button group'>
<StyledButton variant='contained' onClick={addNew} startIcon={<IconPlus />}>
Add New
</StyledButton>
</ButtonGroup>
</ButtonGroup>
</Toolbar>
</Box>
{!isLoading && (!view || view === 'card') && getAllChatflowsApi.data && (
<Grid container spacing={gridSpacing}>
{getAllChatflowsApi.data.filter(filterFlows).map((data, index) => (
<Grid key={index} item lg={3} md={4} sm={6} xs={12}>
<ItemCard onClick={() => goToCanvas(data)} data={data} images={images[data.id]} />
</Grid>
))}
</Grid>
)}
{!isLoading && view === 'list' && getAllChatflowsApi.data && (
<FlowListTable
sx={{ mt: 20 }}
data={getAllChatflowsApi.data}
images={images}
filterFunction={filterFlows}
updateFlowsApi={getAllChatflowsApi}
/>
)}
</Stack>
{!isLoading && (!getAllChatflowsApi.data || getAllChatflowsApi.data.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={WorkflowEmptySVG} alt='WorkflowEmptySVG' />
</Box>
<div>No Chatflows Yet</div>
</Stack> </Stack>
)} )}
<LoginDialog show={loginDialogOpen} dialogProps={loginDialogProps} onConfirm={onLoginClick} /> <LoginDialog show={loginDialogOpen} dialogProps={loginDialogProps} onConfirm={onLoginClick} />
<ConfirmDialog /> <ConfirmDialog />
</MainCard> </MainCard>
@@ -29,7 +29,7 @@ import useNotifier from '@/utils/useNotifier'
import { baseURL, REDACTED_CREDENTIAL_VALUE } from '@/store/constant' import { baseURL, REDACTED_CREDENTIAL_VALUE } from '@/store/constant'
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from '@/store/actions' import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from '@/store/actions'
const AddEditCredentialDialog = ({ show, dialogProps, onCancel, onConfirm }) => { const AddEditCredentialDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) => {
const portalElement = document.getElementById('portal') const portalElement = document.getElementById('portal')
const dispatch = useDispatch() const dispatch = useDispatch()
@@ -70,6 +70,20 @@ const AddEditCredentialDialog = ({ show, dialogProps, onCancel, onConfirm }) =>
} }
}, [getSpecificComponentCredentialApi.data]) }, [getSpecificComponentCredentialApi.data])
useEffect(() => {
if (getSpecificCredentialApi.error) {
setError(getSpecificCredentialApi.error)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getSpecificCredentialApi.error])
useEffect(() => {
if (getSpecificComponentCredentialApi.error) {
setError(getSpecificComponentCredentialApi.error)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getSpecificComponentCredentialApi.error])
useEffect(() => { useEffect(() => {
if (dialogProps.type === 'EDIT' && dialogProps.data) { if (dialogProps.type === 'EDIT' && dialogProps.data) {
// When credential dialog is opened from Credentials dashboard // When credential dialog is opened from Credentials dashboard
@@ -118,6 +132,7 @@ const AddEditCredentialDialog = ({ show, dialogProps, onCancel, onConfirm }) =>
onConfirm(createResp.data.id) onConfirm(createResp.data.id)
} }
} catch (error) { } catch (error) {
setError(error)
enqueueSnackbar({ enqueueSnackbar({
message: `Failed to add new Credential: ${error.response.data.message}`, message: `Failed to add new Credential: ${error.response.data.message}`,
options: { options: {
@@ -167,6 +182,7 @@ const AddEditCredentialDialog = ({ show, dialogProps, onCancel, onConfirm }) =>
onConfirm(saveResp.data.id) onConfirm(saveResp.data.id)
} }
} catch (error) { } catch (error) {
setError(error)
enqueueSnackbar({ enqueueSnackbar({
message: `Failed to save Credential: ${error.response.data.message}`, message: `Failed to save Credential: ${error.response.data.message}`,
options: { options: {
@@ -284,7 +300,8 @@ AddEditCredentialDialog.propTypes = {
show: PropTypes.bool, show: PropTypes.bool,
dialogProps: PropTypes.object, dialogProps: PropTypes.object,
onCancel: PropTypes.func, onCancel: PropTypes.func,
onConfirm: PropTypes.func onConfirm: PropTypes.func,
setError: PropTypes.func
} }
export default AddEditCredentialDialog export default AddEditCredentialDialog
@@ -2,19 +2,7 @@ import { useState, useEffect } from 'react'
import { createPortal } from 'react-dom' import { createPortal } from 'react-dom'
import { useSelector, useDispatch } from 'react-redux' import { useSelector, useDispatch } from 'react-redux'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { import { List, ListItemButton, Dialog, DialogContent, DialogTitle, Box, OutlinedInput, InputAdornment, Typography } from '@mui/material'
List,
ListItemButton,
ListItem,
ListItemAvatar,
ListItemText,
Dialog,
DialogContent,
DialogTitle,
Box,
OutlinedInput,
InputAdornment
} from '@mui/material'
import { useTheme } from '@mui/material/styles' import { useTheme } from '@mui/material/styles'
import { IconSearch, IconX } from '@tabler/icons' import { IconSearch, IconX } from '@tabler/icons'
@@ -58,17 +46,27 @@ const CredentialListDialog = ({ show, dialogProps, onCancel, onCredentialSelecte
const component = show ? ( const component = show ? (
<Dialog <Dialog
fullWidth fullWidth
maxWidth='xs' maxWidth='md'
open={show} open={show}
onClose={onCancel} onClose={onCancel}
aria-labelledby='alert-dialog-title' aria-labelledby='alert-dialog-title'
aria-describedby='alert-dialog-description' aria-describedby='alert-dialog-description'
> >
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'> <DialogTitle sx={{ fontSize: '1rem', p: 3, pb: 0 }} id='alert-dialog-title'>
{dialogProps.title} {dialogProps.title}
<Box sx={{ p: 2 }}> </DialogTitle>
<DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2, maxHeight: '75vh', position: 'relative', px: 3, pb: 3 }}>
<Box
sx={{
backgroundColor: customization.isDarkMode ? theme.palette.background.darkPaper : theme.palette.background.paper,
pt: 2,
position: 'sticky',
top: 0,
zIndex: 10
}}
>
<OutlinedInput <OutlinedInput
sx={{ width: '100%', pr: 2, pl: 2, my: 2 }} sx={{ width: '100%', pr: 2, pl: 2, position: 'sticky' }}
id='input-search-credential' id='input-search-credential'
value={searchValue} value={searchValue}
onChange={(e) => filterSearch(e.target.value)} onChange={(e) => filterSearch(e.target.value)}
@@ -106,60 +104,59 @@ const CredentialListDialog = ({ show, dialogProps, onCancel, onCredentialSelecte
}} }}
/> />
</Box> </Box>
</DialogTitle>
<DialogContent>
<List <List
sx={{ sx={{
width: '100%', width: '100%',
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gap: 2,
py: 0, py: 0,
zIndex: 9,
borderRadius: '10px', borderRadius: '10px',
[theme.breakpoints.down('md')]: { [theme.breakpoints.down('md')]: {
maxWidth: 370 maxWidth: 370
},
'& .MuiListItemSecondaryAction-root': {
top: 22
},
'& .MuiDivider-root': {
my: 0
},
'& .list-container': {
pl: 7
} }
}} }}
> >
{[...componentsCredentials].map((componentCredential) => ( {[...componentsCredentials].map((componentCredential) => (
<div key={componentCredential.name}> <ListItemButton
<ListItemButton alignItems='center'
onClick={() => onCredentialSelected(componentCredential)} key={componentCredential.name}
sx={{ p: 0, borderRadius: `${customization.borderRadius}px` }} onClick={() => onCredentialSelected(componentCredential)}
sx={{
border: 1,
borderColor: theme.palette.grey[900] + 25,
borderRadius: 2,
display: 'flex',
alignItems: 'center',
justifyContent: 'start',
textAlign: 'left',
gap: 1,
p: 2
}}
>
<div
style={{
width: 50,
height: 50,
borderRadius: '50%',
backgroundColor: 'white'
}}
> >
<ListItem alignItems='center'> <img
<ListItemAvatar> style={{
<div width: '100%',
style={{ height: '100%',
width: 50, padding: 7,
height: 50, borderRadius: '50%',
borderRadius: '50%', objectFit: 'contain'
backgroundColor: 'white' }}
}} alt={componentCredential.name}
> src={`${baseURL}/api/v1/components-credentials-icon/${componentCredential.name}`}
<img />
style={{ </div>
width: '100%', <Typography>{componentCredential.label}</Typography>
height: '100%', </ListItemButton>
padding: 7,
borderRadius: '50%',
objectFit: 'contain'
}}
alt={componentCredential.name}
src={`${baseURL}/api/v1/components-credentials-icon/${componentCredential.name}`}
/>
</div>
</ListItemAvatar>
<ListItemText sx={{ ml: 1 }} primary={componentCredential.label} />
</ListItem>
</ListItemButton>
</div>
))} ))}
</List> </List>
</DialogContent> </DialogContent>
+192 -126
View File
@@ -4,9 +4,12 @@ import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackba
import moment from 'moment' import moment from 'moment'
// material-ui // material-ui
import { styled } from '@mui/material/styles'
import { tableCellClasses } from '@mui/material/TableCell'
import { import {
Button, Button,
Box, Box,
Skeleton,
Stack, Stack,
Table, Table,
TableBody, TableBody,
@@ -16,12 +19,8 @@ import {
TableRow, TableRow,
Paper, Paper,
IconButton, IconButton,
Toolbar, useTheme
TextField,
InputAdornment,
ButtonGroup
} from '@mui/material' } from '@mui/material'
import { useTheme } from '@mui/material/styles'
// project imports // project imports
import MainCard from '@/ui-component/cards/MainCard' import MainCard from '@/ui-component/cards/MainCard'
@@ -41,25 +40,48 @@ import useConfirm from '@/hooks/useConfirm'
import useNotifier from '@/utils/useNotifier' import useNotifier from '@/utils/useNotifier'
// Icons // Icons
import { IconTrash, IconEdit, IconX, IconPlus, IconSearch } from '@tabler/icons' import { IconTrash, IconEdit, IconX, IconPlus } from '@tabler/icons'
import CredentialEmptySVG from '@/assets/images/credential_empty.svg' import CredentialEmptySVG from '@/assets/images/credential_empty.svg'
// const // const
import { baseURL } from '@/store/constant' import { baseURL } from '@/store/constant'
import { SET_COMPONENT_CREDENTIALS } from '@/store/actions' import { SET_COMPONENT_CREDENTIALS } from '@/store/actions'
import ViewHeader from '@/layout/MainLayout/ViewHeader'
import ErrorBoundary from '@/ErrorBoundary'
const StyledTableCell = styled(TableCell)(({ theme }) => ({
borderColor: theme.palette.grey[900] + 25,
padding: '6px 16px',
[`&.${tableCellClasses.head}`]: {
color: theme.palette.grey[900]
},
[`&.${tableCellClasses.body}`]: {
fontSize: 14,
height: 64
}
}))
const StyledTableRow = styled(TableRow)(() => ({
// hide last border
'&:last-child td, &:last-child th': {
border: 0
}
}))
// ==============================|| Credentials ||============================== // // ==============================|| Credentials ||============================== //
const Credentials = () => { const Credentials = () => {
const theme = useTheme() const theme = useTheme()
const customization = useSelector((state) => state.customization) const customization = useSelector((state) => state.customization)
const dispatch = useDispatch() const dispatch = useDispatch()
useNotifier() useNotifier()
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
const [isLoading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [showCredentialListDialog, setShowCredentialListDialog] = useState(false) const [showCredentialListDialog, setShowCredentialListDialog] = useState(false)
const [credentialListDialogProps, setCredentialListDialogProps] = useState({}) const [credentialListDialogProps, setCredentialListDialogProps] = useState({})
const [showSpecificCredentialDialog, setShowSpecificCredentialDialog] = useState(false) const [showSpecificCredentialDialog, setShowSpecificCredentialDialog] = useState(false)
@@ -174,12 +196,22 @@ const Credentials = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])
useEffect(() => {
setLoading(getAllCredentialsApi.loading)
}, [getAllCredentialsApi.loading])
useEffect(() => { useEffect(() => {
if (getAllCredentialsApi.data) { if (getAllCredentialsApi.data) {
setCredentials(getAllCredentialsApi.data) setCredentials(getAllCredentialsApi.data)
} }
}, [getAllCredentialsApi.data]) }, [getAllCredentialsApi.data])
useEffect(() => {
if (getAllCredentialsApi.error) {
setError(getAllCredentialsApi.error)
}
}, [getAllCredentialsApi.error])
useEffect(() => { useEffect(() => {
if (getAllComponentsCredentialsApi.data) { if (getAllComponentsCredentialsApi.data) {
setComponentsCredentials(getAllComponentsCredentialsApi.data) setComponentsCredentials(getAllComponentsCredentialsApi.data)
@@ -189,131 +221,164 @@ const Credentials = () => {
return ( return (
<> <>
<MainCard sx={{ background: customization.isDarkMode ? theme.palette.common.black : '' }}> <MainCard>
<Stack flexDirection='row'> {error ? (
<Box sx={{ flexGrow: 1 }}> <ErrorBoundary error={error} />
<Toolbar ) : (
disableGutters={true} <Stack flexDirection='column' sx={{ gap: 3 }}>
style={{ <ViewHeader
margin: 1, onSearchChange={onSearchChange}
padding: 1, search={true}
paddingBottom: 10, searchPlaceholder='Search Credentials'
display: 'flex', title='Credentials'
justifyContent: 'space-between',
width: '100%'
}}
> >
<h1>Credentials&nbsp;</h1> <StyledButton
<TextField
size='small'
sx={{ display: { xs: 'none', sm: 'block' }, ml: 3 }}
variant='outlined'
placeholder='Search credential name'
onChange={onSearchChange}
InputProps={{
startAdornment: (
<InputAdornment position='start'>
<IconSearch />
</InputAdornment>
)
}}
/>
<Box sx={{ flexGrow: 1 }} />
<ButtonGroup
sx={{ maxHeight: 40 }}
disableElevation
variant='contained' variant='contained'
aria-label='outlined primary button group' sx={{ borderRadius: 2, height: '100%' }}
onClick={listCredential}
startIcon={<IconPlus />}
> >
<ButtonGroup disableElevation aria-label='outlined primary button group'> Add Credential
<StyledButton </StyledButton>
variant='contained' </ViewHeader>
sx={{ color: 'white', mr: 1, height: 37 }} {!isLoading && credentials.length <= 0 ? (
onClick={listCredential} <Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
startIcon={<IconPlus />} <Box sx={{ p: 2, height: 'auto' }}>
<img
style={{ objectFit: 'cover', height: '16vh', width: 'auto' }}
src={CredentialEmptySVG}
alt='CredentialEmptySVG'
/>
</Box>
<div>No Credentials Yet</div>
</Stack>
) : (
<TableContainer
sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2 }}
component={Paper}
>
<Table sx={{ minWidth: 650 }} aria-label='simple table'>
<TableHead
sx={{
backgroundColor: customization.isDarkMode
? theme.palette.common.black
: theme.palette.grey[100],
height: 56
}}
> >
Add Credential <TableRow>
</StyledButton> <StyledTableCell>Name</StyledTableCell>
</ButtonGroup> <StyledTableCell>Last Updated</StyledTableCell>
</ButtonGroup> <StyledTableCell>Created</StyledTableCell>
</Toolbar> <StyledTableCell> </StyledTableCell>
</Box> <StyledTableCell> </StyledTableCell>
</Stack> </TableRow>
{credentials.length <= 0 && ( </TableHead>
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'> <TableBody>
<Box sx={{ p: 2, height: 'auto' }}> {isLoading ? (
<img <>
style={{ objectFit: 'cover', height: '30vh', width: 'auto' }} <StyledTableRow>
src={CredentialEmptySVG} <StyledTableCell>
alt='CredentialEmptySVG' <Skeleton variant='text' />
/> </StyledTableCell>
</Box> <StyledTableCell>
<div>No Credentials Yet</div> <Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
</StyledTableRow>
<StyledTableRow>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
</StyledTableRow>
</>
) : (
<>
{credentials.filter(filterCredentials).map((credential, index) => (
<StyledTableRow key={index} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
<StyledTableCell scope='row'>
<Box
sx={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
gap: 1
}}
>
<Box
sx={{
width: 35,
height: 35,
borderRadius: '50%',
backgroundColor: customization.isDarkMode
? theme.palette.common.white
: theme.palette.grey[300] + 75
}}
>
<img
style={{
width: '100%',
height: '100%',
padding: 5,
objectFit: 'contain'
}}
alt={credential.credentialName}
src={`${baseURL}/api/v1/components-credentials-icon/${credential.credentialName}`}
/>
</Box>
{credential.name}
</Box>
</StyledTableCell>
<StyledTableCell>
{moment(credential.updatedDate).format('MMMM Do, YYYY')}
</StyledTableCell>
<StyledTableCell>
{moment(credential.createdDate).format('MMMM Do, YYYY')}
</StyledTableCell>
<StyledTableCell>
<IconButton title='Edit' color='primary' onClick={() => edit(credential)}>
<IconEdit />
</IconButton>
</StyledTableCell>
<StyledTableCell>
<IconButton
title='Delete'
color='error'
onClick={() => deleteCredential(credential)}
>
<IconTrash />
</IconButton>
</StyledTableCell>
</StyledTableRow>
))}
</>
)}
</TableBody>
</Table>
</TableContainer>
)}
</Stack> </Stack>
)} )}
{credentials.length > 0 && (
<TableContainer component={Paper}>
<Table sx={{ minWidth: 650 }} aria-label='simple table'>
<TableHead>
<TableRow>
<TableCell>Name</TableCell>
<TableCell>Last Updated</TableCell>
<TableCell>Created</TableCell>
<TableCell> </TableCell>
<TableCell> </TableCell>
</TableRow>
</TableHead>
<TableBody>
{credentials.filter(filterCredentials).map((credential, 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%'
}}
>
<img
style={{
width: '100%',
height: '100%',
borderRadius: '50%',
objectFit: 'contain'
}}
alt={credential.credentialName}
src={`${baseURL}/api/v1/components-credentials-icon/${credential.credentialName}`}
/>
</div>
{credential.name}
</div>
</TableCell>
<TableCell>{moment(credential.updatedDate).format('DD-MMM-YY')}</TableCell>
<TableCell>{moment(credential.createdDate).format('DD-MMM-YY')}</TableCell>
<TableCell>
<IconButton title='Edit' color='primary' onClick={() => edit(credential)}>
<IconEdit />
</IconButton>
</TableCell>
<TableCell>
<IconButton title='Delete' color='error' onClick={() => deleteCredential(credential)}>
<IconTrash />
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
</MainCard> </MainCard>
<CredentialListDialog <CredentialListDialog
show={showCredentialListDialog} show={showCredentialListDialog}
@@ -326,6 +391,7 @@ const Credentials = () => {
dialogProps={specificCredentialDialogProps} dialogProps={specificCredentialDialogProps}
onCancel={() => setShowSpecificCredentialDialog(false)} onCancel={() => setShowSpecificCredentialDialog(false)}
onConfirm={onConfirm} onConfirm={onConfirm}
setError={setError}
></AddEditCredentialDialog> ></AddEditCredentialDialog>
<ConfirmDialog /> <ConfirmDialog />
</> </>
+245 -222
View File
@@ -1,19 +1,13 @@
import * as React from 'react' import * as React from 'react'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { useSelector } from 'react-redux'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
// material-ui // material-ui
import { import {
Grid,
Box, Box,
Stack, Stack,
Badge, Badge,
Toolbar,
TextField,
InputAdornment,
ButtonGroup,
ToggleButton, ToggleButton,
InputLabel, InputLabel,
FormControl, FormControl,
@@ -21,10 +15,10 @@ import {
OutlinedInput, OutlinedInput,
Checkbox, Checkbox,
ListItemText, ListItemText,
Button Skeleton
} from '@mui/material' } from '@mui/material'
import { useTheme } from '@mui/material/styles' import { useTheme } from '@mui/material/styles'
import { IconChevronsDown, IconChevronsUp, IconLayoutGrid, IconList, IconSearch } from '@tabler/icons' import { IconLayoutGrid, IconList } from '@tabler/icons'
// project imports // project imports
import MainCard from '@/ui-component/cards/MainCard' import MainCard from '@/ui-component/cards/MainCard'
@@ -44,6 +38,8 @@ import { baseURL } from '@/store/constant'
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup' import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'
import { MarketplaceTable } from '@/ui-component/table/MarketplaceTable' import { MarketplaceTable } from '@/ui-component/table/MarketplaceTable'
import MenuItem from '@mui/material/MenuItem' import MenuItem from '@mui/material/MenuItem'
import ViewHeader from '@/layout/MainLayout/ViewHeader'
import ErrorBoundary from '@/ErrorBoundary'
function TabPanel(props) { function TabPanel(props) {
const { children, value, index, ...other } = props const { children, value, index, ...other } = props
@@ -66,28 +62,30 @@ TabPanel.propTypes = {
value: PropTypes.number.isRequired value: PropTypes.number.isRequired
} }
const ITEM_HEIGHT = 48
const ITEM_PADDING_TOP = 8
const badges = ['POPULAR', 'NEW'] const badges = ['POPULAR', 'NEW']
const types = ['Chatflow', 'Tool'] const types = ['Chatflow', 'Tool']
const framework = ['Langchain', 'LlamaIndex'] const framework = ['Langchain', 'LlamaIndex']
const MenuProps = { const MenuProps = {
PaperProps: { PaperProps: {
style: { style: {
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP, width: 160
width: 250
} }
} }
} }
const SelectStyles = {
'& .MuiOutlinedInput-notchedOutline': {
borderRadius: 2
}
}
// ==============================|| Marketplace ||============================== // // ==============================|| Marketplace ||============================== //
const Marketplace = () => { const Marketplace = () => {
const navigate = useNavigate() const navigate = useNavigate()
const theme = useTheme() const theme = useTheme()
const customization = useSelector((state) => state.customization)
const [isLoading, setLoading] = useState(true) const [isLoading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [images, setImages] = useState({}) const [images, setImages] = useState({})
const [showToolDialog, setShowToolDialog] = useState(false) const [showToolDialog, setShowToolDialog] = useState(false)
@@ -101,7 +99,6 @@ const Marketplace = () => {
const [badgeFilter, setBadgeFilter] = useState([]) const [badgeFilter, setBadgeFilter] = useState([])
const [typeFilter, setTypeFilter] = useState([]) const [typeFilter, setTypeFilter] = useState([])
const [frameworkFilter, setFrameworkFilter] = useState([]) const [frameworkFilter, setFrameworkFilter] = useState([])
const [open, setOpen] = useState(false)
const handleBadgeFilterChange = (event) => { const handleBadgeFilterChange = (event) => {
const { const {
target: { value } target: { value }
@@ -223,222 +220,248 @@ const Marketplace = () => {
} }
}, [getAllTemplatesMarketplacesApi.data]) }, [getAllTemplatesMarketplacesApi.data])
useEffect(() => {
if (getAllTemplatesMarketplacesApi.error) {
setError(getAllTemplatesMarketplacesApi.error)
}
}, [getAllTemplatesMarketplacesApi.error])
return ( return (
<> <>
<MainCard sx={{ background: customization.isDarkMode ? theme.palette.common.black : '' }}> <MainCard>
<Box sx={{ flexGrow: 1 }}> {error ? (
<Toolbar <ErrorBoundary error={error} />
disableGutters={true} ) : (
style={{ <Stack flexDirection='column' sx={{ gap: 3 }}>
margin: 1, <ViewHeader
padding: 1, filters={
paddingBottom: 10, <>
display: 'flex', <FormControl
justifyContent: 'space-between', sx={{
width: '100%' borderRadius: 2,
}} display: 'flex',
> flexDirection: 'column',
<h1>Marketplace</h1> justifyContent: 'end',
<TextField minWidth: 120
size='small' }}
id='search-filter-textbox'
sx={{ display: { xs: 'none', sm: 'block' }, ml: 3 }}
variant='outlined'
fullWidth='true'
placeholder='Search name or description or node name'
onChange={onSearchChange}
InputProps={{
startAdornment: (
<InputAdornment position='start'>
<IconSearch />
</InputAdornment>
)
}}
/>
<Button
sx={{ width: '220px', ml: 3, mr: 5 }}
variant='outlined'
onClick={() => setOpen(!open)}
startIcon={open ? <IconChevronsUp /> : <IconChevronsDown />}
>
{open ? 'Hide Filters' : 'Show Filters'}
</Button>
<Box sx={{ flexGrow: 1 }} />
<ButtonGroup sx={{ maxHeight: 40 }} disableElevation variant='contained' aria-label='outlined primary button group'>
<ButtonGroup disableElevation variant='contained' aria-label='outlined primary button group'>
<ToggleButtonGroup
sx={{ maxHeight: 40 }}
value={view}
color='primary'
exclusive
onChange={handleViewChange}
>
<ToggleButton
sx={{ color: theme?.customization?.isDarkMode ? 'white' : 'inherit' }}
variant='contained'
value='card'
title='Card View'
> >
<IconLayoutGrid /> <InputLabel size='small' id='filter-badge-label'>
</ToggleButton> Tag
<ToggleButton </InputLabel>
sx={{ color: theme?.customization?.isDarkMode ? 'white' : 'inherit' }} <Select
variant='contained' labelId='filter-badge-label'
value='list' id='filter-badge-checkbox'
title='List View' size='small'
multiple
value={badgeFilter}
onChange={handleBadgeFilterChange}
input={<OutlinedInput label='Badge' />}
renderValue={(selected) => selected.join(', ')}
MenuProps={MenuProps}
sx={SelectStyles}
>
{badges.map((name) => (
<MenuItem
key={name}
value={name}
sx={{ display: 'flex', alignItems: 'center', gap: 1, p: 1 }}
>
<Checkbox checked={badgeFilter.indexOf(name) > -1} sx={{ p: 0 }} />
<ListItemText primary={name} />
</MenuItem>
))}
</Select>
</FormControl>
<FormControl
sx={{
borderRadius: 2,
display: 'flex',
flexDirection: 'column',
justifyContent: 'end',
minWidth: 120
}}
> >
<IconList /> <InputLabel size='small' id='type-badge-label'>
</ToggleButton> Type
</ToggleButtonGroup> </InputLabel>
</ButtonGroup> <Select
</ButtonGroup> size='small'
</Toolbar> labelId='type-badge-label'
</Box> id='type-badge-checkbox'
{open && ( multiple
<Box sx={{ flexGrow: 1, mb: 2 }}> value={typeFilter}
<Toolbar onChange={handleTypeFilterChange}
disableGutters={true} input={<OutlinedInput label='Badge' />}
style={{ renderValue={(selected) => selected.join(', ')}
margin: 1, MenuProps={MenuProps}
padding: 1, sx={SelectStyles}
paddingBottom: 10, >
display: 'flex', {types.map((name) => (
justifyContent: 'flex-start', <MenuItem
width: '100%', key={name}
borderBottom: '1px solid' value={name}
}} sx={{ display: 'flex', alignItems: 'center', gap: 1, p: 1 }}
>
<Checkbox checked={typeFilter.indexOf(name) > -1} sx={{ p: 0 }} />
<ListItemText primary={name} />
</MenuItem>
))}
</Select>
</FormControl>
<FormControl
sx={{
borderRadius: 2,
display: 'flex',
flexDirection: 'column',
justifyContent: 'end',
minWidth: 120
}}
>
<InputLabel size='small' id='type-fw-label'>
Framework
</InputLabel>
<Select
size='small'
labelId='type-fw-label'
id='type-fw-checkbox'
multiple
value={frameworkFilter}
onChange={handleFrameworkFilterChange}
input={<OutlinedInput label='Badge' />}
renderValue={(selected) => selected.join(', ')}
MenuProps={MenuProps}
sx={SelectStyles}
>
{framework.map((name) => (
<MenuItem
key={name}
value={name}
sx={{ display: 'flex', alignItems: 'center', gap: 1, p: 1 }}
>
<Checkbox checked={frameworkFilter.indexOf(name) > -1} sx={{ p: 0 }} />
<ListItemText primary={name} />
</MenuItem>
))}
</Select>
</FormControl>
</>
}
onSearchChange={onSearchChange}
search={true}
searchPlaceholder='Search Name/Description/Node'
title='Marketplace'
> >
<FormControl sx={{ m: 1, width: 250 }}> <ToggleButtonGroup
<InputLabel size='small' id='filter-badge-label'> sx={{ borderRadius: 2, height: '100%' }}
Tag value={view}
</InputLabel> color='primary'
<Select exclusive
labelId='filter-badge-label' onChange={handleViewChange}
id='filter-badge-checkbox' >
size='small' <ToggleButton
multiple sx={{
value={badgeFilter} borderColor: theme.palette.grey[900] + 25,
onChange={handleBadgeFilterChange} borderRadius: 2,
input={<OutlinedInput label='Badge' />} color: theme?.customization?.isDarkMode ? 'white' : 'inherit'
renderValue={(selected) => selected.join(', ')} }}
MenuProps={MenuProps} variant='contained'
value='card'
title='Card View'
> >
{badges.map((name) => ( <IconLayoutGrid />
<MenuItem key={name} value={name}> </ToggleButton>
<Checkbox checked={badgeFilter.indexOf(name) > -1} /> <ToggleButton
<ListItemText primary={name} /> sx={{
</MenuItem> borderColor: theme.palette.grey[900] + 25,
))} borderRadius: 2,
</Select> color: theme?.customization?.isDarkMode ? 'white' : 'inherit'
</FormControl> }}
<FormControl sx={{ m: 1, width: 250 }}> variant='contained'
<InputLabel size='small' id='type-badge-label'> value='list'
Type title='List View'
</InputLabel>
<Select
size='small'
labelId='type-badge-label'
id='type-badge-checkbox'
multiple
value={typeFilter}
onChange={handleTypeFilterChange}
input={<OutlinedInput label='Badge' />}
renderValue={(selected) => selected.join(', ')}
MenuProps={MenuProps}
> >
{types.map((name) => ( <IconList />
<MenuItem key={name} value={name}> </ToggleButton>
<Checkbox checked={typeFilter.indexOf(name) > -1} /> </ToggleButtonGroup>
<ListItemText primary={name} /> </ViewHeader>
</MenuItem> {!view || view === 'card' ? (
))} <>
</Select> {isLoading ? (
</FormControl> <Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
<FormControl sx={{ m: 1, width: 250 }}> <Skeleton variant='rounded' height={160} />
<InputLabel size='small' id='type-fw-label'> <Skeleton variant='rounded' height={160} />
Framework <Skeleton variant='rounded' height={160} />
</InputLabel> </Box>
<Select ) : (
size='small' <Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
labelId='type-fw-label' {getAllTemplatesMarketplacesApi.data
id='type-fw-checkbox' ?.filter(filterByBadge)
multiple .filter(filterByType)
value={frameworkFilter} .filter(filterFlows)
onChange={handleFrameworkFilterChange} .filter(filterByFramework)
input={<OutlinedInput label='Badge' />} .map((data, index) => (
renderValue={(selected) => selected.join(', ')} <Box key={index}>
MenuProps={MenuProps} {data.badge && (
> <Badge
{framework.map((name) => ( sx={{
<MenuItem key={name} value={name}> width: '100%',
<Checkbox checked={frameworkFilter.indexOf(name) > -1} /> height: '100%',
<ListItemText primary={name} /> '& .MuiBadge-badge': {
</MenuItem> right: 20
))} }
</Select> }}
</FormControl> badgeContent={data.badge}
</Toolbar> color={data.badge === 'POPULAR' ? 'primary' : 'error'}
</Box> >
)} {data.type === 'Chatflow' && (
<ItemCard
{!isLoading && (!view || view === 'card') && getAllTemplatesMarketplacesApi.data && ( onClick={() => goToCanvas(data)}
<> data={data}
<Grid container spacing={gridSpacing}> images={images[data.id]}
{getAllTemplatesMarketplacesApi.data />
.filter(filterByBadge) )}
.filter(filterByType) {data.type === 'Tool' && (
.filter(filterFlows) <ItemCard data={data} onClick={() => goToTool(data)} />
.filter(filterByFramework) )}
.map((data, index) => ( </Badge>
<Grid key={index} item lg={3} md={4} sm={6} xs={12}> )}
{data.badge && ( {!data.badge && data.type === 'Chatflow' && (
<Badge <ItemCard onClick={() => goToCanvas(data)} data={data} images={images[data.id]} />
sx={{ )}
'& .MuiBadge-badge': { {!data.badge && data.type === 'Tool' && (
right: 20 <ItemCard data={data} onClick={() => goToTool(data)} />
} )}
}} </Box>
badgeContent={data.badge} ))}
color={data.badge === 'POPULAR' ? 'primary' : 'error'} </Box>
> )}
{data.type === 'Chatflow' && ( </>
<ItemCard onClick={() => goToCanvas(data)} data={data} images={images[data.id]} /> ) : (
)} <MarketplaceTable
{data.type === 'Tool' && <ItemCard data={data} onClick={() => goToTool(data)} />} data={getAllTemplatesMarketplacesApi.data}
</Badge> filterFunction={filterFlows}
)} filterByType={filterByType}
{!data.badge && data.type === 'Chatflow' && ( filterByBadge={filterByBadge}
<ItemCard onClick={() => goToCanvas(data)} data={data} images={images[data.id]} /> filterByFramework={filterByFramework}
)} goToTool={goToTool}
{!data.badge && data.type === 'Tool' && <ItemCard data={data} onClick={() => goToTool(data)} />} goToCanvas={goToCanvas}
</Grid> isLoading={isLoading}
))} setError={setError}
</Grid>
</>
)}
{!isLoading && view === 'list' && getAllTemplatesMarketplacesApi.data && (
<MarketplaceTable
sx={{ mt: 20 }}
data={getAllTemplatesMarketplacesApi.data}
filterFunction={filterFlows}
filterByType={filterByType}
filterByBadge={filterByBadge}
filterByFramework={filterByFramework}
goToTool={goToTool}
goToCanvas={goToCanvas}
/>
)}
{!isLoading && (!getAllTemplatesMarketplacesApi.data || getAllTemplatesMarketplacesApi.data.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={WorkflowEmptySVG}
alt='WorkflowEmptySVG'
/> />
</Box> )}
<div>No Marketplace Yet</div>
{!isLoading && (!getAllTemplatesMarketplacesApi.data || getAllTemplatesMarketplacesApi.data.length === 0) && (
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
<Box sx={{ p: 2, height: 'auto' }}>
<img
style={{ objectFit: 'cover', height: '16vh', width: 'auto' }}
src={WorkflowEmptySVG}
alt='WorkflowEmptySVG'
/>
</Box>
<div>No Marketplace Yet</div>
</Stack>
)}
</Stack> </Stack>
)} )}
</MainCard> </MainCard>
+117 -115
View File
@@ -5,7 +5,7 @@ import { useDispatch, useSelector } from 'react-redux'
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions' import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions'
import { cloneDeep } from 'lodash' import { cloneDeep } from 'lodash'
import { Box, Typography, Button, Dialog, DialogActions, DialogContent, DialogTitle, Stack, OutlinedInput } from '@mui/material' import { Box, Button, Typography, Dialog, DialogActions, DialogContent, DialogTitle, Stack, OutlinedInput } from '@mui/material'
import { StyledButton } from '@/ui-component/button/StyledButton' import { StyledButton } from '@/ui-component/button/StyledButton'
import { Grid } from '@/ui-component/grid/Grid' import { Grid } from '@/ui-component/grid/Grid'
import { TooltipWithParser } from '@/ui-component/tooltip/TooltipWithParser' import { TooltipWithParser } from '@/ui-component/tooltip/TooltipWithParser'
@@ -16,7 +16,7 @@ import { CodeEditor } from '@/ui-component/editor/CodeEditor'
import HowToUseFunctionDialog from './HowToUseFunctionDialog' import HowToUseFunctionDialog from './HowToUseFunctionDialog'
// Icons // Icons
import { IconX, IconFileExport } from '@tabler/icons' import { IconX, IconFileDownload, IconPlus } from '@tabler/icons'
// API // API
import toolsApi from '@/api/tools' import toolsApi from '@/api/tools'
@@ -55,7 +55,7 @@ try {
return ''; return '';
}` }`
const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm }) => { const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm, setError }) => {
const portalElement = document.getElementById('portal') const portalElement = document.getElementById('portal')
const customization = useSelector((state) => state.customization) const customization = useSelector((state) => state.customization)
@@ -160,6 +160,13 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm }) =
} }
}, [getSpecificToolApi.data]) }, [getSpecificToolApi.data])
useEffect(() => {
if (getSpecificToolApi.error) {
setError(getSpecificToolApi.error)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getSpecificToolApi.error])
useEffect(() => { useEffect(() => {
if (dialogProps.type === 'EDIT' && dialogProps.data) { if (dialogProps.type === 'EDIT' && dialogProps.data) {
// When tool dialog is opened from Tools dashboard // When tool dialog is opened from Tools dashboard
@@ -383,128 +390,122 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm }) =
aria-labelledby='alert-dialog-title' aria-labelledby='alert-dialog-title'
aria-describedby='alert-dialog-description' aria-describedby='alert-dialog-description'
> >
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'> <DialogTitle sx={{ fontSize: '1rem', p: 3, pb: 0 }} id='alert-dialog-title'>
<div style={{ display: 'flex', flexDirection: 'row' }}> <Box sx={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between' }}>
{dialogProps.title} {dialogProps.title}
<div style={{ flex: 1 }} />
{dialogProps.type === 'EDIT' && ( {dialogProps.type === 'EDIT' && (
<Button variant='outlined' onClick={() => exportTool()} startIcon={<IconFileExport />}> <Button variant='outlined' onClick={() => exportTool()} startIcon={<IconFileDownload />}>
Export Export
</Button> </Button>
)} )}
</div>
</DialogTitle>
<DialogContent>
<Box sx={{ p: 2 }}>
<Stack sx={{ position: 'relative' }} direction='row'>
<Typography variant='overline'>
Tool Name
<span style={{ color: 'red' }}>&nbsp;*</span>
<TooltipWithParser
style={{ marginLeft: 10 }}
title={'Tool name must be small capital letter with underscore. Ex: my_tool'}
/>
</Typography>
</Stack>
<OutlinedInput
id='toolName'
type='string'
fullWidth
disabled={dialogProps.type === 'TEMPLATE'}
placeholder='My New Tool'
value={toolName}
name='toolName'
onChange={(e) => setToolName(e.target.value)}
/>
</Box> </Box>
<Box sx={{ p: 2 }}> </DialogTitle>
<Stack sx={{ position: 'relative' }} direction='row'> <DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2, maxHeight: '75vh', position: 'relative', px: 3, pb: 3 }}>
<Typography variant='overline'> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: 2 }}>
Tool description <Box>
<span style={{ color: 'red' }}>&nbsp;*</span> <Stack sx={{ position: 'relative', alignItems: 'center' }} direction='row'>
<Typography variant='overline'>
Tool Name
<span style={{ color: 'red' }}>&nbsp;*</span>
</Typography>
<TooltipWithParser title={'Tool name must be small capital letter with underscore. Ex: my_tool'} />
</Stack>
<OutlinedInput
id='toolName'
type='string'
fullWidth
disabled={dialogProps.type === 'TEMPLATE'}
placeholder='My New Tool'
value={toolName}
name='toolName'
onChange={(e) => setToolName(e.target.value)}
/>
</Box>
<Box>
<Stack sx={{ position: 'relative', alignItems: 'center' }} direction='row'>
<Typography variant='overline'>
Tool description
<span style={{ color: 'red' }}>&nbsp;*</span>
</Typography>
<TooltipWithParser <TooltipWithParser
style={{ marginLeft: 10 }}
title={'Description of what the tool does. This is for ChatGPT to determine when to use this tool.'} title={'Description of what the tool does. This is for ChatGPT to determine when to use this tool.'}
/> />
</Typography> </Stack>
</Stack> <OutlinedInput
<OutlinedInput id='toolDesc'
id='toolDesc' type='string'
type='string' fullWidth
fullWidth disabled={dialogProps.type === 'TEMPLATE'}
disabled={dialogProps.type === 'TEMPLATE'} placeholder='Description of what the tool does. This is for ChatGPT to determine when to use this tool.'
placeholder='Description of what the tool does. This is for ChatGPT to determine when to use this tool.' multiline={true}
multiline={true} rows={3}
rows={3} value={toolDesc}
value={toolDesc} name='toolDesc'
name='toolDesc' onChange={(e) => setToolDesc(e.target.value)}
onChange={(e) => setToolDesc(e.target.value)} />
/> </Box>
</Box> <Box>
<Box sx={{ p: 2 }}> <Stack sx={{ position: 'relative' }} direction='row'>
<Stack sx={{ position: 'relative' }} direction='row'> <Typography variant='overline'>Tool Icon Source</Typography>
<Typography variant='overline'>Tool Icon Src</Typography> </Stack>
</Stack> <OutlinedInput
<OutlinedInput id='toolIcon'
id='toolIcon' type='string'
type='string' fullWidth
fullWidth disabled={dialogProps.type === 'TEMPLATE'}
disabled={dialogProps.type === 'TEMPLATE'} placeholder='https://raw.githubusercontent.com/gilbarbara/logos/main/logos/airtable.svg'
placeholder='https://raw.githubusercontent.com/gilbarbara/logos/main/logos/airtable.svg' value={toolIcon}
value={toolIcon} name='toolIcon'
name='toolIcon' onChange={(e) => setToolIcon(e.target.value)}
onChange={(e) => setToolIcon(e.target.value)} />
/> </Box>
</Box> <Box>
<Box sx={{ p: 2 }}> <Stack sx={{ position: 'relative', justifyContent: 'space-between' }} direction='row'>
<Stack sx={{ position: 'relative' }} direction='row'> <Stack sx={{ position: 'relative', alignItems: 'center' }} direction='row'>
<Typography variant='overline'> <Typography variant='overline'>Output Schema</Typography>
Output Schema <TooltipWithParser title={'What should be the output response in JSON format?'} />
<TooltipWithParser style={{ marginLeft: 10 }} title={'What should be the output response in JSON format?'} /> </Stack>
</Typography> {dialogProps.type !== 'TEMPLATE' && (
</Stack> <Button variant='outlined' onClick={addNewRow} startIcon={<IconPlus />}>
<Grid Add Item
columns={columns} </Button>
rows={toolSchema} )}
disabled={dialogProps.type === 'TEMPLATE'} </Stack>
addNewRow={addNewRow} <Grid columns={columns} rows={toolSchema} disabled={dialogProps.type === 'TEMPLATE'} onRowUpdate={onRowUpdate} />
onRowUpdate={onRowUpdate} </Box>
/> <Box>
</Box> <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box sx={{ p: 2 }}> <Stack sx={{ position: 'relative', alignItems: 'center' }} direction='row'>
<Stack sx={{ position: 'relative' }} direction='row'> <Typography variant='overline'>Javascript Function</Typography>
<Typography variant='overline'> <TooltipWithParser title='Function to execute when tool is being used. You can use properties specified in Output Schema as variables. For example, if the property is <code>userid</code>, you can use as <code>$userid</code>. Return value must be a string. You can also override the code from API by following this <a target="_blank" href="https://docs.flowiseai.com/tools/custom-tool#override-function-from-api">guide</a>' />
Javascript Function </Stack>
<TooltipWithParser <Stack direction='row'>
style={{ marginLeft: 10 }} <Button
title='Function to execute when tool is being used. You can use properties specified in Output Schema as variables. For example, if the property is <code>userid</code>, you can use as <code>$userid</code>. Return value must be a string. You can also override the code from API by following this <a target="_blank" href="https://docs.flowiseai.com/tools/custom-tool#override-function-from-api">guide</a>' style={{ marginBottom: 10, marginRight: 10 }}
/> color='secondary'
</Typography> variant='text'
</Stack> onClick={() => setShowHowToDialog(true)}
<Button >
style={{ marginBottom: 10, marginRight: 10 }} How to use Function
color='secondary' </Button>
variant='outlined' {dialogProps.type !== 'TEMPLATE' && (
onClick={() => setShowHowToDialog(true)} <Button style={{ marginBottom: 10 }} variant='outlined' onClick={() => setToolFunc(exampleAPIFunc)}>
> See Example
How to use Function </Button>
</Button> )}
{dialogProps.type !== 'TEMPLATE' && ( </Stack>
<Button style={{ marginBottom: 10 }} variant='outlined' onClick={() => setToolFunc(exampleAPIFunc)}> </Box>
See Example <CodeEditor
</Button> disabled={dialogProps.type === 'TEMPLATE'}
)} value={toolFunc}
<CodeEditor theme={customization.isDarkMode ? 'dark' : 'light'}
disabled={dialogProps.type === 'TEMPLATE'} lang={'js'}
value={toolFunc} onValueChange={(code) => setToolFunc(code)}
height='calc(100vh - 220px)' />
theme={customization.isDarkMode ? 'dark' : 'light'} </Box>
lang={'js'}
onValueChange={(code) => setToolFunc(code)}
/>
</Box> </Box>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions sx={{ p: 3 }}>
{dialogProps.type === 'EDIT' && ( {dialogProps.type === 'EDIT' && (
<StyledButton color='error' variant='contained' onClick={() => deleteTool()}> <StyledButton color='error' variant='contained' onClick={() => deleteTool()}>
Delete Delete
@@ -538,7 +539,8 @@ ToolDialog.propTypes = {
dialogProps: PropTypes.object, dialogProps: PropTypes.object,
onUseTemplate: PropTypes.func, onUseTemplate: PropTypes.func,
onCancel: PropTypes.func, onCancel: PropTypes.func,
onConfirm: PropTypes.func onConfirm: PropTypes.func,
setError: PropTypes.func
} }
export default ToolDialog export default ToolDialog
+78 -43
View File
@@ -1,9 +1,7 @@
import { useEffect, useState, useRef } from 'react' import { useEffect, useState, useRef } from 'react'
import { useSelector } from 'react-redux'
// material-ui // material-ui
import { Grid, Box, Stack, Button } from '@mui/material' import { Box, Stack, Button, ButtonGroup, Skeleton } from '@mui/material'
import { useTheme } from '@mui/material/styles'
// project imports // project imports
import MainCard from '@/ui-component/cards/MainCard' import MainCard from '@/ui-component/cards/MainCard'
@@ -20,16 +18,17 @@ import toolsApi from '@/api/tools'
import useApi from '@/hooks/useApi' import useApi from '@/hooks/useApi'
// icons // icons
import { IconPlus, IconFileImport } from '@tabler/icons' import { IconPlus, IconFileUpload } from '@tabler/icons'
import ViewHeader from '@/layout/MainLayout/ViewHeader'
import ErrorBoundary from '@/ErrorBoundary'
// ==============================|| CHATFLOWS ||============================== // // ==============================|| CHATFLOWS ||============================== //
const Tools = () => { const Tools = () => {
const theme = useTheme()
const customization = useSelector((state) => state.customization)
const getAllToolsApi = useApi(toolsApi.getAllTools) const getAllToolsApi = useApi(toolsApi.getAllTools)
const [isLoading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [showDialog, setShowDialog] = useState(false) const [showDialog, setShowDialog] = useState(false)
const [dialogProps, setDialogProps] = useState({}) const [dialogProps, setDialogProps] = useState({})
@@ -101,44 +100,79 @@ const Tools = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])
useEffect(() => {
setLoading(getAllToolsApi.loading)
}, [getAllToolsApi.loading])
useEffect(() => {
if (getAllToolsApi.error) {
setError(getAllToolsApi.error)
}
}, [getAllToolsApi.error])
return ( return (
<> <>
<MainCard sx={{ background: customization.isDarkMode ? theme.palette.common.black : '' }}> <MainCard>
<Stack flexDirection='row'> {error ? (
<h1>Tools</h1> <ErrorBoundary error={error} />
<Grid sx={{ mb: 1.25 }} container direction='row'> ) : (
<Box sx={{ flexGrow: 1 }} /> <Stack flexDirection='column' sx={{ gap: 3 }}>
<Grid item> <ViewHeader title='Tools'>
<Button <Box sx={{ display: 'flex', alignItems: 'center' }}>
variant='outlined' <Button
sx={{ mr: 2 }} variant='outlined'
onClick={() => inputRef.current.click()} onClick={() => inputRef.current.click()}
startIcon={<IconFileImport />} startIcon={<IconFileUpload />}
> sx={{ borderRadius: 2, height: 40 }}
Load >
</Button> Load
<input ref={inputRef} type='file' hidden accept='.json' onChange={(e) => handleFileUpload(e)} /> </Button>
<StyledButton variant='contained' sx={{ color: 'white' }} onClick={addNew} startIcon={<IconPlus />}> <input
Create style={{ display: 'none' }}
</StyledButton> ref={inputRef}
</Grid> type='file'
</Grid> hidden
</Stack> accept='.json'
<Grid container spacing={gridSpacing}> onChange={(e) => handleFileUpload(e)}
{!getAllToolsApi.loading && />
getAllToolsApi.data && </Box>
getAllToolsApi.data.map((data, index) => ( <ButtonGroup disableElevation aria-label='outlined primary button group'>
<Grid key={index} item lg={3} md={4} sm={6} xs={12}> <StyledButton
<ItemCard data={data} onClick={() => edit(data)} /> variant='contained'
</Grid> onClick={addNew}
))} startIcon={<IconPlus />}
</Grid> sx={{ borderRadius: 2, height: 40 }}
{!getAllToolsApi.loading && (!getAllToolsApi.data || getAllToolsApi.data.length === 0) && ( >
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'> Create
<Box sx={{ p: 2, height: 'auto' }}> </StyledButton>
<img style={{ objectFit: 'cover', height: '30vh', width: 'auto' }} src={ToolEmptySVG} alt='ToolEmptySVG' /> </ButtonGroup>
</Box> </ViewHeader>
<div>No Tools Created Yet</div> {isLoading ? (
<Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
<Skeleton variant='rounded' height={160} />
<Skeleton variant='rounded' height={160} />
<Skeleton variant='rounded' height={160} />
</Box>
) : (
<Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
{getAllToolsApi.data &&
getAllToolsApi.data.map((data, index) => (
<ItemCard data={data} key={index} onClick={() => edit(data)} />
))}
</Box>
)}
{!isLoading && (!getAllToolsApi.data || getAllToolsApi.data.length === 0) && (
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
<Box sx={{ p: 2, height: 'auto' }}>
<img
style={{ objectFit: 'cover', height: '16vh', width: 'auto' }}
src={ToolEmptySVG}
alt='ToolEmptySVG'
/>
</Box>
<div>No Tools Created Yet</div>
</Stack>
)}
</Stack> </Stack>
)} )}
</MainCard> </MainCard>
@@ -147,6 +181,7 @@ const Tools = () => {
dialogProps={dialogProps} dialogProps={dialogProps}
onCancel={() => setShowDialog(false)} onCancel={() => setShowDialog(false)}
onConfirm={onConfirm} onConfirm={onConfirm}
setError={setError}
></ToolDialog> ></ToolDialog>
</> </>
) )
@@ -39,7 +39,7 @@ const variableTypes = [
} }
] ]
const AddEditVariableDialog = ({ show, dialogProps, onCancel, onConfirm }) => { const AddEditVariableDialog = ({ show, dialogProps, onCancel, onConfirm, setError }) => {
const portalElement = document.getElementById('portal') const portalElement = document.getElementById('portal')
const dispatch = useDispatch() const dispatch = useDispatch()
@@ -111,6 +111,7 @@ const AddEditVariableDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
onConfirm(createResp.data.id) onConfirm(createResp.data.id)
} }
} catch (err) { } catch (err) {
setError(err)
enqueueSnackbar({ enqueueSnackbar({
message: `Failed to add new Variable: ${error.response.data.message}`, message: `Failed to add new Variable: ${error.response.data.message}`,
options: { options: {
@@ -153,6 +154,7 @@ const AddEditVariableDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
onConfirm(saveResp.data.id) onConfirm(saveResp.data.id)
} }
} catch (error) { } catch (error) {
setError(err)
enqueueSnackbar({ enqueueSnackbar({
message: `Failed to save Variable: ${error.response.data.message}`, message: `Failed to save Variable: ${error.response.data.message}`,
options: { options: {
@@ -281,7 +283,8 @@ AddEditVariableDialog.propTypes = {
show: PropTypes.bool, show: PropTypes.bool,
dialogProps: PropTypes.object, dialogProps: PropTypes.object,
onCancel: PropTypes.func, onCancel: PropTypes.func,
onConfirm: PropTypes.func onConfirm: PropTypes.func,
setError: PropTypes.func
} }
export default AddEditVariableDialog export default AddEditVariableDialog
+207 -138
View File
@@ -4,9 +4,12 @@ import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackba
import moment from 'moment' import moment from 'moment'
// material-ui // material-ui
import { styled } from '@mui/material/styles'
import { tableCellClasses } from '@mui/material/TableCell'
import { import {
Button, Button,
Box, Box,
Skeleton,
Stack, Stack,
Table, Table,
TableBody, TableBody,
@@ -16,13 +19,9 @@ import {
TableRow, TableRow,
Paper, Paper,
IconButton, IconButton,
Toolbar, Chip,
TextField, useTheme
InputAdornment,
ButtonGroup,
Chip
} from '@mui/material' } from '@mui/material'
import { useTheme } from '@mui/material/styles'
// project imports // project imports
import MainCard from '@/ui-component/cards/MainCard' import MainCard from '@/ui-component/cards/MainCard'
@@ -40,25 +39,47 @@ import useConfirm from '@/hooks/useConfirm'
import useNotifier from '@/utils/useNotifier' import useNotifier from '@/utils/useNotifier'
// Icons // Icons
import { IconTrash, IconEdit, IconX, IconPlus, IconSearch, IconVariable } from '@tabler/icons' import { IconTrash, IconEdit, IconX, IconPlus, IconVariable } from '@tabler/icons'
import VariablesEmptySVG from '@/assets/images/variables_empty.svg' import VariablesEmptySVG from '@/assets/images/variables_empty.svg'
// const // const
import AddEditVariableDialog from './AddEditVariableDialog' import AddEditVariableDialog from './AddEditVariableDialog'
import HowToUseVariablesDialog from './HowToUseVariablesDialog' import HowToUseVariablesDialog from './HowToUseVariablesDialog'
import ViewHeader from '@/layout/MainLayout/ViewHeader'
import ErrorBoundary from '@/ErrorBoundary'
const StyledTableCell = styled(TableCell)(({ theme }) => ({
borderColor: theme.palette.grey[900] + 25,
[`&.${tableCellClasses.head}`]: {
color: theme.palette.grey[900]
},
[`&.${tableCellClasses.body}`]: {
fontSize: 14,
height: 64
}
}))
const StyledTableRow = styled(TableRow)(() => ({
// hide last border
'&:last-child td, &:last-child th': {
border: 0
}
}))
// ==============================|| Credentials ||============================== // // ==============================|| Credentials ||============================== //
const Variables = () => { const Variables = () => {
const theme = useTheme() const theme = useTheme()
const customization = useSelector((state) => state.customization) const customization = useSelector((state) => state.customization)
const dispatch = useDispatch() const dispatch = useDispatch()
useNotifier() useNotifier()
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
const [isLoading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [showVariableDialog, setShowVariableDialog] = useState(false) const [showVariableDialog, setShowVariableDialog] = useState(false)
const [variableDialogProps, setVariableDialogProps] = useState({}) const [variableDialogProps, setVariableDialogProps] = useState({})
const [variables, setVariables] = useState([]) const [variables, setVariables] = useState([])
@@ -154,6 +175,16 @@ const Variables = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [])
useEffect(() => {
setLoading(getAllVariables.loading)
}, [getAllVariables.loading])
useEffect(() => {
if (getAllVariables.error) {
setError(getAllVariables.error)
}
}, [getAllVariables.error])
useEffect(() => { useEffect(() => {
if (getAllVariables.data) { if (getAllVariables.data) {
setVariables(getAllVariables.data) setVariables(getAllVariables.data)
@@ -162,149 +193,187 @@ const Variables = () => {
return ( return (
<> <>
<MainCard sx={{ background: customization.isDarkMode ? theme.palette.common.black : '' }}> <MainCard>
<Stack flexDirection='row'> {error ? (
<Box sx={{ flexGrow: 1 }}> <ErrorBoundary error={error} />
<Toolbar ) : (
disableGutters={true} <Stack flexDirection='column' sx={{ gap: 3 }}>
style={{ <ViewHeader onSearchChange={onSearchChange} search={true} searchPlaceholder='Search Variables' title='Variables'>
margin: 1, <Button variant='outlined' sx={{ borderRadius: 2, height: '100%' }} onClick={() => setShowHowToDialog(true)}>
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 How To Use
</Button> </Button>
<ButtonGroup <StyledButton
sx={{ maxHeight: 40 }}
disableElevation
variant='contained' variant='contained'
aria-label='outlined primary button group' sx={{ borderRadius: 2, height: '100%' }}
onClick={addNew}
startIcon={<IconPlus />}
id='btn_createVariable'
> >
<ButtonGroup disableElevation aria-label='outlined primary button group'> Add Variable
<StyledButton </StyledButton>
variant='contained' </ViewHeader>
sx={{ color: 'white', mr: 1, height: 37 }} {!isLoading && variables.length === 0 ? (
onClick={addNew} <Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
startIcon={<IconPlus />} <Box sx={{ p: 2, height: 'auto' }}>
id='btn_createVariable' <img
style={{ objectFit: 'cover', height: '16vh', width: 'auto' }}
src={VariablesEmptySVG}
alt='VariablesEmptySVG'
/>
</Box>
<div>No Variables Yet</div>
</Stack>
) : (
<TableContainer
sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2 }}
component={Paper}
>
<Table sx={{ minWidth: 650 }} aria-label='simple table'>
<TableHead
sx={{
backgroundColor: customization.isDarkMode
? theme.palette.common.black
: theme.palette.grey[100],
height: 56
}}
> >
Add Variable <TableRow>
</StyledButton> <StyledTableCell>Name</StyledTableCell>
</ButtonGroup> <StyledTableCell>Value</StyledTableCell>
</ButtonGroup> <StyledTableCell>Type</StyledTableCell>
</Toolbar> <StyledTableCell>Last Updated</StyledTableCell>
</Box> <StyledTableCell>Created</StyledTableCell>
</Stack> <StyledTableCell> </StyledTableCell>
{variables.length === 0 && ( <StyledTableCell> </StyledTableCell>
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'> </TableRow>
<Box sx={{ p: 2, height: 'auto' }}> </TableHead>
<img <TableBody>
style={{ objectFit: 'cover', height: '30vh', width: 'auto' }} {isLoading ? (
src={VariablesEmptySVG} <>
alt='VariablesEmptySVG' <StyledTableRow>
/> <StyledTableCell>
</Box> <Skeleton variant='text' />
<div>No Variables Yet</div> </StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
</StyledTableRow>
<StyledTableRow>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
</StyledTableRow>
</>
) : (
<>
{variables.filter(filterVariables).map((variable, index) => (
<StyledTableRow key={index} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
<StyledTableCell 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>
</StyledTableCell>
<StyledTableCell>{variable.value}</StyledTableCell>
<StyledTableCell>
<Chip
color={variable.type === 'static' ? 'info' : 'secondary'}
size='small'
label={variable.type}
/>
</StyledTableCell>
<StyledTableCell>
{moment(variable.updatedDate).format('MMMM Do, YYYY')}
</StyledTableCell>
<StyledTableCell>
{moment(variable.createdDate).format('MMMM Do, YYYY')}
</StyledTableCell>
<StyledTableCell>
<IconButton title='Edit' color='primary' onClick={() => edit(variable)}>
<IconEdit />
</IconButton>
</StyledTableCell>
<StyledTableCell>
<IconButton
title='Delete'
color='error'
onClick={() => deleteVariable(variable)}
>
<IconTrash />
</IconButton>
</StyledTableCell>
</StyledTableRow>
))}
</>
)}
</TableBody>
</Table>
</TableContainer>
)}
</Stack> </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> </MainCard>
<AddEditVariableDialog <AddEditVariableDialog
show={showVariableDialog} show={showVariableDialog}
dialogProps={variableDialogProps} dialogProps={variableDialogProps}
onCancel={() => setShowVariableDialog(false)} onCancel={() => setShowVariableDialog(false)}
onConfirm={onConfirm} onConfirm={onConfirm}
setError={setError}
></AddEditVariableDialog> ></AddEditVariableDialog>
<HowToUseVariablesDialog show={showHowToDialog} onCancel={() => setShowHowToDialog(false)}></HowToUseVariablesDialog> <HowToUseVariablesDialog show={showHowToDialog} onCancel={() => setShowHowToDialog(false)}></HowToUseVariablesDialog>
<ConfirmDialog /> <ConfirmDialog />