mirror of
https://github.com/farcasclaudiu/Flowise.git
synced 2026-06-28 21:00:58 +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:
@@ -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