From 19bb23440ac1ae3c1787a2729096e7d01a329e0f Mon Sep 17 00:00:00 2001 From: Ilango Date: Mon, 8 Apr 2024 11:15:42 +0530 Subject: [PATCH] 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 --- packages/ui/src/ErrorBoundary.jsx | 51 ++ .../Sidebar/MenuList/NavGroup/index.jsx | 1 + .../src/layout/MainLayout/Sidebar/index.jsx | 27 +- .../ui/src/layout/MainLayout/ViewHeader.jsx | 83 ++++ packages/ui/src/layout/MainLayout/index.jsx | 22 +- packages/ui/src/store/constant.js | 1 + .../src/ui-component/button/FlowListMenu.jsx | 6 +- .../ui/src/ui-component/cards/ItemCard.jsx | 180 +++---- .../ui/src/ui-component/cards/MainCard.jsx | 14 +- .../ui-component/dropdown/AsyncDropdown.jsx | 3 +- .../ui/src/ui-component/dropdown/Dropdown.jsx | 7 +- .../ui-component/dropdown/MultiDropdown.jsx | 7 +- packages/ui/src/ui-component/grid/Grid.jsx | 10 +- .../src/ui-component/table/FlowListTable.jsx | 277 +++++++---- .../ui-component/table/MarketplaceTable.jsx | 243 +++++---- .../tooltip/TooltipWithParser.jsx | 10 +- packages/ui/src/views/apikey/APIKeyDialog.jsx | 7 +- packages/ui/src/views/apikey/index.jsx | 309 +++++++----- .../src/views/assistants/AssistantDialog.jsx | 397 ++++++++------- .../views/assistants/LoadAssistantDialog.jsx | 12 +- packages/ui/src/views/assistants/index.jsx | 111 +++-- packages/ui/src/views/canvas/CanvasHeader.jsx | 306 ++++++------ .../views/canvas/CredentialInputHandler.jsx | 43 +- packages/ui/src/views/chatflows/index.jsx | 176 ++++--- .../credentials/AddEditCredentialDialog.jsx | 21 +- .../credentials/CredentialListDialog.jsx | 115 +++-- packages/ui/src/views/credentials/index.jsx | 318 +++++++----- packages/ui/src/views/marketplaces/index.jsx | 467 +++++++++--------- packages/ui/src/views/tools/ToolDialog.jsx | 232 ++++----- packages/ui/src/views/tools/index.jsx | 121 +++-- .../views/variables/AddEditVariableDialog.jsx | 7 +- packages/ui/src/views/variables/index.jsx | 345 +++++++------ 32 files changed, 2267 insertions(+), 1662 deletions(-) create mode 100644 packages/ui/src/ErrorBoundary.jsx create mode 100644 packages/ui/src/layout/MainLayout/ViewHeader.jsx diff --git a/packages/ui/src/ErrorBoundary.jsx b/packages/ui/src/ErrorBoundary.jsx new file mode 100644 index 00000000..63636973 --- /dev/null +++ b/packages/ui/src/ErrorBoundary.jsx @@ -0,0 +1,51 @@ +import PropTypes from 'prop-types' + +import { Box, Card, IconButton, Stack, Typography, useTheme } from '@mui/material' +import { IconCopy } from '@tabler/icons' + +const ErrorBoundary = ({ error }) => { + const theme = useTheme() + + const copyToClipboard = () => { + const errorMessage = `Status: ${error.response.status}\n${error.response.data.message}` + navigator.clipboard.writeText(errorMessage) + } + + return ( + + + + Oh snap! + The following error occured when loading this page. + + + + + + +
+                            {`Status: ${error.response.status}`}
+                            
+ {error.response.data.message} +
+
+
+ + Please retry after some time. If the issue persists, reach out to us on our Discord server. +
+ Alternatively, you can raise an issue on Github. +
+
+
+ ) +} + +ErrorBoundary.propTypes = { + error: PropTypes.object +} + +export default ErrorBoundary diff --git a/packages/ui/src/layout/MainLayout/Sidebar/MenuList/NavGroup/index.jsx b/packages/ui/src/layout/MainLayout/Sidebar/MenuList/NavGroup/index.jsx index 1f33210e..0625d54e 100644 --- a/packages/ui/src/layout/MainLayout/Sidebar/MenuList/NavGroup/index.jsx +++ b/packages/ui/src/layout/MainLayout/Sidebar/MenuList/NavGroup/index.jsx @@ -44,6 +44,7 @@ const NavGroup = ({ item }) => { ) } + sx={{ py: '20px' }} > {items} diff --git a/packages/ui/src/layout/MainLayout/Sidebar/index.jsx b/packages/ui/src/layout/MainLayout/Sidebar/index.jsx index eafe8672..efdda72c 100644 --- a/packages/ui/src/layout/MainLayout/Sidebar/index.jsx +++ b/packages/ui/src/layout/MainLayout/Sidebar/index.jsx @@ -11,7 +11,7 @@ import { BrowserView, MobileView } from 'react-device-detect' // project imports import MenuList from './MenuList' import LogoSection from '../LogoSection' -import { drawerWidth } from '@/store/constant' +import { drawerWidth, headerHeight } from '@/store/constant' // ==============================|| SIDEBAR DRAWER ||============================== // @@ -21,7 +21,12 @@ const Sidebar = ({ drawerOpen, drawerToggle, window }) => { const drawer = ( <> - + @@ -30,7 +35,7 @@ const Sidebar = ({ drawerOpen, drawerToggle, window }) => { { const container = window !== undefined ? () => window.document.body : undefined return ( - + { width: drawerWidth, background: theme.palette.background.default, color: theme.palette.text.primary, - borderRight: 'none', [theme.breakpoints.up('md')]: { - top: '66px' - } + top: `${headerHeight}px` + }, + borderRight: drawerOpen ? '1px solid' : 'none', + borderColor: drawerOpen ? theme.palette.primary[200] + 75 : 'transparent' } }} ModalProps={{ keepMounted: true }} diff --git a/packages/ui/src/layout/MainLayout/ViewHeader.jsx b/packages/ui/src/layout/MainLayout/ViewHeader.jsx new file mode 100644 index 00000000..2c1388d5 --- /dev/null +++ b/packages/ui/src/layout/MainLayout/ViewHeader.jsx @@ -0,0 +1,83 @@ +import PropTypes from 'prop-types' + +// material-ui +import { Box, OutlinedInput, Toolbar, Typography } from '@mui/material' +import { useTheme } from '@mui/material/styles' + +// icons +import { IconSearch } from '@tabler/icons' + +const ViewHeader = ({ children, filters = null, onSearchChange, search, searchPlaceholder = 'Search', title }) => { + const theme = useTheme() + + return ( + + + + {title} + + + {search && ( + + + + } + type='search' + /> + )} + {filters} + {children} + + + + ) +} + +ViewHeader.propTypes = { + children: PropTypes.node, + filters: PropTypes.node, + onSearchChange: PropTypes.func, + search: PropTypes.bool, + searchPlaceholder: PropTypes.string, + title: PropTypes.string +} + +export default ViewHeader diff --git a/packages/ui/src/layout/MainLayout/index.jsx b/packages/ui/src/layout/MainLayout/index.jsx index 0cbcc0d9..236d27c9 100644 --- a/packages/ui/src/layout/MainLayout/index.jsx +++ b/packages/ui/src/layout/MainLayout/index.jsx @@ -9,21 +9,23 @@ import { AppBar, Box, CssBaseline, Toolbar, useMediaQuery } from '@mui/material' // project imports import Header from './Header' import Sidebar from './Sidebar' -import { drawerWidth } from '@/store/constant' +import { drawerWidth, headerHeight } from '@/store/constant' import { SET_MENU } from '@/store/actions' // styles const Main = styled('main', { shouldForwardProp: (prop) => prop !== 'open' })(({ theme, open }) => ({ ...theme.typography.mainContent, ...(!open && { + backgroundColor: 'transparent', borderBottomLeftRadius: 0, borderBottomRightRadius: 0, - transition: theme.transitions.create('margin', { + transition: theme.transitions.create('all', { easing: theme.transitions.easing.sharp, duration: theme.transitions.duration.leavingScreen }), + marginRight: 0, [theme.breakpoints.up('md')]: { - marginLeft: -(drawerWidth - 20), + marginLeft: -drawerWidth, width: `calc(100% - ${drawerWidth}px)` }, [theme.breakpoints.down('md')]: { @@ -39,20 +41,16 @@ const Main = styled('main', { shouldForwardProp: (prop) => prop !== 'open' })(({ } }), ...(open && { - transition: theme.transitions.create('margin', { + backgroundColor: 'transparent', + transition: theme.transitions.create('all', { easing: theme.transitions.easing.easeOut, duration: theme.transitions.duration.enteringScreen }), marginLeft: 0, + marginRight: 0, borderBottomLeftRadius: 0, borderBottomRightRadius: 0, - width: `calc(100% - ${drawerWidth}px)`, - [theme.breakpoints.down('md')]: { - marginLeft: '20px' - }, - [theme.breakpoints.down('sm')]: { - marginLeft: '10px' - } + width: `calc(100% - ${drawerWidth}px)` }) })) @@ -88,7 +86,7 @@ const MainLayout = () => { transition: leftDrawerOpened ? theme.transitions.create('width') : 'none' }} > - +
diff --git a/packages/ui/src/store/constant.js b/packages/ui/src/store/constant.js index c420558b..8081815c 100644 --- a/packages/ui/src/store/constant.js +++ b/packages/ui/src/store/constant.js @@ -2,6 +2,7 @@ export const gridSpacing = 3 export const drawerWidth = 260 export const appDrawerWidth = 320 +export const headerHeight = 80 export const maxScroll = 100000 export const baseURL = import.meta.env.PROD === true diff --git a/packages/ui/src/ui-component/button/FlowListMenu.jsx b/packages/ui/src/ui-component/button/FlowListMenu.jsx index a70e7276..d8023194 100644 --- a/packages/ui/src/ui-component/button/FlowListMenu.jsx +++ b/packages/ui/src/ui-component/button/FlowListMenu.jsx @@ -72,7 +72,7 @@ const StyledMenu = styled((props) => ( } })) -export default function FlowListMenu({ chatflow, updateFlowsApi }) { +export default function FlowListMenu({ chatflow, setError, updateFlowsApi }) { const { confirm } = useConfirm() const dispatch = useDispatch() const updateChatflowApi = useApi(chatflowsApi.updateChatflow) @@ -153,6 +153,7 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { await updateChatflowApi.request(chatflow.id, updateBody) await updateFlowsApi.request() } catch (error) { + setError(error) enqueueSnackbar({ message: error.response.data.message, options: { @@ -191,6 +192,7 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { await updateChatflowApi.request(chatflow.id, updateBody) await updateFlowsApi.request() } catch (error) { + setError(error) enqueueSnackbar({ message: error.response.data.message, options: { @@ -222,6 +224,7 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { await chatflowsApi.deleteChatflow(chatflow.id) await updateFlowsApi.request() } catch (error) { + setError(error) enqueueSnackbar({ message: error.response.data.message, options: { @@ -370,5 +373,6 @@ export default function FlowListMenu({ chatflow, updateFlowsApi }) { FlowListMenu.propTypes = { chatflow: PropTypes.object, + setError: PropTypes.func, updateFlowsApi: PropTypes.object } diff --git a/packages/ui/src/ui-component/cards/ItemCard.jsx b/packages/ui/src/ui-component/cards/ItemCard.jsx index 37f1eb54..f2774e1b 100644 --- a/packages/ui/src/ui-component/cards/ItemCard.jsx +++ b/packages/ui/src/ui-component/cards/ItemCard.jsx @@ -1,12 +1,12 @@ import PropTypes from 'prop-types' +import { useSelector } from 'react-redux' // material-ui import { styled } from '@mui/material/styles' -import { Box, Grid, Typography } from '@mui/material' +import { Box, Grid, Typography, useTheme } from '@mui/material' // project imports import MainCard from '@/ui-component/cards/MainCard' -import SkeletonChatflowCard from '@/ui-component/cards/Skeleton/ChatflowCard' const CardWrapper = styled(MainCard)(({ theme }) => ({ background: theme.palette.card.main, @@ -19,101 +19,115 @@ const CardWrapper = styled(MainCard)(({ theme }) => ({ background: theme.palette.card.hover, boxShadow: '0 2px 14px 0 rgb(32 40 45 / 20%)' }, + height: '100%', + minHeight: '160px', maxHeight: '300px', - maxWidth: '300px', + width: '100%', overflowWrap: 'break-word', whiteSpace: 'pre-line' })) // ===========================|| CONTRACT CARD ||=========================== // -const ItemCard = ({ isLoading, data, images, onClick }) => { +const ItemCard = ({ data, images, onClick }) => { + const theme = useTheme() + const customization = useSelector((state) => state.customization) + return ( - <> - {isLoading ? ( - - ) : ( - - - -
- {data.iconSrc && ( -
- )} - {!data.iconSrc && data.color && ( -
- )} - - {data.templateName || data.name} - -
- {data.description && ( - - {data.description} - - )} - {images && ( + + + + +
+ {data.iconSrc && (
+ )} + {!data.iconSrc && data.color && ( +
+ )} + + {data.templateName || data.name} + +
+ {data.description && ( + {data.description} + )} +
+ {images && ( + + {images.slice(0, images.length > 3 ? 3 : images.length).map((img) => ( + - {images.map((img) => ( -
- -
- ))} - + +
+ ))} + {images.length > 3 && ( + + + {images.length - 3} More + )} -
-
-
- )} - +
+ )} + + +
) } diff --git a/packages/ui/src/ui-component/cards/MainCard.jsx b/packages/ui/src/ui-component/cards/MainCard.jsx index 302b15da..61bbf752 100644 --- a/packages/ui/src/ui-component/cards/MainCard.jsx +++ b/packages/ui/src/ui-component/cards/MainCard.jsx @@ -2,7 +2,6 @@ import PropTypes from 'prop-types' import { forwardRef } from 'react' // material-ui -import { useTheme } from '@mui/material/styles' import { Card, CardContent, CardHeader, Divider, Typography } from '@mui/material' // constant @@ -14,12 +13,14 @@ const headerSX = { const MainCard = forwardRef(function MainCard( { - border = true, boxShadow, children, content = true, contentClass = '', - contentSX = {}, + contentSX = { + px: 2, + py: 0 + }, darkTitle, secondary, shadow, @@ -29,18 +30,17 @@ const MainCard = forwardRef(function MainCard( }, ref ) { - const theme = useTheme() - return ( diff --git a/packages/ui/src/ui-component/dropdown/AsyncDropdown.jsx b/packages/ui/src/ui-component/dropdown/AsyncDropdown.jsx index ac95d3f4..1eb1776a 100644 --- a/packages/ui/src/ui-component/dropdown/AsyncDropdown.jsx +++ b/packages/ui/src/ui-component/dropdown/AsyncDropdown.jsx @@ -114,7 +114,7 @@ export const AsyncDropdown = ({ disabled={disabled} disableClearable={disableClearable} size='small' - sx={{ width: '100%' }} + sx={{ width: '100%', height: '52px' }} open={open} onOpen={() => { setOpen(true) @@ -148,6 +148,7 @@ export const AsyncDropdown = ({ ) }} + sx={{ height: '100%', '& .MuiInputBase-root': { height: '100%' } }} /> )} renderOption={(props, option) => ( diff --git a/packages/ui/src/ui-component/dropdown/Dropdown.jsx b/packages/ui/src/ui-component/dropdown/Dropdown.jsx index 12d10bef..801169a9 100644 --- a/packages/ui/src/ui-component/dropdown/Dropdown.jsx +++ b/packages/ui/src/ui-component/dropdown/Dropdown.jsx @@ -25,7 +25,7 @@ export const Dropdown = ({ name, value, options, onSelect, disabled = false, dis let [internalValue, setInternalValue] = useState(value ?? 'choose an option') return ( - + } + renderInput={(params) => ( + + )} renderOption={(props, option) => (
@@ -50,6 +52,7 @@ export const Dropdown = ({ name, value, options, onSelect, disabled = false, dis
)} + sx={{ height: '100%' }} />
) diff --git a/packages/ui/src/ui-component/dropdown/MultiDropdown.jsx b/packages/ui/src/ui-component/dropdown/MultiDropdown.jsx index 9b96e55c..3e6e9f1a 100644 --- a/packages/ui/src/ui-component/dropdown/MultiDropdown.jsx +++ b/packages/ui/src/ui-component/dropdown/MultiDropdown.jsx @@ -30,7 +30,7 @@ export const MultiDropdown = ({ name, value, options, onSelect, formControlSx = let [internalValue, setInternalValue] = useState(value ?? []) return ( - + } + renderInput={(params) => ( + + )} renderOption={(props, option) => (
@@ -64,6 +66,7 @@ export const MultiDropdown = ({ name, value, options, onSelect, formControlSx =
)} + sx={{ height: '100%' }} />
) diff --git a/packages/ui/src/ui-component/grid/Grid.jsx b/packages/ui/src/ui-component/grid/Grid.jsx index 0670d69b..66251597 100644 --- a/packages/ui/src/ui-component/grid/Grid.jsx +++ b/packages/ui/src/ui-component/grid/Grid.jsx @@ -1,9 +1,7 @@ import PropTypes from 'prop-types' import { DataGrid } from '@mui/x-data-grid' -import { IconPlus } from '@tabler/icons' -import { Button } from '@mui/material' -export const Grid = ({ columns, rows, style, disabled = false, onRowUpdate, addNewRow }) => { +export const Grid = ({ columns, rows, style, disabled = false, onRowUpdate }) => { const handleProcessRowUpdate = (newRow) => { onRowUpdate(newRow) return newRow @@ -11,11 +9,6 @@ export const Grid = ({ columns, rows, style, disabled = false, onRowUpdate, addN return ( <> - {!disabled && ( - - )} {rows && columns && (
({ + borderColor: theme.palette.grey[900] + 25, + [`&.${tableCellClasses.head}`]: { - backgroundColor: theme.palette.common.black, - color: theme.palette.common.white + color: theme.palette.grey[900] }, [`&.${tableCellClasses.body}`]: { - fontSize: 14 + fontSize: 14, + height: 64 } })) -const StyledTableRow = styled(TableRow)(({ theme }) => ({ - '&:nth-of-type(odd)': { - backgroundColor: theme.palette.action.hover - }, +const StyledTableRow = styled(TableRow)(() => ({ // hide last border '&:last-child td, &:last-child th': { border: 0 } })) -export const FlowListTable = ({ data, images, filterFunction, updateFlowsApi }) => { - const navigate = useNavigate() - const goToCanvas = (selectedChatflow) => { - navigate(`/canvas/${selectedChatflow.id}`) - } +export const FlowListTable = ({ data, images, isLoading, filterFunction, updateFlowsApi, setError }) => { + const theme = useTheme() + const customization = useSelector((state) => state.customization) return ( <> - + - - + + Name @@ -63,82 +74,150 @@ export const FlowListTable = ({ data, images, filterFunction, updateFlowsApi }) - {data.filter(filterFunction).map((row, index) => ( - - - - - - - -
-   - {row.category && - row.category - .split(';') - .map((tag, index) => ( - - ))} -
-
- - {images[row.id] && ( -
- {images[row.id].slice(0, images[row.id].length > 5 ? 5 : images[row.id].length).map((img) => ( -
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) : ( + <> + {data?.filter(filterFunction).map((row, index) => ( + + + + - -
- ))} - {images[row.id].length > 5 && ( - - + {images[row.id].length - 5} More + + {row.templateName || row.name} + + + + +
+   + {row.category && + row.category + .split(';') + .map((tag, index) => ( + + ))} +
+
+ + {images[row.id] && ( + + {images[row.id] + .slice(0, images[row.id].length > 5 ? 5 : images[row.id].length) + .map((img) => ( + + + + ))} + {images[row.id].length > 5 && ( + + + {images[row.id].length - 5} More + + )} + )} -
- )} -
- {moment(row.updatedDate).format('MMMM Do, YYYY')} - - - - - -
- ))} + + {moment(row.updatedDate).format('MMMM Do, YYYY')} + + + + + + + ))} + + )}
@@ -149,6 +228,8 @@ export const FlowListTable = ({ data, images, filterFunction, updateFlowsApi }) FlowListTable.propTypes = { data: PropTypes.array, images: PropTypes.object, + isLoading: PropTypes.bool, filterFunction: PropTypes.func, - updateFlowsApi: PropTypes.object + updateFlowsApi: PropTypes.object, + setError: PropTypes.func } diff --git a/packages/ui/src/ui-component/table/MarketplaceTable.jsx b/packages/ui/src/ui-component/table/MarketplaceTable.jsx index 3b66409b..82d16114 100644 --- a/packages/ui/src/ui-component/table/MarketplaceTable.jsx +++ b/packages/ui/src/ui-component/table/MarketplaceTable.jsx @@ -1,36 +1,54 @@ import PropTypes from 'prop-types' +import { useSelector } from 'react-redux' import { styled } from '@mui/material/styles' -import Table from '@mui/material/Table' -import TableBody from '@mui/material/TableBody' -import TableCell, { tableCellClasses } from '@mui/material/TableCell' -import TableContainer from '@mui/material/TableContainer' -import TableHead from '@mui/material/TableHead' -import TableRow from '@mui/material/TableRow' -import Paper from '@mui/material/Paper' -import Chip from '@mui/material/Chip' -import { Button, Typography } from '@mui/material' +import { tableCellClasses } from '@mui/material/TableCell' +import { + Button, + Chip, + Paper, + Skeleton, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Typography, + useTheme +} from '@mui/material' const StyledTableCell = styled(TableCell)(({ theme }) => ({ + borderColor: theme.palette.grey[900] + 25, + [`&.${tableCellClasses.head}`]: { - backgroundColor: theme.palette.common.black, - color: theme.palette.common.white + color: theme.palette.grey[900] }, [`&.${tableCellClasses.body}`]: { - fontSize: 14 + fontSize: 14, + height: 64 } })) -const StyledTableRow = styled(TableRow)(({ theme }) => ({ - '&:nth-of-type(odd)': { - backgroundColor: theme.palette.action.hover - }, +const StyledTableRow = styled(TableRow)(() => ({ // hide last border '&:last-child td, &:last-child th': { border: 0 } })) -export const MarketplaceTable = ({ data, filterFunction, filterByBadge, filterByType, filterByFramework, goToCanvas, goToTool }) => { +export const MarketplaceTable = ({ + data, + filterFunction, + filterByBadge, + filterByType, + filterByFramework, + goToCanvas, + goToTool, + isLoading +}) => { + const theme = useTheme() + const customization = useSelector((state) => state.customization) + const openTemplate = (selectedTemplate) => { if (selectedTemplate.flowData) { goToCanvas(selectedTemplate) @@ -41,10 +59,15 @@ export const MarketplaceTable = ({ data, filterFunction, filterByBadge, filterBy return ( <> - + - - + + Name @@ -63,71 +86,120 @@ export const MarketplaceTable = ({ data, filterFunction, filterByBadge, filterBy - {data - .filter(filterByBadge) - .filter(filterByType) - .filter(filterFunction) - .filter(filterByFramework) - .map((row, index) => ( - - - - - - - - {row.type} - - - - {row.description || ''} - - - -
- {row.categories && - row.categories - .split(',') - .map((tag, index) => ( - - ))} -
-
- - - {row.badge && - row.badge - .split(';') - .map((tag, index) => ( - - ))} - - + {isLoading ? ( + <> + + + + + + + + + + + + + + + + - ))} + + + + + + + + + + + + + + + + + + + ) : ( + <> + {data + ?.filter(filterByBadge) + .filter(filterByType) + .filter(filterFunction) + .filter(filterByFramework) + .map((row, index) => ( + + + + + + + + {row.type} + + + + {row.description || ''} + + + +
+ {row.categories && + row.categories + .split(',') + .map((tag, index) => ( + + ))} +
+
+ + + {row.badge && + row.badge + .split(';') + .map((tag, index) => ( + + ))} + + +
+ ))} + + )}
@@ -142,5 +214,6 @@ MarketplaceTable.propTypes = { filterByType: PropTypes.func, filterByFramework: PropTypes.func, goToTool: PropTypes.func, - goToCanvas: PropTypes.func + goToCanvas: PropTypes.func, + isLoading: PropTypes.bool } diff --git a/packages/ui/src/ui-component/tooltip/TooltipWithParser.jsx b/packages/ui/src/ui-component/tooltip/TooltipWithParser.jsx index 7362b55c..a84b6c13 100644 --- a/packages/ui/src/ui-component/tooltip/TooltipWithParser.jsx +++ b/packages/ui/src/ui-component/tooltip/TooltipWithParser.jsx @@ -4,15 +4,15 @@ import parser from 'html-react-parser' import PropTypes from 'prop-types' import { useSelector } from 'react-redux' -export const TooltipWithParser = ({ title, style }) => { +export const TooltipWithParser = ({ title, sx }) => { const customization = useSelector((state) => state.customization) return ( - + { TooltipWithParser.propTypes = { title: PropTypes.node, - style: PropTypes.any + sx: PropTypes.any } diff --git a/packages/ui/src/views/apikey/APIKeyDialog.jsx b/packages/ui/src/views/apikey/APIKeyDialog.jsx index c0326eb9..a59bb57f 100644 --- a/packages/ui/src/views/apikey/APIKeyDialog.jsx +++ b/packages/ui/src/views/apikey/APIKeyDialog.jsx @@ -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 diff --git a/packages/ui/src/views/apikey/index.jsx b/packages/ui/src/views/apikey/index.jsx index 9eeb0a8c..11d4e032 100644 --- a/packages/ui/src/views/apikey/index.jsx +++ b/packages/ui/src/views/apikey/index.jsx @@ -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 ( <> - {props.apiKey.keyName} - + + {props.apiKey.keyName} + + {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! - - + + {props.apiKey.chatFlows.length}{' '} {props.apiKey.chatFlows.length > 0 && ( setOpen(!open)}> {props.apiKey.chatFlows.length > 0 && open ? : } )} - - {props.apiKey.createdAt} - + + {moment(props.apiKey.createdAt).format('MMMM Do, YYYY')} + - - + + - + {open && ( - + - + - + - - Chatflow Name - + Chatflow Name Modified On - Category + Category {props.apiKey.chatFlows.map((flow, index) => ( - - {flow.flowName} - {moment(flow.updatedDate).format('DD-MMM-YY')} - + + {flow.flowName} + {moment(flow.updatedDate).format('MMMM Do, YYYY')} +   {flow.category && flow.category @@ -157,14 +154,14 @@ function APIKeyRow(props) { .map((tag, index) => ( ))} - - + + ))}
-
+
)} @@ -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 ( <> - - - - -

API Keys 

- - - - ) - }} - /> - - + {error ? ( + + ) : ( + + + } + id='btn_createApiKey' > - - } - id='btn_createApiKey' - > - Create Key - - - -
-
-
- {apiKeys.length <= 0 && ( - - - APIEmptySVG - -
No API Keys Yet
-
- )} - {apiKeys.length > 0 && ( - - - - - Key Name - API Key - Usage - Created - - - - - - {apiKeys.filter(filterKeys).map((key, index) => ( - { - 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 + + + {!isLoading && apiKeys.length <= 0 ? ( + + + APIEmptySVG - ))} - -
-
+ +
No API Keys Yet
+ + ) : ( + + + + + Key Name + API Key + Usage + Created + + + + + + {isLoading ? ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) : ( + <> + {apiKeys.filter(filterKeys).map((key, index) => ( + { + 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)} + /> + ))} + + )} + +
+
+ )} + )}
{ dialogProps={dialogProps} onCancel={() => setShowDialog(false)} onConfirm={onConfirm} + setError={setError} > diff --git a/packages/ui/src/views/assistants/AssistantDialog.jsx b/packages/ui/src/views/assistants/AssistantDialog.jsx index da2e54ad..27f83732 100644 --- a/packages/ui/src/views/assistants/AssistantDialog.jsx +++ b/packages/ui/src/views/assistants/AssistantDialog.jsx @@ -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' > - + {dialogProps.title} - - - - - Assistant Name - - - - setAssistantName(e.target.value)} - /> - - - - - Assistant Description - - - - setAssistantDesc(e.target.value)} - /> - - - - Assistant Icon Src - -
- {assistantName} + + + + Assistant Name + + + setAssistantName(e.target.value)} /> -
- setAssistantIcon(e.target.value)} - /> -
- - - - Assistant Model -  * - - - setAssistantModel(newValue)} - value={assistantModel ?? 'choose an option'} - /> - - - - - OpenAI Credential -  * - - - setAssistantCredential(newValue)} - /> - - - - - Assistant Instruction + + + + Assistant Description + + + setAssistantDesc(e.target.value)} + /> + + + + Assistant Icon Src + +
+ {assistantName} +
+ setAssistantIcon(e.target.value)} + /> +
+ + + + Assistant Model +  * + + + setAssistantModel(newValue)} + value={assistantModel ?? 'choose an option'} + /> + + + + + OpenAI Credential +  * + + + setAssistantCredential(newValue)} + /> + + + + Assistant Instruction - - - setAssistantInstructions(e.target.value)} - /> - - - - - Assistant Tools - - - - (newValue ? setAssistantTools(JSON.parse(newValue)) : setAssistantTools([]))} - value={assistantTools ?? 'choose an option'} - /> - - - - - Knowledge Files - - - -
- {assistantFiles.map((file, index) => ( -
- {file.filename} - onFileDeleteClick(file.id)}> - - -
- ))} -
- setUploadAssistantFiles(newValue)} - value={uploadAssistantFiles ?? 'Choose a file to upload'} - /> + + setAssistantInstructions(e.target.value)} + /> +
+ + + Assistant Tools + + + (newValue ? setAssistantTools(JSON.parse(newValue)) : setAssistantTools([]))} + value={assistantTools ?? 'choose an option'} + /> + + + + Knowledge Files + + +
+ {assistantFiles.map((file, index) => ( +
+ {file.filename} + onFileDeleteClick(file.id)}> + + +
+ ))} +
+ setUploadAssistantFiles(newValue)} + value={uploadAssistantFiles ?? 'Choose a file to upload'} + /> +
- + {dialogProps.type === 'EDIT' && ( onSyncClick()}> Sync diff --git a/packages/ui/src/views/assistants/LoadAssistantDialog.jsx b/packages/ui/src/views/assistants/LoadAssistantDialog.jsx index 921df8fe..57dea792 100644 --- a/packages/ui/src/views/assistants/LoadAssistantDialog.jsx +++ b/packages/ui/src/views/assistants/LoadAssistantDialog.jsx @@ -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 ? ( { - 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 ( <> - - - -

OpenAI Assistants

- - - - }> + } + > Add - -
-
- - {!getAllAssistantsApi.loading && - getAllAssistantsApi.data && - getAllAssistantsApi.data.map((data, index) => ( - - edit(data)} - /> - - ))} - - {!getAllAssistantsApi.loading && (!getAllAssistantsApi.data || getAllAssistantsApi.data.length === 0) && ( - - - ToolEmptySVG - -
No Assistants Added Yet
+ + {isLoading ? ( + + + + + + ) : ( + + {getAllAssistantsApi.data && + getAllAssistantsApi.data.map((data, index) => ( + edit(data)} + /> + ))} + + )} + {!isLoading && (!getAllAssistantsApi.data || getAllAssistantsApi.data.length === 0) && ( + + + ToolEmptySVG + +
No Assistants Added Yet
+
+ )}
)}
@@ -132,12 +161,14 @@ const Assistants = () => { dialogProps={loadDialogProps} onCancel={() => setShowLoadDialog(false)} onAssistantSelected={onAssistantSelected} + setError={setError} > setShowDialog(false)} onConfirm={onConfirm} + setError={setError} > ) diff --git a/packages/ui/src/views/canvas/CanvasHeader.jsx b/packages/ui/src/views/canvas/CanvasHeader.jsx index 2ec30a16..130f816e 100644 --- a/packages/ui/src/views/canvas/CanvasHeader.jsx +++ b/packages/ui/src/views/canvas/CanvasHeader.jsx @@ -192,186 +192,192 @@ const CanvasHeader = ({ chatflow, handleSaveFlow, handleDeleteFlow, handleLoadFl return ( <> - - - - window.history.state && window.history.state.idx > 0 ? navigate(-1) : navigate('/', { replace: true }) - } - > - - - - - - {!isEditingFlowName && ( - - - {canvas.isDirty && *} {flowName} - - {chatflow?.id && ( - - + + + + + window.history.state && window.history.state.idx > 0 ? navigate(-1) : navigate('/', { replace: true }) + } + > + + + + + + {!isEditingFlowName ? ( + + setEditingFlowName(true)} > - - - + {canvas.isDirty && *} {flowName} + + {chatflow?.id && ( + + setEditingFlowName(true)} + > + + + + )} + + ) : ( + + + + + + + + + setEditingFlowName(false)} + > + + + + )} - - )} - {isEditingFlowName && ( - - - + + + + {chatflow?.id && ( + - + - - setEditingFlowName(false)} - > - - - - - )} - - - {chatflow?.id && ( - + )} + - + - )} - - - - - - - setSettingsOpen(!isSettingsOpen)} - > - - - - + + setSettingsOpen(!isSettingsOpen)} + > + + + + + {inputParam.type === 'credential' && ( - <> -
-
- { - setCredentialId(newValue) - onSelect(newValue) - }} - onCreateNew={() => addAsyncOption(inputParam.name)} - /> - {credentialId && ( - editCredential(credentialId)}> - - - )} -
- +
+ { + setCredentialId(newValue) + onSelect(newValue) + }} + onCreateNew={() => addAsyncOption(inputParam.name)} + /> + {credentialId && ( + editCredential(credentialId)}> + + + )} +
)} )} diff --git a/packages/ui/src/views/chatflows/index.jsx b/packages/ui/src/views/chatflows/index.jsx index 7a7de2bf..2175c1d8 100644 --- a/packages/ui/src/views/chatflows/index.jsx +++ b/packages/ui/src/views/chatflows/index.jsx @@ -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 ( - - - - -

Chatflows

- - - - ) - }} + + {error ? ( + + ) : ( + + + + + + + + + + + } sx={{ borderRadius: 2, height: 40 }}> + Add New + + + {!view || view === 'card' ? ( + <> + {isLoading && !getAllChatflowsApi.data ? ( + + + + + + ) : ( + + {getAllChatflowsApi.data?.filter(filterFlows).map((data, index) => ( + goToCanvas(data)} data={data} images={images[data.id]} /> + ))} + + )} + + ) : ( + - - - - - - - - - - - - - - - }> - Add New - - - -
-
- {!isLoading && (!view || view === 'card') && getAllChatflowsApi.data && ( - - {getAllChatflowsApi.data.filter(filterFlows).map((data, index) => ( - - goToCanvas(data)} data={data} images={images[data.id]} /> - - ))} - - )} - {!isLoading && view === 'list' && getAllChatflowsApi.data && ( - - )} -
- - {!isLoading && (!getAllChatflowsApi.data || getAllChatflowsApi.data.length === 0) && ( - - - WorkflowEmptySVG - -
No Chatflows Yet
+ )} + {!isLoading && (!getAllChatflowsApi.data || getAllChatflowsApi.data.length === 0) && ( + + + WorkflowEmptySVG + +
No Chatflows Yet
+
+ )}
)} +
diff --git a/packages/ui/src/views/credentials/AddEditCredentialDialog.jsx b/packages/ui/src/views/credentials/AddEditCredentialDialog.jsx index d21ab95c..7d36826f 100644 --- a/packages/ui/src/views/credentials/AddEditCredentialDialog.jsx +++ b/packages/ui/src/views/credentials/AddEditCredentialDialog.jsx @@ -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 diff --git a/packages/ui/src/views/credentials/CredentialListDialog.jsx b/packages/ui/src/views/credentials/CredentialListDialog.jsx index 1f1ab817..b7001b7a 100644 --- a/packages/ui/src/views/credentials/CredentialListDialog.jsx +++ b/packages/ui/src/views/credentials/CredentialListDialog.jsx @@ -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 ? ( - + {dialogProps.title} - + + + filterSearch(e.target.value)} @@ -106,60 +104,59 @@ const CredentialListDialog = ({ show, dialogProps, onCancel, onCredentialSelecte }} /> - - {[...componentsCredentials].map((componentCredential) => ( -
- onCredentialSelected(componentCredential)} - sx={{ p: 0, borderRadius: `${customization.borderRadius}px` }} + 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 + }} + > +
- - -
- {componentCredential.name} -
-
- -
- -
+ {componentCredential.name} +
+ {componentCredential.label} + ))}
diff --git a/packages/ui/src/views/credentials/index.jsx b/packages/ui/src/views/credentials/index.jsx index 6e144101..dbe07960 100644 --- a/packages/ui/src/views/credentials/index.jsx +++ b/packages/ui/src/views/credentials/index.jsx @@ -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 ( <> - - - - + {error ? ( + + ) : ( + + -

Credentials 

- - - - ) - }} - /> - - } > - - } + Add Credential + +
+ {!isLoading && credentials.length <= 0 ? ( + + + CredentialEmptySVG + +
No Credentials Yet
+
+ ) : ( + + + - Add Credential - - - - - - - {credentials.length <= 0 && ( - - - CredentialEmptySVG - -
No Credentials Yet
+ + Name + Last Updated + Created + + + +
+ + {isLoading ? ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) : ( + <> + {credentials.filter(filterCredentials).map((credential, index) => ( + + + + + {credential.credentialName} + + {credential.name} + + + + {moment(credential.updatedDate).format('MMMM Do, YYYY')} + + + {moment(credential.createdDate).format('MMMM Do, YYYY')} + + + edit(credential)}> + + + + + deleteCredential(credential)} + > + + + + + ))} + + )} + +
+
+ )}
)} - {credentials.length > 0 && ( - - - - - Name - Last Updated - Created - - - - - - {credentials.filter(filterCredentials).map((credential, index) => ( - - -
-
- {credential.credentialName} -
- {credential.name} -
-
- {moment(credential.updatedDate).format('DD-MMM-YY')} - {moment(credential.createdDate).format('DD-MMM-YY')} - - edit(credential)}> - - - - - deleteCredential(credential)}> - - - -
- ))} -
-
-
- )}
{ dialogProps={specificCredentialDialogProps} onCancel={() => setShowSpecificCredentialDialog(false)} onConfirm={onConfirm} + setError={setError} > diff --git a/packages/ui/src/views/marketplaces/index.jsx b/packages/ui/src/views/marketplaces/index.jsx index 645c5fc0..4ddf603e 100644 --- a/packages/ui/src/views/marketplaces/index.jsx +++ b/packages/ui/src/views/marketplaces/index.jsx @@ -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 ( <> - - - -

Marketplace

- - - - ) - }} - /> - - - - - - + {error ? ( + + ) : ( + + + - - - + Tag + + + + - - - - - -
-
- {open && ( - - + Type + + + + + + Framework + + + + + } + onSearchChange={onSearchChange} + search={true} + searchPlaceholder='Search Name/Description/Node' + title='Marketplace' > - - - Tag - - - - - - Type - - - - - - Framework - - - - - - )} - - {!isLoading && (!view || view === 'card') && getAllTemplatesMarketplacesApi.data && ( - <> - - {getAllTemplatesMarketplacesApi.data - .filter(filterByBadge) - .filter(filterByType) - .filter(filterFlows) - .filter(filterByFramework) - .map((data, index) => ( - - {data.badge && ( - - {data.type === 'Chatflow' && ( - goToCanvas(data)} data={data} images={images[data.id]} /> - )} - {data.type === 'Tool' && goToTool(data)} />} - - )} - {!data.badge && data.type === 'Chatflow' && ( - goToCanvas(data)} data={data} images={images[data.id]} /> - )} - {!data.badge && data.type === 'Tool' && goToTool(data)} />} - - ))} - - - )} - {!isLoading && view === 'list' && getAllTemplatesMarketplacesApi.data && ( - - )} - - {!isLoading && (!getAllTemplatesMarketplacesApi.data || getAllTemplatesMarketplacesApi.data.length === 0) && ( - - - WorkflowEmptySVG + + + + {!view || view === 'card' ? ( + <> + {isLoading ? ( + + + + + + ) : ( + + {getAllTemplatesMarketplacesApi.data + ?.filter(filterByBadge) + .filter(filterByType) + .filter(filterFlows) + .filter(filterByFramework) + .map((data, index) => ( + + {data.badge && ( + + {data.type === 'Chatflow' && ( + goToCanvas(data)} + data={data} + images={images[data.id]} + /> + )} + {data.type === 'Tool' && ( + goToTool(data)} /> + )} + + )} + {!data.badge && data.type === 'Chatflow' && ( + goToCanvas(data)} data={data} images={images[data.id]} /> + )} + {!data.badge && data.type === 'Tool' && ( + goToTool(data)} /> + )} + + ))} + + )} + + ) : ( + - -
No Marketplace Yet
+ )} + + {!isLoading && (!getAllTemplatesMarketplacesApi.data || getAllTemplatesMarketplacesApi.data.length === 0) && ( + + + WorkflowEmptySVG + +
No Marketplace Yet
+
+ )}
)}
diff --git a/packages/ui/src/views/tools/ToolDialog.jsx b/packages/ui/src/views/tools/ToolDialog.jsx index 0a314f77..a94ad494 100644 --- a/packages/ui/src/views/tools/ToolDialog.jsx +++ b/packages/ui/src/views/tools/ToolDialog.jsx @@ -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' > - -
+ + {dialogProps.title} -
{dialogProps.type === 'EDIT' && ( - )} -
-
- - - - - Tool Name -  * - - - - setToolName(e.target.value)} - /> - - - - Tool description -  * + + + + + + + Tool Name +  * + + + + setToolName(e.target.value)} + /> + + + + + Tool description +  * + - - - setToolDesc(e.target.value)} - /> - - - - Tool Icon Src - - setToolIcon(e.target.value)} - /> - - - - - Output Schema - - - - - - - - - Javascript Function - - - - - {dialogProps.type !== 'TEMPLATE' && ( - - )} - setToolFunc(code)} - /> + + setToolDesc(e.target.value)} + /> + + + + Tool Icon Source + + setToolIcon(e.target.value)} + /> + + + + + Output Schema + + + {dialogProps.type !== 'TEMPLATE' && ( + + )} + + + + + + + Javascript Function + + + + + {dialogProps.type !== 'TEMPLATE' && ( + + )} + + + setToolFunc(code)} + /> + - + {dialogProps.type === 'EDIT' && ( 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 diff --git a/packages/ui/src/views/tools/index.jsx b/packages/ui/src/views/tools/index.jsx index c923fc46..f433a99e 100644 --- a/packages/ui/src/views/tools/index.jsx +++ b/packages/ui/src/views/tools/index.jsx @@ -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 ( <> - - -

Tools

- - - - - handleFileUpload(e)} /> - }> - Create - - - -
- - {!getAllToolsApi.loading && - getAllToolsApi.data && - getAllToolsApi.data.map((data, index) => ( - - edit(data)} /> - - ))} - - {!getAllToolsApi.loading && (!getAllToolsApi.data || getAllToolsApi.data.length === 0) && ( - - - ToolEmptySVG - -
No Tools Created Yet
+ + {error ? ( + + ) : ( + + + + + handleFileUpload(e)} + /> + + + } + sx={{ borderRadius: 2, height: 40 }} + > + Create + + + + {isLoading ? ( + + + + + + ) : ( + + {getAllToolsApi.data && + getAllToolsApi.data.map((data, index) => ( + edit(data)} /> + ))} + + )} + {!isLoading && (!getAllToolsApi.data || getAllToolsApi.data.length === 0) && ( + + + ToolEmptySVG + +
No Tools Created Yet
+
+ )}
)}
@@ -147,6 +181,7 @@ const Tools = () => { dialogProps={dialogProps} onCancel={() => setShowDialog(false)} onConfirm={onConfirm} + setError={setError} > ) diff --git a/packages/ui/src/views/variables/AddEditVariableDialog.jsx b/packages/ui/src/views/variables/AddEditVariableDialog.jsx index ba4f45b1..cf302497 100644 --- a/packages/ui/src/views/variables/AddEditVariableDialog.jsx +++ b/packages/ui/src/views/variables/AddEditVariableDialog.jsx @@ -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 diff --git a/packages/ui/src/views/variables/index.jsx b/packages/ui/src/views/variables/index.jsx index cc6aee78..280711e9 100644 --- a/packages/ui/src/views/variables/index.jsx +++ b/packages/ui/src/views/variables/index.jsx @@ -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 ( <> - - - - -

Variables 

- - - - ) - }} - /> - - - } + id='btn_createVariable' > - - } - id='btn_createVariable' + Add Variable + + + {!isLoading && variables.length === 0 ? ( + + + VariablesEmptySVG + +
No Variables Yet
+
+ ) : ( + + + - Add Variable - - - - - - - {variables.length === 0 && ( - - - VariablesEmptySVG - -
No Variables Yet
+ + Name + Value + Type + Last Updated + Created + + + +
+ + {isLoading ? ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) : ( + <> + {variables.filter(filterVariables).map((variable, index) => ( + + +
+
+ +
+ {variable.name} +
+
+ {variable.value} + + + + + {moment(variable.updatedDate).format('MMMM Do, YYYY')} + + + {moment(variable.createdDate).format('MMMM Do, YYYY')} + + + edit(variable)}> + + + + + deleteVariable(variable)} + > + + + +
+ ))} + + )} +
+
+
+ )}
)} - {variables.length > 0 && ( - - - - - Name - Value - Type - Last Updated - Created - - - - - - {variables.filter(filterVariables).map((variable, index) => ( - - -
-
- -
- {variable.name} -
-
- {variable.value} - - - - {moment(variable.updatedDate).format('DD-MMM-YY')} - {moment(variable.createdDate).format('DD-MMM-YY')} - - edit(variable)}> - - - - - deleteVariable(variable)}> - - - -
- ))} -
-
-
- )}
setShowVariableDialog(false)} onConfirm={onConfirm} + setError={setError} > setShowHowToDialog(false)}>