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
} }
+52 -38
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,28 +19,32 @@ 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}>
<Box sx={{ p: 2.25 }}>
<Grid container direction='column'>
<div <div
style={{ style={{
width: '100%',
display: 'flex', display: 'flex',
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center' alignItems: 'center',
overflow: 'hidden'
}} }}
> >
{data.iconSrc && ( {data.iconSrc && (
@@ -48,6 +52,8 @@ const ItemCard = ({ isLoading, data, images, onClick }) => {
style={{ style={{
width: 35, width: 35,
height: 35, height: 35,
display: 'flex',
flexShrink: 0,
marginRight: 10, marginRight: 10,
borderRadius: '50%', borderRadius: '50%',
background: `url(${data.iconSrc})`, background: `url(${data.iconSrc})`,
@@ -62,6 +68,8 @@ const ItemCard = ({ isLoading, data, images, onClick }) => {
style={{ style={{
width: 35, width: 35,
height: 35, height: 35,
display: 'flex',
flexShrink: 0,
marginRight: 10, marginRight: 10,
borderRadius: '50%', borderRadius: '50%',
background: data.color background: data.color
@@ -69,51 +77,57 @@ const ItemCard = ({ isLoading, data, images, onClick }) => {
></div> ></div>
)} )}
<Typography <Typography
sx={{ fontSize: '1.5rem', fontWeight: 500, overflowWrap: 'break-word', whiteSpace: 'pre-line' }} sx={{
display: '-webkit-box',
fontSize: '1.25rem',
fontWeight: 500,
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
textOverflow: 'ellipsis',
overflow: 'hidden'
}}
> >
{data.templateName || data.name} {data.templateName || data.name}
</Typography> </Typography>
</div> </div>
{data.description && ( {data.description && (
<span style={{ marginTop: 10, overflowWrap: 'break-word', whiteSpace: 'pre-line' }}> <span style={{ marginTop: 10, overflowWrap: 'break-word', whiteSpace: 'pre-line' }}>{data.description}</span>
{data.description}
</span>
)} )}
</Box>
{images && ( {images && (
<div <Box
style={{ sx={{
display: 'flex', display: 'flex',
flexDirection: 'row', alignItems: 'center',
flexWrap: 'wrap', justifyContent: 'start',
marginTop: 5 gap: 1
}} }}
> >
{images.map((img) => ( {images.slice(0, images.length > 3 ? 3 : images.length).map((img) => (
<div <Box
key={img} key={img}
style={{ sx={{
width: 35, width: 30,
height: 35, height: 30,
marginRight: 5,
borderRadius: '50%', borderRadius: '50%',
backgroundColor: 'white', backgroundColor: customization.isDarkMode
marginTop: 5 ? theme.palette.common.white
: theme.palette.grey[300] + 75
}} }}
> >
<img <img style={{ width: '100%', height: '100%', padding: 5, objectFit: 'contain' }} alt='' src={img} />
style={{ width: '100%', height: '100%', padding: 5, objectFit: 'contain' }} </Box>
alt=''
src={img}
/>
</div>
))} ))}
</div> {images.length > 3 && (
<Typography sx={{ alignItems: 'center', display: 'flex', fontSize: '.9rem', fontWeight: 200 }}>
+ {images.length - 3} More
</Typography>
)}
</Box>
)} )}
</Grid> </Grid>
</Box> </Box>
</CardWrapper> </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,18 +74,67 @@ export const FlowListTable = ({ data, images, filterFunction, updateFlowsApi })
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{data.filter(filterFunction).map((row, index) => ( {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>
</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(filterFunction).map((row, index) => (
<StyledTableRow key={index}> <StyledTableRow key={index}>
<TableCell key='0'> <StyledTableCell key='0'>
<Tooltip title={row.templateName || row.name}>
<Typography <Typography
sx={{ fontSize: '1.2rem', fontWeight: 500, overflowWrap: 'break-word', whiteSpace: 'pre-line' }} sx={{
display: '-webkit-box',
fontSize: 14,
fontWeight: 500,
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
textOverflow: 'ellipsis',
overflow: 'hidden'
}}
> >
<Button onClick={() => goToCanvas(row)} sx={{ textAlign: 'left' }}> <Link to={`/canvas/${row.id}`} style={{ color: '#2196f3', textDecoration: 'none' }}>
{row.templateName || row.name} {row.templateName || row.name}
</Button> </Link>
</Typography> </Typography>
</TableCell> </Tooltip>
<TableCell key='1'> </StyledTableCell>
<StyledTableCell key='1'>
<div <div
style={{ style={{
display: 'flex', display: 'flex',
@@ -91,54 +151,73 @@ export const FlowListTable = ({ data, images, filterFunction, updateFlowsApi })
<Chip key={index} label={tag} style={{ marginRight: 5, marginBottom: 5 }} /> <Chip key={index} label={tag} style={{ marginRight: 5, marginBottom: 5 }} />
))} ))}
</div> </div>
</TableCell> </StyledTableCell>
<TableCell key='2'> <StyledTableCell key='2'>
{images[row.id] && ( {images[row.id] && (
<div <Box
style={{ sx={{
display: 'flex', display: 'flex',
flexDirection: 'row', alignItems: 'center',
flexWrap: 'wrap', justifyContent: 'start',
marginTop: 5 gap: 1
}} }}
> >
{images[row.id].slice(0, images[row.id].length > 5 ? 5 : images[row.id].length).map((img) => ( {images[row.id]
<div .slice(0, images[row.id].length > 5 ? 5 : images[row.id].length)
.map((img) => (
<Box
key={img} key={img}
style={{ sx={{
width: 35, width: 30,
height: 35, height: 30,
marginRight: 5,
borderRadius: '50%', borderRadius: '50%',
backgroundColor: 'white', backgroundColor: customization.isDarkMode
marginTop: 5 ? theme.palette.common.white
: theme.palette.grey[300] + 75
}} }}
> >
<img <img
style={{ width: '100%', height: '100%', padding: 5, objectFit: 'contain' }} style={{
width: '100%',
height: '100%',
padding: 5,
objectFit: 'contain'
}}
alt='' alt=''
src={img} src={img}
/> />
</div> </Box>
))} ))}
{images[row.id].length > 5 && ( {images[row.id].length > 5 && (
<Typography <Typography
sx={{ alignItems: 'center', display: 'flex', fontSize: '.8rem', fontWeight: 200 }} sx={{
alignItems: 'center',
display: 'flex',
fontSize: '.9rem',
fontWeight: 200
}}
> >
+ {images[row.id].length - 5} More + {images[row.id].length - 5} More
</Typography> </Typography>
)} )}
</div> </Box>
)} )}
</TableCell> </StyledTableCell>
<TableCell key='3'>{moment(row.updatedDate).format('MMMM Do, YYYY')}</TableCell> <StyledTableCell key='3'>{moment(row.updatedDate).format('MMMM Do, YYYY')}</StyledTableCell>
<TableCell key='4'> <StyledTableCell key='4'>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={1} justifyContent='center' alignItems='center'> <Stack
<FlowListMenu chatflow={row} updateFlowsApi={updateFlowsApi} /> direction={{ xs: 'column', sm: 'row' }}
spacing={1}
justifyContent='center'
alignItems='center'
>
<FlowListMenu chatflow={row} setError={setError} updateFlowsApi={updateFlowsApi} />
</Stack> </Stack>
</TableCell> </StyledTableCell>
</StyledTableRow> </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,31 +86,78 @@ export const MarketplaceTable = ({ data, filterFunction, filterByBadge, filterBy
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <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>
</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 {data
.filter(filterByBadge) ?.filter(filterByBadge)
.filter(filterByType) .filter(filterByType)
.filter(filterFunction) .filter(filterFunction)
.filter(filterByFramework) .filter(filterByFramework)
.map((row, index) => ( .map((row, index) => (
<StyledTableRow key={index}> <StyledTableRow key={index}>
<TableCell key='0'> <StyledTableCell key='0'>
<Typography <Typography
sx={{ fontSize: '1.2rem', fontWeight: 500, overflowWrap: 'break-word', whiteSpace: 'pre-line' }} sx={{
display: '-webkit-box',
fontSize: 14,
fontWeight: 500,
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
textOverflow: 'ellipsis',
overflow: 'hidden'
}}
> >
<Button onClick={() => openTemplate(row)} sx={{ textAlign: 'left' }}> <Button onClick={() => openTemplate(row)} sx={{ textAlign: 'left' }}>
{row.templateName || row.name} {row.templateName || row.name}
</Button> </Button>
</Typography> </Typography>
</TableCell> </StyledTableCell>
<TableCell key='1'> <StyledTableCell key='1'>
<Typography>{row.type}</Typography> <Typography>{row.type}</Typography>
</TableCell> </StyledTableCell>
<TableCell key='2'> <StyledTableCell key='2'>
<Typography sx={{ overflowWrap: 'break-word', whiteSpace: 'pre-line' }}> <Typography sx={{ overflowWrap: 'break-word', whiteSpace: 'pre-line' }}>
{row.description || ''} {row.description || ''}
</Typography> </Typography>
</TableCell> </StyledTableCell>
<TableCell key='3'> <StyledTableCell key='3'>
<div <div
style={{ style={{
display: 'flex', display: 'flex',
@@ -109,8 +179,8 @@ export const MarketplaceTable = ({ data, filterFunction, filterByBadge, filterBy
/> />
))} ))}
</div> </div>
</TableCell> </StyledTableCell>
<TableCell key='4'> <StyledTableCell key='4'>
<Typography> <Typography>
{row.badge && {row.badge &&
row.badge row.badge
@@ -125,9 +195,11 @@ export const MarketplaceTable = ({ data, filterFunction, filterByBadge, filterBy
/> />
))} ))}
</Typography> </Typography>
</TableCell> </StyledTableCell>
</StyledTableRow> </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
+134 -97
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,87 +314,120 @@ 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,
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'
aria-label='outlined primary button group'
>
<ButtonGroup disableElevation aria-label='outlined primary button group'>
<StyledButton <StyledButton
variant='contained' variant='contained'
sx={{ color: 'white', mr: 1, height: 37 }} sx={{ borderRadius: 2, height: '100%' }}
onClick={addNew} onClick={addNew}
startIcon={<IconPlus />} startIcon={<IconPlus />}
id='btn_createApiKey' id='btn_createApiKey'
> >
Create Key Create Key
</StyledButton> </StyledButton>
</ButtonGroup> </ViewHeader>
</ButtonGroup> {!isLoading && apiKeys.length <= 0 ? (
</Toolbar>
</Box>
</Stack>
{apiKeys.length <= 0 && (
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'> <Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
<Box sx={{ p: 2, height: 'auto' }}> <Box sx={{ p: 2, height: 'auto' }}>
<img style={{ objectFit: 'cover', height: '30vh', width: 'auto' }} src={APIEmptySVG} alt='APIEmptySVG' /> <img
style={{ objectFit: 'cover', height: '16vh', width: 'auto' }}
src={APIEmptySVG}
alt='APIEmptySVG'
/>
</Box> </Box>
<div>No API Keys Yet</div> <div>No API Keys Yet</div>
</Stack> </Stack>
)} ) : (
{apiKeys.length > 0 && ( <TableContainer
<TableContainer component={Paper}> sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2 }}
component={Paper}
>
<Table sx={{ minWidth: 650 }} aria-label='simple table'> <Table sx={{ minWidth: 650 }} aria-label='simple table'>
<TableHead> <TableHead
sx={{
backgroundColor: customization.isDarkMode
? theme.palette.common.black
: theme.palette.grey[100],
height: 56
}}
>
<TableRow> <TableRow>
<TableCell>Key Name</TableCell> <StyledTableCell>Key Name</StyledTableCell>
<TableCell>API Key</TableCell> <StyledTableCell>API Key</StyledTableCell>
<TableCell>Usage</TableCell> <StyledTableCell>Usage</StyledTableCell>
<TableCell>Created</TableCell> <StyledTableCell>Created</StyledTableCell>
<TableCell> </TableCell> <StyledTableCell> </StyledTableCell>
<TableCell> </TableCell> <StyledTableCell> </StyledTableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <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) => ( {apiKeys.filter(filterKeys).map((key, index) => (
<APIKeyRow <APIKeyRow
key={index} key={index}
@@ -417,16 +449,21 @@ const APIKey = () => {
onDeleteClick={() => deleteKey(key)} onDeleteClick={() => deleteKey(key)}
/> />
))} ))}
</>
)}
</TableBody> </TableBody>
</Table> </Table>
</TableContainer> </TableContainer>
)} )}
</Stack>
)}
</MainCard> </MainCard>
<APIKeyDialog <APIKeyDialog
show={showDialog} show={showDialog}
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,19 +419,15 @@ 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 }}
title={'The name of the assistant. The maximum length is 256 characters.'}
/>
</Typography>
</Stack> </Stack>
<OutlinedInput <OutlinedInput
id='assistantName' id='assistantName'
@@ -427,15 +439,10 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
onChange={(e) => setAssistantName(e.target.value)} onChange={(e) => setAssistantName(e.target.value)}
/> />
</Box> </Box>
<Box sx={{ p: 2 }}> <Box>
<Stack sx={{ position: 'relative' }} direction='row'> <Stack sx={{ position: 'relative', alignItems: 'center' }} direction='row'>
<Typography variant='overline'> <Typography variant='overline'>Assistant Description</Typography>
Assistant Description <TooltipWithParser title={'The description of the assistant. The maximum length is 512 characters.'} />
<TooltipWithParser
style={{ marginLeft: 10 }}
title={'The description of the assistant. The maximum length is 512 characters.'}
/>
</Typography>
</Stack> </Stack>
<OutlinedInput <OutlinedInput
id='assistantDesc' id='assistantDesc'
@@ -449,7 +456,7 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
onChange={(e) => setAssistantDesc(e.target.value)} onChange={(e) => setAssistantDesc(e.target.value)}
/> />
</Box> </Box>
<Box sx={{ p: 2 }}> <Box>
<Stack sx={{ position: 'relative' }} direction='row'> <Stack sx={{ position: 'relative' }} direction='row'>
<Typography variant='overline'>Assistant Icon Src</Typography> <Typography variant='overline'>Assistant Icon Src</Typography>
</Stack> </Stack>
@@ -483,7 +490,7 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
onChange={(e) => setAssistantIcon(e.target.value)} onChange={(e) => setAssistantIcon(e.target.value)}
/> />
</Box> </Box>
<Box sx={{ p: 2 }}> <Box>
<Stack sx={{ position: 'relative' }} direction='row'> <Stack sx={{ position: 'relative' }} direction='row'>
<Typography variant='overline'> <Typography variant='overline'>
Assistant Model Assistant Model
@@ -498,7 +505,7 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
value={assistantModel ?? 'choose an option'} value={assistantModel ?? 'choose an option'}
/> />
</Box> </Box>
<Box sx={{ p: 2 }}> <Box>
<Stack sx={{ position: 'relative' }} direction='row'> <Stack sx={{ position: 'relative' }} direction='row'>
<Typography variant='overline'> <Typography variant='overline'>
OpenAI Credential OpenAI Credential
@@ -517,15 +524,12 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
onSelect={(newValue) => setAssistantCredential(newValue)} onSelect={(newValue) => setAssistantCredential(newValue)}
/> />
</Box> </Box>
<Box sx={{ p: 2 }}> <Box>
<Stack sx={{ position: 'relative' }} direction='row'> <Stack sx={{ position: 'relative', alignItems: 'center' }} direction='row'>
<Typography variant='overline'> <Typography variant='overline'>Assistant Instruction</Typography>
Assistant Instruction
<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'
@@ -539,15 +543,10 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
onChange={(e) => setAssistantInstructions(e.target.value)} onChange={(e) => setAssistantInstructions(e.target.value)}
/> />
</Box> </Box>
<Box sx={{ p: 2 }}> <Box>
<Stack sx={{ position: 'relative' }} direction='row'> <Stack sx={{ position: 'relative', alignItems: 'center' }} direction='row'>
<Typography variant='overline'> <Typography variant='overline'>Assistant Tools</Typography>
Assistant Tools <TooltipWithParser title='A list of tool enabled on the assistant. There can be a maximum of 128 tools per assistant.' />
<TooltipWithParser
style={{ marginLeft: 10 }}
title='A list of tool enabled on the assistant. There can be a maximum of 128 tools per assistant.'
/>
</Typography>
</Stack> </Stack>
<MultiDropdown <MultiDropdown
key={JSON.stringify(assistantTools)} key={JSON.stringify(assistantTools)}
@@ -566,15 +565,10 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
value={assistantTools ?? 'choose an option'} value={assistantTools ?? 'choose an option'}
/> />
</Box> </Box>
<Box sx={{ p: 2 }}> <Box>
<Stack sx={{ position: 'relative' }} direction='row'> <Stack sx={{ position: 'relative', alignItems: 'center' }} direction='row'>
<Typography variant='overline'> <Typography variant='overline'>Knowledge Files</Typography>
Knowledge Files <TooltipWithParser title='Allow assistant to use the content from uploaded files for retrieval and code interpreter. MAX: 20 files' />
<TooltipWithParser
style={{ marginLeft: 10 }}
title='Allow assistant to use the content from uploaded files for retrieval and code interpreter. MAX: 20 files'
/>
</Typography>
</Stack> </Stack>
<div style={{ display: 'flex', flexDirection: 'row' }}> <div style={{ display: 'flex', flexDirection: 'row' }}>
{assistantFiles.map((file, index) => ( {assistantFiles.map((file, index) => (
@@ -609,8 +603,9 @@ const AssistantDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
value={uploadAssistantFiles ?? 'Choose a file to upload'} value={uploadAssistantFiles ?? 'Choose a file to upload'}
/> />
</Box> </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
+57 -26
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,59 +84,91 @@ 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} />
</Box>
) : (
<Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
{getAllAssistantsApi.data &&
getAllAssistantsApi.data.map((data, index) => ( getAllAssistantsApi.data.map((data, index) => (
<Grid key={index} item lg={3} md={4} sm={6} xs={12}>
<ItemCard <ItemCard
data={{ data={{
name: JSON.parse(data.details)?.name, name: JSON.parse(data.details)?.name,
description: JSON.parse(data.details)?.instructions, description: JSON.parse(data.details)?.instructions,
iconSrc: data.iconSrc iconSrc: data.iconSrc
}} }}
key={index}
onClick={() => edit(data)} onClick={() => edit(data)}
/> />
</Grid>
))} ))}
</Grid> </Box>
{!getAllAssistantsApi.loading && (!getAllAssistantsApi.data || getAllAssistantsApi.data.length === 0) && ( )}
{!isLoading && (!getAllAssistantsApi.data || getAllAssistantsApi.data.length === 0) && (
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'> <Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
<Box sx={{ p: 2, height: 'auto' }}> <Box sx={{ p: 2, height: 'auto' }}>
<img style={{ objectFit: 'cover', height: '30vh', width: 'auto' }} src={ToolEmptySVG} alt='ToolEmptySVG' /> <img
style={{ objectFit: 'cover', height: '16vh', width: 'auto' }}
src={ToolEmptySVG}
alt='ToolEmptySVG'
/>
</Box> </Box>
<div>No Assistants Added Yet</div> <div>No Assistants Added Yet</div>
</Stack> </Stack>
)} )}
</Stack>
)}
</MainCard> </MainCard>
<LoadAssistantDialog <LoadAssistantDialog
show={showLoadDialog} show={showLoadDialog}
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>
</> </>
) )
+13 -7
View File
@@ -192,6 +192,8 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
return ( return (
<> <>
<Stack flexDirection='row' justifyContent='space-between' sx={{ width: '100%' }}>
<Stack flexDirection='row' sx={{ width: '100%', maxWidth: '50%' }}>
<Box> <Box>
<ButtonBase title='Back' sx={{ borderRadius: '50%' }}> <ButtonBase title='Back' sx={{ borderRadius: '50%' }}>
<Avatar <Avatar
@@ -216,14 +218,17 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
</Avatar> </Avatar>
</ButtonBase> </ButtonBase>
</Box> </Box>
<Box sx={{ flexGrow: 1 }}> <Box sx={{ width: '100%' }}>
{!isEditingFlowName && ( {!isEditingFlowName ? (
<Stack flexDirection='row'> <Stack flexDirection='row'>
<Typography <Typography
sx={{ sx={{
fontSize: '1.5rem', fontSize: '1.5rem',
fontWeight: 600, fontWeight: 600,
ml: 2 ml: 2,
textOverflow: 'ellipsis',
overflow: 'hidden',
whiteSpace: 'nowrap'
}} }}
> >
{canvas.isDirty && <strong style={{ color: theme.palette.orange.main }}>*</strong>} {flowName} {canvas.isDirty && <strong style={{ color: theme.palette.orange.main }}>*</strong>} {flowName}
@@ -252,14 +257,13 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
</ButtonBase> </ButtonBase>
)} )}
</Stack> </Stack>
)} ) : (
{isEditingFlowName && ( <Stack flexDirection='row' sx={{ width: '100%' }}>
<Stack flexDirection='row'>
<TextField <TextField
size='small' size='small'
inputRef={flowNameRef} inputRef={flowNameRef}
sx={{ sx={{
width: '50%', width: '100%',
ml: 2 ml: 2
}} }}
defaultValue={flowName} defaultValue={flowName}
@@ -309,6 +313,7 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
</Stack> </Stack>
)} )}
</Box> </Box>
</Stack>
<Box> <Box>
{chatflow?.id && ( {chatflow?.id && (
<ButtonBase title='API Endpoint' sx={{ borderRadius: '50%', mr: 2 }}> <ButtonBase title='API Endpoint' sx={{ borderRadius: '50%', mr: 2 }}>
@@ -372,6 +377,7 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl
</Avatar> </Avatar>
</ButtonBase> </ButtonBase>
</Box> </Box>
</Stack>
<Settings <Settings
chatflow={chatflow} chatflow={chatflow}
isSettingsOpen={isSettingsOpen} isSettingsOpen={isSettingsOpen}
@@ -97,8 +97,6 @@ const CredentialInputHandler = ({ inputParam, data, onSelect, disabled = false }
{inputParam && ( {inputParam && (
<> <>
{inputParam.type === 'credential' && ( {inputParam.type === 'credential' && (
<>
<div style={{ marginTop: 10 }} />
<div key={reloadTimestamp} style={{ display: 'flex', flexDirection: 'row' }}> <div key={reloadTimestamp} style={{ display: 'flex', flexDirection: 'row' }}>
<AsyncDropdown <AsyncDropdown
disabled={disabled} disabled={disabled}
@@ -119,7 +117,6 @@ const CredentialInputHandler = ({ inputParam, data, onSelect, disabled = false }
</IconButton> </IconButton>
)} )}
</div> </div>
</>
)} )}
</> </>
)} )}
+55 -57
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,41 +127,25 @@ 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}
}}
> >
<h1>Chatflows</h1>
<TextField
size='small'
sx={{ display: { xs: 'none', sm: 'block' }, ml: 3 }}
variant='outlined'
placeholder='Search name or category'
onChange={onSearchChange}
InputProps={{
startAdornment: (
<InputAdornment position='start'>
<IconSearch />
</InputAdornment>
)
}}
/>
<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={handleChange}>
<ToggleButton <ToggleButton
sx={{ color: theme?.customization?.isDarkMode ? 'white' : 'inherit' }} sx={{
borderColor: theme.palette.grey[900] + 25,
borderRadius: 2,
color: theme?.customization?.isDarkMode ? 'white' : 'inherit'
}}
variant='contained' variant='contained'
value='card' value='card'
title='Card View' title='Card View'
@@ -166,7 +153,11 @@ const Chatflows = () => {
<IconLayoutGrid /> <IconLayoutGrid />
</ToggleButton> </ToggleButton>
<ToggleButton <ToggleButton
sx={{ color: theme?.customization?.isDarkMode ? 'white' : 'inherit' }} sx={{
borderColor: theme.palette.grey[900] + 25,
borderRadius: 2,
color: theme?.customization?.isDarkMode ? 'white' : 'inherit'
}}
variant='contained' variant='contained'
value='list' value='list'
title='List View' title='List View'
@@ -174,44 +165,51 @@ const Chatflows = () => {
<IconList /> <IconList />
</ToggleButton> </ToggleButton>
</ToggleButtonGroup> </ToggleButtonGroup>
</ButtonGroup> <StyledButton variant='contained' onClick={addNew} startIcon={<IconPlus />} sx={{ borderRadius: 2, height: 40 }}>
<Box sx={{ width: 5 }} />
<ButtonGroup disableElevation aria-label='outlined primary button group'>
<StyledButton variant='contained' onClick={addNew} startIcon={<IconPlus />}>
Add New Add New
</StyledButton> </StyledButton>
</ButtonGroup> </ViewHeader>
</ButtonGroup> {!view || view === 'card' ? (
</Toolbar> <>
{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>
{!isLoading && (!view || view === 'card') && getAllChatflowsApi.data && ( ) : (
<Grid container spacing={gridSpacing}> <Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
{getAllChatflowsApi.data.filter(filterFlows).map((data, index) => ( {getAllChatflowsApi.data?.filter(filterFlows).map((data, index) => (
<Grid key={index} item lg={3} md={4} sm={6} xs={12}> <ItemCard key={index} onClick={() => goToCanvas(data)} data={data} images={images[data.id]} />
<ItemCard onClick={() => goToCanvas(data)} data={data} images={images[data.id]} />
</Grid>
))} ))}
</Grid> </Box>
)} )}
{!isLoading && view === 'list' && getAllChatflowsApi.data && ( </>
) : (
<FlowListTable <FlowListTable
sx={{ mt: 20 }}
data={getAllChatflowsApi.data} data={getAllChatflowsApi.data}
images={images} images={images}
isLoading={isLoading}
filterFunction={filterFlows} filterFunction={filterFlows}
updateFlowsApi={getAllChatflowsApi} updateFlowsApi={getAllChatflowsApi}
setError={setError}
/> />
)} )}
</Stack>
{!isLoading && (!getAllChatflowsApi.data || getAllChatflowsApi.data.length === 0) && ( {!isLoading && (!getAllChatflowsApi.data || getAllChatflowsApi.data.length === 0) && (
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'> <Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
<Box sx={{ p: 2, height: 'auto' }}> <Box sx={{ p: 2, height: 'auto' }}>
<img style={{ objectFit: 'cover', height: '30vh', width: 'auto' }} src={WorkflowEmptySVG} alt='WorkflowEmptySVG' /> <img
style={{ objectFit: 'cover', height: '16vh', width: 'auto' }}
src={WorkflowEmptySVG}
alt='WorkflowEmptySVG'
/>
</Box> </Box>
<div>No Chatflows Yet</div> <div>No Chatflows Yet</div>
</Stack> </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,35 +104,37 @@ 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'
key={componentCredential.name}
onClick={() => onCredentialSelected(componentCredential)} onClick={() => onCredentialSelected(componentCredential)}
sx={{ p: 0, borderRadius: `${customization.borderRadius}px` }} sx={{
border: 1,
borderColor: theme.palette.grey[900] + 25,
borderRadius: 2,
display: 'flex',
alignItems: 'center',
justifyContent: 'start',
textAlign: 'left',
gap: 1,
p: 2
}}
> >
<ListItem alignItems='center'>
<ListItemAvatar>
<div <div
style={{ style={{
width: 50, width: 50,
@@ -155,11 +155,8 @@ const CredentialListDialog = ({ show, dialogProps, onCancel, onCredentialSelecte
src={`${baseURL}/api/v1/components-credentials-icon/${componentCredential.name}`} src={`${baseURL}/api/v1/components-credentials-icon/${componentCredential.name}`}
/> />
</div> </div>
</ListItemAvatar> <Typography>{componentCredential.label}</Typography>
<ListItemText sx={{ ml: 1 }} primary={componentCredential.label} />
</ListItem>
</ListItemButton> </ListItemButton>
</div>
))} ))}
</List> </List>
</DialogContent> </DialogContent>
+149 -83
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>
<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'
aria-label='outlined primary button group'
>
<ButtonGroup disableElevation aria-label='outlined primary button group'>
<StyledButton <StyledButton
variant='contained' variant='contained'
sx={{ color: 'white', mr: 1, height: 37 }} sx={{ borderRadius: 2, height: '100%' }}
onClick={listCredential} onClick={listCredential}
startIcon={<IconPlus />} startIcon={<IconPlus />}
> >
Add Credential Add Credential
</StyledButton> </StyledButton>
</ButtonGroup> </ViewHeader>
</ButtonGroup> {!isLoading && credentials.length <= 0 ? (
</Toolbar>
</Box>
</Stack>
{credentials.length <= 0 && (
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'> <Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
<Box sx={{ p: 2, height: 'auto' }}> <Box sx={{ p: 2, height: 'auto' }}>
<img <img
style={{ objectFit: 'cover', height: '30vh', width: 'auto' }} style={{ objectFit: 'cover', height: '16vh', width: 'auto' }}
src={CredentialEmptySVG} src={CredentialEmptySVG}
alt='CredentialEmptySVG' alt='CredentialEmptySVG'
/> />
</Box> </Box>
<div>No Credentials Yet</div> <div>No Credentials Yet</div>
</Stack> </Stack>
)} ) : (
{credentials.length > 0 && ( <TableContainer
<TableContainer component={Paper}> sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2 }}
component={Paper}
>
<Table sx={{ minWidth: 650 }} aria-label='simple table'> <Table sx={{ minWidth: 650 }} aria-label='simple table'>
<TableHead> <TableHead
sx={{
backgroundColor: customization.isDarkMode
? theme.palette.common.black
: theme.palette.grey[100],
height: 56
}}
>
<TableRow> <TableRow>
<TableCell>Name</TableCell> <StyledTableCell>Name</StyledTableCell>
<TableCell>Last Updated</TableCell> <StyledTableCell>Last Updated</StyledTableCell>
<TableCell>Created</TableCell> <StyledTableCell>Created</StyledTableCell>
<TableCell> </TableCell> <StyledTableCell> </StyledTableCell>
<TableCell> </TableCell> <StyledTableCell> </StyledTableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <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>
</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) => ( {credentials.filter(filterCredentials).map((credential, index) => (
<TableRow key={index} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}> <StyledTableRow key={index} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
<TableCell component='th' scope='row'> <StyledTableCell scope='row'>
<div <Box
style={{ sx={{
display: 'flex', display: 'flex',
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center' alignItems: 'center',
gap: 1
}} }}
> >
<div <Box
style={{ sx={{
width: 25, width: 35,
height: 25, height: 35,
marginRight: 10, borderRadius: '50%',
borderRadius: '50%' backgroundColor: customization.isDarkMode
? theme.palette.common.white
: theme.palette.grey[300] + 75
}} }}
> >
<img <img
style={{ style={{
width: '100%', width: '100%',
height: '100%', height: '100%',
borderRadius: '50%', padding: 5,
objectFit: 'contain' objectFit: 'contain'
}} }}
alt={credential.credentialName} alt={credential.credentialName}
src={`${baseURL}/api/v1/components-credentials-icon/${credential.credentialName}`} src={`${baseURL}/api/v1/components-credentials-icon/${credential.credentialName}`}
/> />
</div> </Box>
{credential.name} {credential.name}
</div> </Box>
</TableCell> </StyledTableCell>
<TableCell>{moment(credential.updatedDate).format('DD-MMM-YY')}</TableCell> <StyledTableCell>
<TableCell>{moment(credential.createdDate).format('DD-MMM-YY')}</TableCell> {moment(credential.updatedDate).format('MMMM Do, YYYY')}
<TableCell> </StyledTableCell>
<StyledTableCell>
{moment(credential.createdDate).format('MMMM Do, YYYY')}
</StyledTableCell>
<StyledTableCell>
<IconButton title='Edit' color='primary' onClick={() => edit(credential)}> <IconButton title='Edit' color='primary' onClick={() => edit(credential)}>
<IconEdit /> <IconEdit />
</IconButton> </IconButton>
</TableCell> </StyledTableCell>
<TableCell> <StyledTableCell>
<IconButton title='Delete' color='error' onClick={() => deleteCredential(credential)}> <IconButton
title='Delete'
color='error'
onClick={() => deleteCredential(credential)}
>
<IconTrash /> <IconTrash />
</IconButton> </IconButton>
</TableCell> </StyledTableCell>
</TableRow> </StyledTableRow>
))} ))}
</>
)}
</TableBody> </TableBody>
</Table> </Table>
</TableContainer> </TableContainer>
)} )}
</Stack>
)}
</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 />
</> </>
+144 -121
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,92 +220,31 @@ 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, <>
<FormControl
sx={{
borderRadius: 2,
display: 'flex', display: 'flex',
justifyContent: 'space-between', flexDirection: 'column',
width: '100%' justifyContent: 'end',
minWidth: 120
}} }}
> >
<h1>Marketplace</h1>
<TextField
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 />
</ToggleButton>
<ToggleButton
sx={{ color: theme?.customization?.isDarkMode ? 'white' : 'inherit' }}
variant='contained'
value='list'
title='List View'
>
<IconList />
</ToggleButton>
</ToggleButtonGroup>
</ButtonGroup>
</ButtonGroup>
</Toolbar>
</Box>
{open && (
<Box sx={{ flexGrow: 1, mb: 2 }}>
<Toolbar
disableGutters={true}
style={{
margin: 1,
padding: 1,
paddingBottom: 10,
display: 'flex',
justifyContent: 'flex-start',
width: '100%',
borderBottom: '1px solid'
}}
>
<FormControl sx={{ m: 1, width: 250 }}>
<InputLabel size='small' id='filter-badge-label'> <InputLabel size='small' id='filter-badge-label'>
Tag Tag
</InputLabel> </InputLabel>
@@ -322,16 +258,29 @@ const Marketplace = () => {
input={<OutlinedInput label='Badge' />} input={<OutlinedInput label='Badge' />}
renderValue={(selected) => selected.join(', ')} renderValue={(selected) => selected.join(', ')}
MenuProps={MenuProps} MenuProps={MenuProps}
sx={SelectStyles}
> >
{badges.map((name) => ( {badges.map((name) => (
<MenuItem key={name} value={name}> <MenuItem
<Checkbox checked={badgeFilter.indexOf(name) > -1} /> 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} /> <ListItemText primary={name} />
</MenuItem> </MenuItem>
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl sx={{ m: 1, width: 250 }}> <FormControl
sx={{
borderRadius: 2,
display: 'flex',
flexDirection: 'column',
justifyContent: 'end',
minWidth: 120
}}
>
<InputLabel size='small' id='type-badge-label'> <InputLabel size='small' id='type-badge-label'>
Type Type
</InputLabel> </InputLabel>
@@ -345,16 +294,29 @@ const Marketplace = () => {
input={<OutlinedInput label='Badge' />} input={<OutlinedInput label='Badge' />}
renderValue={(selected) => selected.join(', ')} renderValue={(selected) => selected.join(', ')}
MenuProps={MenuProps} MenuProps={MenuProps}
sx={SelectStyles}
> >
{types.map((name) => ( {types.map((name) => (
<MenuItem key={name} value={name}> <MenuItem
<Checkbox checked={typeFilter.indexOf(name) > -1} /> key={name}
value={name}
sx={{ display: 'flex', alignItems: 'center', gap: 1, p: 1 }}
>
<Checkbox checked={typeFilter.indexOf(name) > -1} sx={{ p: 0 }} />
<ListItemText primary={name} /> <ListItemText primary={name} />
</MenuItem> </MenuItem>
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl sx={{ m: 1, width: 250 }}> <FormControl
sx={{
borderRadius: 2,
display: 'flex',
flexDirection: 'column',
justifyContent: 'end',
minWidth: 120
}}
>
<InputLabel size='small' id='type-fw-label'> <InputLabel size='small' id='type-fw-label'>
Framework Framework
</InputLabel> </InputLabel>
@@ -368,32 +330,82 @@ const Marketplace = () => {
input={<OutlinedInput label='Badge' />} input={<OutlinedInput label='Badge' />}
renderValue={(selected) => selected.join(', ')} renderValue={(selected) => selected.join(', ')}
MenuProps={MenuProps} MenuProps={MenuProps}
sx={SelectStyles}
> >
{framework.map((name) => ( {framework.map((name) => (
<MenuItem key={name} value={name}> <MenuItem
<Checkbox checked={frameworkFilter.indexOf(name) > -1} /> 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} /> <ListItemText primary={name} />
</MenuItem> </MenuItem>
))} ))}
</Select> </Select>
</FormControl> </FormControl>
</Toolbar> </>
</Box> }
)} onSearchChange={onSearchChange}
search={true}
{!isLoading && (!view || view === 'card') && getAllTemplatesMarketplacesApi.data && ( searchPlaceholder='Search Name/Description/Node'
title='Marketplace'
>
<ToggleButtonGroup
sx={{ borderRadius: 2, height: '100%' }}
value={view}
color='primary'
exclusive
onChange={handleViewChange}
>
<ToggleButton
sx={{
borderColor: theme.palette.grey[900] + 25,
borderRadius: 2,
color: theme?.customization?.isDarkMode ? 'white' : 'inherit'
}}
variant='contained'
value='card'
title='Card View'
>
<IconLayoutGrid />
</ToggleButton>
<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>
</ViewHeader>
{!view || view === 'card' ? (
<> <>
<Grid container spacing={gridSpacing}> {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}>
{getAllTemplatesMarketplacesApi.data {getAllTemplatesMarketplacesApi.data
.filter(filterByBadge) ?.filter(filterByBadge)
.filter(filterByType) .filter(filterByType)
.filter(filterFlows) .filter(filterFlows)
.filter(filterByFramework) .filter(filterByFramework)
.map((data, index) => ( .map((data, index) => (
<Grid key={index} item lg={3} md={4} sm={6} xs={12}> <Box key={index}>
{data.badge && ( {data.badge && (
<Badge <Badge
sx={{ sx={{
width: '100%',
height: '100%',
'& .MuiBadge-badge': { '& .MuiBadge-badge': {
right: 20 right: 20
} }
@@ -402,23 +414,30 @@ const Marketplace = () => {
color={data.badge === 'POPULAR' ? 'primary' : 'error'} color={data.badge === 'POPULAR' ? 'primary' : 'error'}
> >
{data.type === 'Chatflow' && ( {data.type === 'Chatflow' && (
<ItemCard onClick={() => goToCanvas(data)} data={data} images={images[data.id]} /> <ItemCard
onClick={() => goToCanvas(data)}
data={data}
images={images[data.id]}
/>
)}
{data.type === 'Tool' && (
<ItemCard data={data} onClick={() => goToTool(data)} />
)} )}
{data.type === 'Tool' && <ItemCard data={data} onClick={() => goToTool(data)} />}
</Badge> </Badge>
)} )}
{!data.badge && data.type === 'Chatflow' && ( {!data.badge && data.type === 'Chatflow' && (
<ItemCard onClick={() => goToCanvas(data)} data={data} images={images[data.id]} /> <ItemCard onClick={() => goToCanvas(data)} data={data} images={images[data.id]} />
)} )}
{!data.badge && data.type === 'Tool' && <ItemCard data={data} onClick={() => goToTool(data)} />} {!data.badge && data.type === 'Tool' && (
</Grid> <ItemCard data={data} onClick={() => goToTool(data)} />
))}
</Grid>
</>
)} )}
{!isLoading && view === 'list' && getAllTemplatesMarketplacesApi.data && ( </Box>
))}
</Box>
)}
</>
) : (
<MarketplaceTable <MarketplaceTable
sx={{ mt: 20 }}
data={getAllTemplatesMarketplacesApi.data} data={getAllTemplatesMarketplacesApi.data}
filterFunction={filterFlows} filterFunction={filterFlows}
filterByType={filterByType} filterByType={filterByType}
@@ -426,6 +445,8 @@ const Marketplace = () => {
filterByFramework={filterByFramework} filterByFramework={filterByFramework}
goToTool={goToTool} goToTool={goToTool}
goToCanvas={goToCanvas} goToCanvas={goToCanvas}
isLoading={isLoading}
setError={setError}
/> />
)} )}
@@ -433,7 +454,7 @@ const Marketplace = () => {
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'> <Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
<Box sx={{ p: 2, height: 'auto' }}> <Box sx={{ p: 2, height: 'auto' }}>
<img <img
style={{ objectFit: 'cover', height: '30vh', width: 'auto' }} style={{ objectFit: 'cover', height: '16vh', width: 'auto' }}
src={WorkflowEmptySVG} src={WorkflowEmptySVG}
alt='WorkflowEmptySVG' alt='WorkflowEmptySVG'
/> />
@@ -441,6 +462,8 @@ const Marketplace = () => {
<div>No Marketplace Yet</div> <div>No Marketplace Yet</div>
</Stack> </Stack>
)} )}
</Stack>
)}
</MainCard> </MainCard>
<ToolDialog <ToolDialog
show={showToolDialog} show={showToolDialog}
+49 -47
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,28 +390,25 @@ 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> </Box>
</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>
<Stack sx={{ position: 'relative', alignItems: 'center' }} direction='row'>
<Typography variant='overline'> <Typography variant='overline'>
Tool Name Tool Name
<span style={{ color: 'red' }}>&nbsp;*</span> <span style={{ color: 'red' }}>&nbsp;*</span>
<TooltipWithParser
style={{ marginLeft: 10 }}
title={'Tool name must be small capital letter with underscore. Ex: my_tool'}
/>
</Typography> </Typography>
<TooltipWithParser title={'Tool name must be small capital letter with underscore. Ex: my_tool'} />
</Stack> </Stack>
<OutlinedInput <OutlinedInput
id='toolName' id='toolName'
@@ -417,16 +421,15 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm }) =
onChange={(e) => setToolName(e.target.value)} onChange={(e) => setToolName(e.target.value)}
/> />
</Box> </Box>
<Box sx={{ p: 2 }}> <Box>
<Stack sx={{ position: 'relative' }} direction='row'> <Stack sx={{ position: 'relative', alignItems: 'center' }} direction='row'>
<Typography variant='overline'> <Typography variant='overline'>
Tool description Tool description
<span style={{ color: 'red' }}>&nbsp;*</span> <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'
@@ -441,9 +444,9 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm }) =
onChange={(e) => setToolDesc(e.target.value)} onChange={(e) => setToolDesc(e.target.value)}
/> />
</Box> </Box>
<Box sx={{ p: 2 }}> <Box>
<Stack sx={{ position: 'relative' }} direction='row'> <Stack sx={{ position: 'relative' }} direction='row'>
<Typography variant='overline'>Tool Icon Src</Typography> <Typography variant='overline'>Tool Icon Source</Typography>
</Stack> </Stack>
<OutlinedInput <OutlinedInput
id='toolIcon' id='toolIcon'
@@ -456,35 +459,31 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm }) =
onChange={(e) => setToolIcon(e.target.value)} onChange={(e) => setToolIcon(e.target.value)}
/> />
</Box> </Box>
<Box sx={{ p: 2 }}> <Box>
<Stack sx={{ position: 'relative' }} direction='row'> <Stack sx={{ position: 'relative', justifyContent: 'space-between' }} direction='row'>
<Typography variant='overline'> <Stack sx={{ position: 'relative', alignItems: 'center' }} direction='row'>
Output Schema <Typography variant='overline'>Output Schema</Typography>
<TooltipWithParser style={{ marginLeft: 10 }} title={'What should be the output response in JSON format?'} /> <TooltipWithParser title={'What should be the output response in JSON format?'} />
</Typography>
</Stack> </Stack>
<Grid {dialogProps.type !== 'TEMPLATE' && (
columns={columns} <Button variant='outlined' onClick={addNewRow} startIcon={<IconPlus />}>
rows={toolSchema} Add Item
disabled={dialogProps.type === 'TEMPLATE'} </Button>
addNewRow={addNewRow} )}
onRowUpdate={onRowUpdate} </Stack>
/> <Grid columns={columns} rows={toolSchema} disabled={dialogProps.type === 'TEMPLATE'} onRowUpdate={onRowUpdate} />
</Box> </Box>
<Box sx={{ p: 2 }}> <Box>
<Stack sx={{ position: 'relative' }} direction='row'> <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Typography variant='overline'> <Stack sx={{ position: 'relative', alignItems: 'center' }} direction='row'>
Javascript Function <Typography variant='overline'>Javascript Function</Typography>
<TooltipWithParser <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>' />
style={{ marginLeft: 10 }}
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>'
/>
</Typography>
</Stack> </Stack>
<Stack direction='row'>
<Button <Button
style={{ marginBottom: 10, marginRight: 10 }} style={{ marginBottom: 10, marginRight: 10 }}
color='secondary' color='secondary'
variant='outlined' variant='text'
onClick={() => setShowHowToDialog(true)} onClick={() => setShowHowToDialog(true)}
> >
How to use Function How to use Function
@@ -494,17 +493,19 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm }) =
See Example See Example
</Button> </Button>
)} )}
</Stack>
</Box>
<CodeEditor <CodeEditor
disabled={dialogProps.type === 'TEMPLATE'} disabled={dialogProps.type === 'TEMPLATE'}
value={toolFunc} value={toolFunc}
height='calc(100vh - 220px)'
theme={customization.isDarkMode ? 'dark' : 'light'} theme={customization.isDarkMode ? 'dark' : 'light'}
lang={'js'} lang={'js'}
onValueChange={(code) => setToolFunc(code)} onValueChange={(code) => setToolFunc(code)}
/> />
</Box> </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
+64 -29
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,52 +100,88 @@ 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'>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Button <Button
variant='outlined' variant='outlined'
sx={{ mr: 2 }}
onClick={() => inputRef.current.click()} onClick={() => inputRef.current.click()}
startIcon={<IconFileImport />} startIcon={<IconFileUpload />}
sx={{ borderRadius: 2, height: 40 }}
> >
Load Load
</Button> </Button>
<input ref={inputRef} type='file' hidden accept='.json' onChange={(e) => handleFileUpload(e)} /> <input
<StyledButton variant='contained' sx={{ color: 'white' }} onClick={addNew} startIcon={<IconPlus />}> style={{ display: 'none' }}
ref={inputRef}
type='file'
hidden
accept='.json'
onChange={(e) => handleFileUpload(e)}
/>
</Box>
<ButtonGroup disableElevation aria-label='outlined primary button group'>
<StyledButton
variant='contained'
onClick={addNew}
startIcon={<IconPlus />}
sx={{ borderRadius: 2, height: 40 }}
>
Create Create
</StyledButton> </StyledButton>
</Grid> </ButtonGroup>
</Grid> </ViewHeader>
</Stack> {isLoading ? (
<Grid container spacing={gridSpacing}> <Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
{!getAllToolsApi.loading && <Skeleton variant='rounded' height={160} />
getAllToolsApi.data && <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) => ( getAllToolsApi.data.map((data, index) => (
<Grid key={index} item lg={3} md={4} sm={6} xs={12}> <ItemCard data={data} key={index} onClick={() => edit(data)} />
<ItemCard data={data} onClick={() => edit(data)} />
</Grid>
))} ))}
</Grid> </Box>
{!getAllToolsApi.loading && (!getAllToolsApi.data || getAllToolsApi.data.length === 0) && ( )}
{!isLoading && (!getAllToolsApi.data || getAllToolsApi.data.length === 0) && (
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'> <Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
<Box sx={{ p: 2, height: 'auto' }}> <Box sx={{ p: 2, height: 'auto' }}>
<img style={{ objectFit: 'cover', height: '30vh', width: 'auto' }} src={ToolEmptySVG} alt='ToolEmptySVG' /> <img
style={{ objectFit: 'cover', height: '16vh', width: 'auto' }}
src={ToolEmptySVG}
alt='ToolEmptySVG'
/>
</Box> </Box>
<div>No Tools Created Yet</div> <div>No Tools Created Yet</div>
</Stack> </Stack>
)} )}
</Stack>
)}
</MainCard> </MainCard>
<ToolDialog <ToolDialog
show={showDialog} show={showDialog}
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
+148 -79
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,90 +193,115 @@ 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
sx={{ maxHeight: 40 }}
disableElevation
variant='contained'
aria-label='outlined primary button group'
>
<ButtonGroup disableElevation aria-label='outlined primary button group'>
<StyledButton <StyledButton
variant='contained' variant='contained'
sx={{ color: 'white', mr: 1, height: 37 }} sx={{ borderRadius: 2, height: '100%' }}
onClick={addNew} onClick={addNew}
startIcon={<IconPlus />} startIcon={<IconPlus />}
id='btn_createVariable' id='btn_createVariable'
> >
Add Variable Add Variable
</StyledButton> </StyledButton>
</ButtonGroup> </ViewHeader>
</ButtonGroup> {!isLoading && variables.length === 0 ? (
</Toolbar>
</Box>
</Stack>
{variables.length === 0 && (
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'> <Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
<Box sx={{ p: 2, height: 'auto' }}> <Box sx={{ p: 2, height: 'auto' }}>
<img <img
style={{ objectFit: 'cover', height: '30vh', width: 'auto' }} style={{ objectFit: 'cover', height: '16vh', width: 'auto' }}
src={VariablesEmptySVG} src={VariablesEmptySVG}
alt='VariablesEmptySVG' alt='VariablesEmptySVG'
/> />
</Box> </Box>
<div>No Variables Yet</div> <div>No Variables Yet</div>
</Stack> </Stack>
)} ) : (
{variables.length > 0 && ( <TableContainer
<TableContainer component={Paper}> sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2 }}
component={Paper}
>
<Table sx={{ minWidth: 650 }} aria-label='simple table'> <Table sx={{ minWidth: 650 }} aria-label='simple table'>
<TableHead> <TableHead
sx={{
backgroundColor: customization.isDarkMode
? theme.palette.common.black
: theme.palette.grey[100],
height: 56
}}
>
<TableRow> <TableRow>
<TableCell>Name</TableCell> <StyledTableCell>Name</StyledTableCell>
<TableCell>Value</TableCell> <StyledTableCell>Value</StyledTableCell>
<TableCell>Type</TableCell> <StyledTableCell>Type</StyledTableCell>
<TableCell>Last Updated</TableCell> <StyledTableCell>Last Updated</StyledTableCell>
<TableCell>Created</TableCell> <StyledTableCell>Created</StyledTableCell>
<TableCell> </TableCell> <StyledTableCell> </StyledTableCell>
<TableCell> </TableCell> <StyledTableCell> </StyledTableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <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>
<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) => ( {variables.filter(filterVariables).map((variable, index) => (
<TableRow key={index} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}> <StyledTableRow key={index} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
<TableCell component='th' scope='row'> <StyledTableCell component='th' scope='row'>
<div <div
style={{ style={{
display: 'flex', display: 'flex',
@@ -272,39 +328,52 @@ const Variables = () => {
</div> </div>
{variable.name} {variable.name}
</div> </div>
</TableCell> </StyledTableCell>
<TableCell>{variable.value}</TableCell> <StyledTableCell>{variable.value}</StyledTableCell>
<TableCell> <StyledTableCell>
<Chip <Chip
color={variable.type === 'static' ? 'info' : 'secondary'} color={variable.type === 'static' ? 'info' : 'secondary'}
size='small' size='small'
label={variable.type} label={variable.type}
/> />
</TableCell> </StyledTableCell>
<TableCell>{moment(variable.updatedDate).format('DD-MMM-YY')}</TableCell> <StyledTableCell>
<TableCell>{moment(variable.createdDate).format('DD-MMM-YY')}</TableCell> {moment(variable.updatedDate).format('MMMM Do, YYYY')}
<TableCell> </StyledTableCell>
<StyledTableCell>
{moment(variable.createdDate).format('MMMM Do, YYYY')}
</StyledTableCell>
<StyledTableCell>
<IconButton title='Edit' color='primary' onClick={() => edit(variable)}> <IconButton title='Edit' color='primary' onClick={() => edit(variable)}>
<IconEdit /> <IconEdit />
</IconButton> </IconButton>
</TableCell> </StyledTableCell>
<TableCell> <StyledTableCell>
<IconButton title='Delete' color='error' onClick={() => deleteVariable(variable)}> <IconButton
title='Delete'
color='error'
onClick={() => deleteVariable(variable)}
>
<IconTrash /> <IconTrash />
</IconButton> </IconButton>
</TableCell> </StyledTableCell>
</TableRow> </StyledTableRow>
))} ))}
</>
)}
</TableBody> </TableBody>
</Table> </Table>
</TableContainer> </TableContainer>
)} )}
</Stack>
)}
</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 />