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