New Feature Pagination (#4704)

* common pagination component

* Pagination for Doc Store Dashboard

* Pagination for Executions Dashboard

* Pagination Support for Tables

* lint fixes

* update view message dialog UI

* initial loading was ignoring the pagination counts

* 1) default page size change
2) ensure page limits are passed on load
3) co-pilot review comments (n+1 query)
4)

* 1) default page size change
2) ensure page limits are passed on load
3) co-pilot review comments (n+1 query)
4) refresh lists after insert/delete.

* Enhancement: Improve handling of empty responses in DocumentStore and API key services

- Added check for empty entities in DocumentStoreDTO.fromEntities to return an empty array.
- Updated condition in getAllDocumentStores to handle total count correctly, allowing for zero total.
- Refined logic in getAllApiKeys to check for empty keys and ensure correct API key retrieval.
- Adjusted UI components to safely handle potential undefined apiKeys array.

* Refresh API key list on pagination change

* Enhancement: Update pagination and filter handling across components
- Increased default items per page in AgentExecutions from 10 to 12.
- Improved JSON parsing for chat type and feedback type filters in ViewMessagesDialog.
- Enhanced execution filtering logic in AgentExecutions to ensure proper pagination and state management.
- Refactored filter section in AgentExecutions for better readability and functionality.
- Updated refresh logic in Agentflows to use the correct agentflow version.

* add workspaceId to removeAllChatMessages

* Refactor chat message retrieval logic for improved efficiency and maintainability

- Introduced a new `handleFeedbackQuery` function to streamline feedback-related queries.
- Enhanced pagination handling for session-based queries in `getMessagesWithFeedback`.
- Updated `ViewMessagesDialog` to sort messages in descending order by default.
- Simplified image rendering logic in `DocumentStoreTable` for better readability.

* - Update  `validateChatflowAPIKey` and `validateAPIKey` functions to get the correct keys array
- Enhanced error handling in the `sanitizeExecution` function to ensure safe access to nested properties

* Refactor API key validation logic for improved accuracy and error handling

- Consolidated API key validation in `validateAPIKey` to return detailed validation results.
- Updated `validateFlowAPIKey` to streamline flow API key validation.
- Introduced `getApiKeyById` function in the API key service for better key retrieval.
- Removed unused function `getAllChatSessionsFromChatflow` from the chat message API.

---------

Co-authored-by: Henry <hzj94@hotmail.com>
This commit is contained in:
Vinod Kiran
2025-07-10 20:29:24 +05:30
committed by GitHub
parent 6baec93860
commit bf05f25f7e
55 changed files with 2595 additions and 1560 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
import client from './client'
const getAllAPIKeys = () => client.get('/apikey')
const getAllAPIKeys = (params) => client.get('/apikey', { params })
const createNewAPI = (body) => client.post(`/apikey`, body)
+2 -2
View File
@@ -1,8 +1,8 @@
import client from './client'
const getAllChatflows = () => client.get('/chatflows?type=CHATFLOW')
const getAllChatflows = (params) => client.get('/chatflows?type=CHATFLOW', { params })
const getAllAgentflows = (type) => client.get(`/chatflows?type=${type}`)
const getAllAgentflows = (type, params) => client.get(`/chatflows?type=${type}`, { params })
const getSpecificChatflow = (id) => client.get(`/chatflows/${id}`)
+2 -2
View File
@@ -1,9 +1,9 @@
import client from './client'
const getAllDatasets = () => client.get('/datasets')
const getAllDatasets = (params) => client.get('/datasets', { params })
//dataset
const getDataset = (id) => client.get(`/datasets/set/${id}`)
const getDataset = (id, params) => client.get(`/datasets/set/${id}`, { params })
const createDataset = (body) => client.post(`/datasets/set`, body)
const updateDataset = (id, body) => client.put(`/datasets/set/${id}`, body)
const deleteDataset = (id) => client.delete(`/datasets/set/${id}`)
+1 -1
View File
@@ -1,6 +1,6 @@
import client from './client'
const getAllDocumentStores = () => client.get('/document-store/store')
const getAllDocumentStores = (params) => client.get('/document-store/store', { params })
const getDocumentLoaders = () => client.get('/document-store/components/loaders')
const getSpecificDocumentStore = (id) => client.get(`/document-store/store/${id}`)
const createDocumentStore = (body) => client.post(`/document-store/store`, body)
+1 -1
View File
@@ -1,7 +1,7 @@
import client from './client'
//evaluation
const getAllEvaluations = () => client.get('/evaluations')
const getAllEvaluations = (params) => client.get('/evaluations', { params })
const getIsOutdated = (id) => client.get(`/evaluations/is-outdated/${id}`)
const getEvaluation = (id) => client.get(`/evaluations/${id}`)
const createEvaluation = (body) => client.post(`/evaluations`, body)
+1 -1
View File
@@ -1,6 +1,6 @@
import client from './client'
const getAllEvaluators = () => client.get('/evaluators')
const getAllEvaluators = (params) => client.get('/evaluators', { params })
//evaluators
const createEvaluator = (body) => client.post(`/evaluators`, body)
+1 -1
View File
@@ -1,6 +1,6 @@
import client from './client'
const getAllTools = () => client.get('/tools')
const getAllTools = (params) => client.get('/tools', { params })
const getSpecificTool = (id) => client.get(`/tools/${id}`)
+1 -1
View File
@@ -1,6 +1,6 @@
import client from './client'
const getAllVariables = () => client.get('/variables')
const getAllVariables = (params) => client.get('/variables', { params })
const createVariable = (body) => client.post(`/variables`, body)
@@ -8,8 +8,14 @@ import Typography from '@mui/material/Typography'
const StatsCard = ({ title, stat }) => {
const customization = useSelector((state) => state.customization)
return (
<Card sx={{ border: '1px solid #e0e0e0', borderRadius: `${customization.borderRadius}px` }}>
<CardContent>
<Card
sx={{
border: customization.isDarkMode ? 'none' : '1px solid #e0e0e0',
boxShadow: customization.isDarkMode ? '0px 3px 8px rgba(255, 255, 255, 0.5)' : 'none',
borderRadius: `${customization.borderRadius}px`
}}
>
<CardContent sx={{ padding: '12px', '&:last-child': { paddingBottom: '12px', paddingLeft: '18px', paddingRight: '8px' } }}>
<Typography sx={{ fontSize: '0.875rem' }} color='text.primary' gutterBottom>
{title}
</Typography>
@@ -24,9 +24,14 @@ import {
CardContent,
FormControlLabel,
Checkbox,
DialogActions
DialogActions,
Pagination,
Typography,
Menu,
MenuItem,
IconButton
} from '@mui/material'
import { useTheme } from '@mui/material/styles'
import { useTheme, styled, alpha } from '@mui/material/styles'
import DatePicker from 'react-datepicker'
import robotPNG from '@/assets/images/robot.png'
@@ -34,7 +39,8 @@ import userPNG from '@/assets/images/account.png'
import msgEmptySVG from '@/assets/images/message_empty.svg'
import multiagent_supervisorPNG from '@/assets/images/multiagent_supervisor.png'
import multiagent_workerPNG from '@/assets/images/multiagent_worker.png'
import { IconTool, IconDeviceSdCard, IconFileExport, IconEraser, IconX, IconDownload, IconPaperclip } from '@tabler/icons-react'
import { IconTool, IconDeviceSdCard, IconFileExport, IconEraser, IconX, IconDownload, IconPaperclip, IconBulb } from '@tabler/icons-react'
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'
// Project import
import { MemoizedReactMarkdown } from '@/ui-component/markdown/MemoizedReactMarkdown'
@@ -63,6 +69,42 @@ import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackba
import '@/views/chatmessage/ChatMessage.css'
import 'react-datepicker/dist/react-datepicker.css'
const StyledMenu = styled((props) => (
<Menu
elevation={0}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right'
}}
transformOrigin={{
vertical: 'top',
horizontal: 'right'
}}
{...props}
/>
))(({ theme }) => ({
'& .MuiPaper-root': {
borderRadius: 6,
marginTop: theme.spacing(1),
minWidth: 180,
boxShadow:
'rgb(255, 255, 255) 0px 0px 0px 0px, rgba(0, 0, 0, 0.05) 0px 0px 0px 1px, rgba(0, 0, 0, 0.1) 0px 10px 15px -3px, rgba(0, 0, 0, 0.05) 0px 4px 6px -2px',
'& .MuiMenu-list': {
padding: '4px 0'
},
'& .MuiMenuItem-root': {
'& .MuiSvgIcon-root': {
fontSize: 18,
color: theme.palette.text.secondary,
marginRight: theme.spacing(1.5)
},
'&:active': {
backgroundColor: alpha(theme.palette.primary.main, theme.palette.action.selectedOpacity)
}
}
}
}))
const DatePickerCustomInput = forwardRef(function DatePickerCustomInput({ value, onClick }, ref) {
return (
<ListItemButton style={{ borderRadius: 15, border: '1px solid #e0e0e0' }} onClick={onClick} ref={ref}>
@@ -104,10 +146,12 @@ const ConfirmDeleteMessageDialog = ({ show, dialogProps, onCancel, onConfirm })
</DialogTitle>
<DialogContent>
<span style={{ marginTop: '20px', marginBottom: '20px' }}>{dialogProps.description}</span>
<FormControlLabel
control={<Checkbox checked={hardDelete} onChange={(event) => setHardDelete(event.target.checked)} />}
label='Remove messages from 3rd party Memory Node'
/>
{dialogProps.isChatflow && (
<FormControlLabel
control={<Checkbox checked={hardDelete} onChange={(event) => setHardDelete(event.target.checked)} />}
label='Remove messages from 3rd party Memory Node'
/>
)}
</DialogContent>
<DialogActions>
<Button onClick={onCancel}>{dialogProps.cancelButtonName}</Button>
@@ -142,7 +186,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
const [chatlogs, setChatLogs] = useState([])
const [allChatlogs, setAllChatLogs] = useState([])
const [chatMessages, setChatMessages] = useState([])
const [stats, setStats] = useState([])
const [stats, setStats] = useState({})
const [selectedMessageIndex, setSelectedMessageIndex] = useState(0)
const [selectedChatId, setSelectedChatId] = useState('')
const [sourceDialogOpen, setSourceDialogOpen] = useState(false)
@@ -154,6 +198,8 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
const [startDate, setStartDate] = useState(new Date(new Date().setMonth(new Date().getMonth() - 1)))
const [endDate, setEndDate] = useState(new Date())
const [leadEmail, setLeadEmail] = useState('')
const [anchorEl, setAnchorEl] = useState(null)
const open = Boolean(anchorEl)
const getChatmessageApi = useApi(chatmessageApi.getAllChatmessageFromChatflow)
const getChatmessageFromPKApi = useApi(chatmessageApi.getChatmessageFromPK)
@@ -161,74 +207,70 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
const getStoragePathFromServer = useApi(chatmessageApi.getStoragePath)
let storagePath = ''
/* Table Pagination */
const [currentPage, setCurrentPage] = useState(1)
const [pageLimit, setPageLimit] = useState(10)
const [total, setTotal] = useState(0)
const onChange = (event, page) => {
setCurrentPage(page)
refresh(page, pageLimit, startDate, endDate, chatTypeFilter, feedbackTypeFilter)
}
const refresh = (page, limit, startDate, endDate, chatTypes, feedbackTypes) => {
getChatmessageApi.request(dialogProps.chatflow.id, {
chatType: chatTypes.length ? chatTypes : undefined,
feedbackType: feedbackTypes.length ? feedbackTypes : undefined,
startDate: startDate,
endDate: endDate,
order: 'DESC',
page: page,
limit: limit
})
getStatsApi.request(dialogProps.chatflow.id, {
chatType: chatTypes.length ? chatTypes : undefined,
feedbackType: feedbackTypes.length ? feedbackTypes : undefined,
startDate: startDate,
endDate: endDate
})
setCurrentPage(page)
}
const onStartDateSelected = (date) => {
const updatedDate = new Date(date)
updatedDate.setHours(0, 0, 0, 0)
setStartDate(updatedDate)
getChatmessageApi.request(dialogProps.chatflow.id, {
startDate: updatedDate,
endDate: endDate,
chatType: chatTypeFilter.length ? chatTypeFilter : undefined,
feedbackType: feedbackTypeFilter.length ? feedbackTypeFilter : undefined
})
getStatsApi.request(dialogProps.chatflow.id, {
startDate: updatedDate,
endDate: endDate,
chatType: chatTypeFilter.length ? chatTypeFilter : undefined,
feedbackType: feedbackTypeFilter.length ? feedbackTypeFilter : undefined
})
refresh(1, pageLimit, updatedDate, endDate, chatTypeFilter, feedbackTypeFilter)
}
const onEndDateSelected = (date) => {
const updatedDate = new Date(date)
updatedDate.setHours(23, 59, 59, 999)
setEndDate(updatedDate)
getChatmessageApi.request(dialogProps.chatflow.id, {
endDate: updatedDate,
startDate: startDate,
chatType: chatTypeFilter.length ? chatTypeFilter : undefined,
feedbackType: feedbackTypeFilter.length ? feedbackTypeFilter : undefined
})
getStatsApi.request(dialogProps.chatflow.id, {
endDate: updatedDate,
startDate: startDate,
chatType: chatTypeFilter.length ? chatTypeFilter : undefined,
feedbackType: feedbackTypeFilter.length ? feedbackTypeFilter : undefined
})
refresh(1, pageLimit, startDate, updatedDate, chatTypeFilter, feedbackTypeFilter)
}
const onChatTypeSelected = (chatTypes) => {
setChatTypeFilter(chatTypes)
getChatmessageApi.request(dialogProps.chatflow.id, {
chatType: chatTypes.length ? chatTypes : undefined,
startDate: startDate,
endDate: endDate,
feedbackType: feedbackTypeFilter.length ? feedbackTypeFilter : undefined
})
getStatsApi.request(dialogProps.chatflow.id, {
chatType: chatTypes.length ? chatTypes : undefined,
startDate: startDate,
endDate: endDate,
feedbackType: feedbackTypeFilter.length ? feedbackTypeFilter : undefined
})
// Parse the JSON string from MultiDropdown back to an array
let parsedChatTypes = []
if (chatTypes && typeof chatTypes === 'string' && chatTypes.startsWith('[') && chatTypes.endsWith(']')) {
parsedChatTypes = JSON.parse(chatTypes)
} else if (Array.isArray(chatTypes)) {
parsedChatTypes = chatTypes
}
setChatTypeFilter(parsedChatTypes)
refresh(1, pageLimit, startDate, endDate, parsedChatTypes, feedbackTypeFilter)
}
const onFeedbackTypeSelected = (feedbackTypes) => {
setFeedbackTypeFilter(feedbackTypes)
getChatmessageApi.request(dialogProps.chatflow.id, {
chatType: chatTypeFilter.length ? chatTypeFilter : undefined,
feedbackType: feedbackTypes.length ? feedbackTypes : undefined,
startDate: startDate,
endDate: endDate,
order: 'ASC'
})
getStatsApi.request(dialogProps.chatflow.id, {
chatType: chatTypeFilter.length ? chatTypeFilter : undefined,
feedbackType: feedbackTypes.length ? feedbackTypes : undefined,
startDate: startDate,
endDate: endDate
})
// Parse the JSON string from MultiDropdown back to an array
let parsedFeedbackTypes = []
if (feedbackTypes && typeof feedbackTypes === 'string' && feedbackTypes.startsWith('[') && feedbackTypes.endsWith(']')) {
parsedFeedbackTypes = JSON.parse(feedbackTypes)
} else if (Array.isArray(feedbackTypes)) {
parsedFeedbackTypes = feedbackTypes
}
setFeedbackTypeFilter(parsedFeedbackTypes)
refresh(1, pageLimit, startDate, endDate, chatTypeFilter, parsedFeedbackTypes)
}
const onDeleteMessages = () => {
@@ -236,7 +278,8 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
title: 'Delete Messages',
description: 'Are you sure you want to delete messages? This action cannot be undone.',
confirmButtonName: 'Delete',
cancelButtonName: 'Cancel'
cancelButtonName: 'Cancel',
isChatflow: dialogProps.isChatflow
})
setHardDeleteDialogOpen(true)
}
@@ -280,18 +323,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
)
}
})
getChatmessageApi.request(chatflowid, {
chatType: chatTypeFilter.length ? chatTypeFilter : undefined,
startDate: startDate,
endDate: endDate,
feedbackType: feedbackTypeFilter.length ? feedbackTypeFilter : undefined
})
getStatsApi.request(chatflowid, {
chatType: chatTypeFilter.length ? chatTypeFilter : undefined,
startDate: startDate,
endDate: endDate,
feedbackType: feedbackTypeFilter.length ? feedbackTypeFilter : undefined
})
refresh(1, pageLimit, startDate, endDate, chatTypeFilter, feedbackTypeFilter)
} catch (error) {
console.error(error)
enqueueSnackbar({
@@ -555,20 +587,42 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
item: allChatMessages[i]
}
} else if (Object.prototype.hasOwnProperty.call(seen, PK) && seen[PK].counter === 1) {
// Properly identify user and API messages regardless of order
const firstMessage = seen[PK].item
const secondMessage = item
let userContent = ''
let apiContent = ''
// Check both messages and assign based on role, not order
if (firstMessage.role === 'userMessage') {
userContent = `User: ${firstMessage.content}`
} else if (firstMessage.role === 'apiMessage') {
apiContent = `Bot: ${firstMessage.content}`
}
if (secondMessage.role === 'userMessage') {
userContent = `User: ${secondMessage.content}`
} else if (secondMessage.role === 'apiMessage') {
apiContent = `Bot: ${secondMessage.content}`
}
seen[PK] = {
counter: 2,
item: {
...seen[PK].item,
apiContent:
seen[PK].item.role === 'apiMessage' ? `Bot: ${seen[PK].item.content}` : `User: ${seen[PK].item.content}`,
userContent: item.role === 'apiMessage' ? `Bot: ${item.content}` : `User: ${item.content}`
apiContent,
userContent
}
}
filteredChatLogs.push(seen[PK].item)
}
}
setChatLogs(filteredChatLogs)
if (filteredChatLogs.length) return getChatPK(filteredChatLogs[0])
// Sort by date to maintain chronological order
const sortedChatLogs = filteredChatLogs.sort((a, b) => new Date(b.createdDate) - new Date(a.createdDate))
setChatLogs(sortedChatLogs)
if (sortedChatLogs.length) return getChatPK(sortedChatLogs[0])
return undefined
}
@@ -613,6 +667,14 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
setSourceDialogOpen(true)
}
const handleClick = (event) => {
setAnchorEl(event.currentTarget)
}
const handleClose = () => {
setAnchorEl(null)
}
const renderFileUploads = (item, index) => {
if (item?.mime?.startsWith('image/')) {
return (
@@ -706,15 +768,13 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
useEffect(() => {
if (getStatsApi.data) {
setStats(getStatsApi.data)
setTotal(getStatsApi.data?.totalSessions ?? 0)
}
}, [getStatsApi.data])
useEffect(() => {
if (dialogProps.chatflow) {
getChatmessageApi.request(dialogProps.chatflow.id, {
startDate: startDate,
endDate: endDate
})
refresh(currentPage, pageLimit, startDate, endDate, chatTypeFilter, feedbackTypeFilter)
getStatsApi.request(dialogProps.chatflow.id, {
startDate: startDate,
endDate: endDate
@@ -733,6 +793,9 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
setEndDate(new Date())
setStats([])
setLeadEmail('')
setTotal(0)
setCurrentPage(1)
setPageLimit(10)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -748,16 +811,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
if (dialogProps.chatflow) {
// when the filter is cleared fetch all messages
if (feedbackTypeFilter.length === 0) {
getChatmessageApi.request(dialogProps.chatflow.id, {
startDate: startDate,
endDate: endDate,
chatType: chatTypeFilter.length ? chatTypeFilter : undefined
})
getStatsApi.request(dialogProps.chatflow.id, {
startDate: startDate,
endDate: endDate,
chatType: chatTypeFilter.length ? chatTypeFilter : undefined
})
refresh(currentPage, pageLimit, startDate, endDate, chatTypeFilter, feedbackTypeFilter)
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -819,19 +873,10 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
onClose={onCancel}
open={show}
fullWidth
maxWidth={'lg'}
maxWidth={'xl'}
aria-labelledby='alert-dialog-title'
aria-describedby='alert-dialog-description'
>
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
<div style={{ display: 'flex', flexDirection: 'row' }}>
{dialogProps.title}
<div style={{ flex: 1 }} />
<Button variant='outlined' onClick={() => exportMessages()} startIcon={<IconFileExport />}>
Export
</Button>
</div>
</DialogTitle>
<DialogContent>
<>
<div
@@ -912,7 +957,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
<b style={{ marginRight: 10 }}>Feedback</b>
<MultiDropdown
key={JSON.stringify(feedbackTypeFilter)}
name='chatType'
name='feedbackType'
options={[
{
label: 'Positive',
@@ -929,31 +974,81 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
/>
</div>
<div style={{ flex: 1 }}></div>
{stats.totalMessages > 0 && (
<Button color='error' variant='outlined' onClick={() => onDeleteMessages()} startIcon={<IconEraser />}>
Delete Messages
</Button>
)}
<Button
id='messages-dialog-action-button'
aria-controls={open ? 'messages-dialog-action-menu' : undefined}
aria-haspopup='true'
aria-expanded={open ? 'true' : undefined}
variant={customization.isDarkMode ? 'contained' : 'outlined'}
disableElevation
color='secondary'
onClick={handleClick}
sx={{
minWidth: 150,
'&:hover': {
backgroundColor: customization.isDarkMode ? alpha(theme.palette.secondary.main, 0.8) : undefined
}
}}
endIcon={
<KeyboardArrowDownIcon style={{ backgroundColor: customization.isDarkMode ? 'transparent' : 'inherit' }} />
}
>
More Actions
</Button>
<StyledMenu
id='messages-dialog-action-menu'
MenuListProps={{
'aria-labelledby': 'messages-dialog-action-button'
}}
anchorEl={anchorEl}
open={open}
onClose={handleClose}
>
<MenuItem
onClick={() => {
handleClose()
exportMessages()
}}
disableRipple
>
<IconFileExport style={{ marginRight: 8 }} />
Export to JSON
</MenuItem>
{(stats.totalMessages ?? 0) > 0 && (
<MenuItem
onClick={() => {
handleClose()
onDeleteMessages()
}}
disableRipple
>
<IconEraser style={{ marginRight: 8 }} />
Delete All
</MenuItem>
)}
</StyledMenu>
</div>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, minmax(0, 1fr))',
gridTemplateColumns: 'repeat(4, minmax(0, 1fr))',
gap: 10,
marginBottom: 16,
marginBottom: 25,
marginLeft: 8,
marginRight: 8
marginRight: 8,
marginTop: 20
}}
>
<StatsCard title='Total Messages' stat={`${stats.totalMessages}`} />
<StatsCard title='Total Feedback Received' stat={`${stats.totalFeedback}`} />
<StatsCard title='Total Sessions' stat={`${stats.totalSessions ?? 0}`} />
<StatsCard title='Total Messages' stat={`${stats.totalMessages ?? 0}`} />
<StatsCard title='Total Feedback Received' stat={`${stats.totalFeedback ?? 0}`} />
<StatsCard
title='Positive Feedback'
stat={`${((stats.positiveFeedback / stats.totalFeedback) * 100 || 0).toFixed(2)}%`}
stat={`${(((stats.positiveFeedback ?? 0) / (stats.totalFeedback ?? 1)) * 100 || 0).toFixed(2)}%`}
/>
</div>
<div style={{ display: 'flex', flexDirection: 'row' }}>
{chatlogs && chatlogs.length == 0 && (
<div style={{ display: 'flex', flexDirection: 'row', overflow: 'hidden', minWidth: 0 }}>
{chatlogs && chatlogs.length === 0 && (
<Stack sx={{ alignItems: 'center', justifyContent: 'center', width: '100%' }} flexDirection='column'>
<Box sx={{ p: 5, height: 'auto' }}>
<img
@@ -966,7 +1061,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
</Stack>
)}
{chatlogs && chatlogs.length > 0 && (
<div style={{ flexBasis: '40%' }}>
<div style={{ flexBasis: '40%', minWidth: 0, overflow: 'hidden' }}>
<Box
sx={{
overflowY: 'auto',
@@ -976,6 +1071,28 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
maxHeight: 'calc(100vh - 260px)'
}}
>
<div
style={{
display: 'flex',
marginLeft: '15px',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 10
}}
>
<Typography variant='h5'>
Sessions {pageLimit * (currentPage - 1) + 1} - {Math.min(pageLimit * currentPage, total)} of{' '}
{total}
</Typography>
<Pagination
style={{ justifyItems: 'right', justifyContent: 'center' }}
count={Math.ceil(total / pageLimit)}
onChange={onChange}
page={currentPage}
color='primary'
/>
</div>
{chatlogs.map((chatmsg, index) => (
<ListItemButton
key={index}
@@ -1018,9 +1135,9 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
</div>
)}
{chatlogs && chatlogs.length > 0 && (
<div style={{ flexBasis: '60%', paddingRight: '30px' }}>
<div style={{ flexBasis: '60%', paddingRight: '30px', minWidth: 0, overflow: 'hidden' }}>
{chatMessages && chatMessages.length > 1 && (
<div style={{ display: 'flex', flexDirection: 'row' }}>
<div style={{ marginBottom: 10, display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<div style={{ flex: 1, marginLeft: '20px', marginBottom: '15px', marginTop: '10px' }}>
{chatMessages[1].sessionId && (
<div>
@@ -1046,31 +1163,26 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
<div
style={{
display: 'flex',
flexDirection: 'column',
flexDirection: 'row',
alignContent: 'center',
alignItems: 'end'
}}
>
<StyledButton
sx={{ height: 'max-content', width: 'max-content' }}
variant='outlined'
color='error'
title='Clear Message'
onClick={() => clearChat(chatMessages[1])}
startIcon={<IconEraser />}
>
Clear
</StyledButton>
<Tooltip title='Clear Message'>
<IconButton color='error' onClick={() => clearChat(chatMessages[1])}>
<IconEraser />
</IconButton>
</Tooltip>
{chatMessages[1].sessionId && (
<Tooltip
title={
'At your left 👈 you will see the Memory node that was used in this conversation. You need to have the matching Memory node with same parameters in the canvas, in order to delete the session conversations stored on the Memory node'
'On the left 👈, youll see the Memory node used in this conversation. To delete the session conversations stored on that Memory node, you must have a matching Memory node with identical parameters in the canvas.'
}
placement='bottom'
>
<h5 style={{ cursor: 'pointer', color: theme.palette.primary.main }}>
Why my session is not deleted?
</h5>
<IconButton color='primary'>
<IconBulb />
</IconButton>
</Tooltip>
)}
</div>
@@ -1081,12 +1193,15 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
display: 'flex',
flexDirection: 'column',
marginLeft: '20px',
border: '1px solid #e0e0e0',
borderRadius: `${customization.borderRadius}px`
marginBottom: '5px',
border: customization.isDarkMode ? 'none' : '1px solid #e0e0e0',
boxShadow: customization.isDarkMode ? '0 0 5px 0 rgba(255, 255, 255, 0.5)' : 'none',
borderRadius: `10px`,
overflow: 'hidden'
}}
className='cloud-message'
>
<div style={{ width: '100%', height: '100%' }}>
<div style={{ width: '100%', height: '100%', overflowY: 'auto' }}>
{chatMessages &&
chatMessages.map((message, index) => {
if (message.type === 'apiMessage' || message.type === 'userMessage') {
@@ -1125,7 +1240,9 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
style={{
display: 'flex',
flexDirection: 'column',
width: '100%'
width: '100%',
minWidth: 0,
overflow: 'hidden'
}}
>
{message.fileUploads && message.fileUploads.length > 0 && (
@@ -1412,7 +1529,10 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
})}
</div>
)}
<div className='markdownanswer'>
<div
className='markdownanswer'
style={{ wordBreak: 'break-word', overflowWrap: 'break-word' }}
>
<MemoizedReactMarkdown chatflowid={dialogProps.chatflow.id}>
{message.message}
</MemoizedReactMarkdown>
@@ -1486,7 +1606,9 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
return (
<Box
sx={{
background: theme.palette.timeMessage.main,
background: customization.isDarkMode
? theme.palette.divider
: theme.palette.timeMessage.main,
p: 2
}}
key={index}
@@ -0,0 +1,85 @@
import { Box, FormControl, MenuItem, Pagination, Select, Typography } from '@mui/material'
import { useEffect, useState } from 'react'
import { useTheme } from '@mui/material/styles'
import { useSelector } from 'react-redux'
import PropTypes from 'prop-types'
export const DEFAULT_ITEMS_PER_PAGE = 12
const TablePagination = ({ currentPage, limit, total, onChange }) => {
const theme = useTheme()
const customization = useSelector((state) => state.customization)
const borderColor = theme.palette.grey[900] + 25
const [itemsPerPage, setItemsPerPage] = useState(DEFAULT_ITEMS_PER_PAGE)
const [activePage, setActivePage] = useState(1)
const [totalItems, setTotalItems] = useState(0)
useEffect(() => {
setTotalItems(total)
}, [total])
useEffect(() => {
setItemsPerPage(limit)
}, [limit])
useEffect(() => {
setActivePage(currentPage)
}, [currentPage])
const handlePageChange = (event, value) => {
setActivePage(value)
onChange(value, itemsPerPage)
}
const handleLimitChange = (event) => {
const itemsPerPage = parseInt(event.target.value, 10)
setItemsPerPage(itemsPerPage)
setActivePage(1)
onChange(1, itemsPerPage)
}
return (
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mt: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Typography variant='body2'>Items per page:</Typography>
<FormControl
variant='outlined'
size='small'
sx={{
minWidth: 80,
'& .MuiOutlinedInput-notchedOutline': {
borderColor: borderColor
},
'& .MuiSvgIcon-root': {
color: customization.isDarkMode ? '#fff' : 'inherit'
}
}}
>
<Select value={itemsPerPage} onChange={handleLimitChange} displayEmpty>
<MenuItem value={12}>12</MenuItem>
<MenuItem value={24}>24</MenuItem>
<MenuItem value={48}>48</MenuItem>
<MenuItem value={100}>100</MenuItem>
</Select>
</FormControl>
</Box>
{totalItems > 0 && (
<Typography variant='body2'>
Items {activePage * itemsPerPage - itemsPerPage + 1} to{' '}
{activePage * itemsPerPage > totalItems ? totalItems : activePage * itemsPerPage} of {totalItems}
</Typography>
)}
<Pagination count={Math.ceil(totalItems / itemsPerPage)} onChange={handlePageChange} page={activePage} color='primary' />
</Box>
)
}
TablePagination.propTypes = {
onChange: PropTypes.func.isRequired,
currentPage: PropTypes.number,
limit: PropTypes.number,
total: PropTypes.number
}
export default TablePagination
@@ -0,0 +1,255 @@
import { useState } from 'react'
import PropTypes from 'prop-types'
import { useSelector } from 'react-redux'
import { styled } from '@mui/material/styles'
import {
Box,
Paper,
Skeleton,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
TableSortLabel,
useTheme,
Typography
} from '@mui/material'
import { tableCellClasses } from '@mui/material/TableCell'
import DocumentStoreStatus from '@/views/docstore/DocumentStoreStatus'
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
}
}))
export const DocumentStoreTable = ({ data, isLoading, onRowClick, images }) => {
const theme = useTheme()
const customization = useSelector((state) => state.customization)
const localStorageKeyOrder = 'doc_store_order'
const localStorageKeyOrderBy = 'doc_store_orderBy'
const [order, setOrder] = useState(localStorage.getItem(localStorageKeyOrder) || 'desc')
const [orderBy, setOrderBy] = useState(localStorage.getItem(localStorageKeyOrderBy) || 'name')
const handleRequestSort = (property) => {
const isAsc = orderBy === property && order === 'asc'
const newOrder = isAsc ? 'desc' : 'asc'
setOrder(newOrder)
setOrderBy(property)
localStorage.setItem(localStorageKeyOrder, newOrder)
localStorage.setItem(localStorageKeyOrderBy, property)
}
const sortedData = data
? [...data].sort((a, b) => {
if (orderBy === 'name') {
return order === 'asc' ? (a.name || '').localeCompare(b.name || '') : (b.name || '').localeCompare(a.name || '')
}
return 0
})
: []
return (
<>
<TableContainer sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2 }} component={Paper}>
<Table sx={{ minWidth: 650 }} size='small' aria-label='document_store_table'>
<TableHead
sx={{
backgroundColor: customization.isDarkMode ? theme.palette.common.black : theme.palette.grey[100],
height: 56
}}
>
<TableRow>
<StyledTableCell>&nbsp;</StyledTableCell>
<StyledTableCell>
<TableSortLabel active={orderBy === 'name'} direction={order} onClick={() => handleRequestSort('name')}>
Name
</TableSortLabel>
</StyledTableCell>
<StyledTableCell>Description</StyledTableCell>
<StyledTableCell>Connected flows</StyledTableCell>
<StyledTableCell>Total characters</StyledTableCell>
<StyledTableCell>Total chunks</StyledTableCell>
<StyledTableCell>Loader Types</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>
</>
) : (
<>
{sortedData.map((row, index) => {
return (
<StyledTableRow
onClick={() => onRowClick(row)}
hover
key={index}
sx={{ cursor: 'pointer', '&:last-child td, &:last-child th': { border: 0 } }}
>
<StyledTableCell>
<DocumentStoreStatus isTableView={true} status={row.status} />
</StyledTableCell>
<StyledTableCell>
<Typography
sx={{
display: '-webkit-box',
WebkitLineClamp: 5,
WebkitBoxOrient: 'vertical',
textOverflow: 'ellipsis',
overflow: 'hidden'
}}
>
{row.name}
</Typography>
</StyledTableCell>
<StyledTableCell>
<Typography
sx={{
display: '-webkit-box',
WebkitLineClamp: 5,
WebkitBoxOrient: 'vertical',
textOverflow: 'ellipsis',
overflow: 'hidden'
}}
>
{row?.description}
</Typography>
</StyledTableCell>
<StyledTableCell>{row.whereUsed?.length ?? 0}</StyledTableCell>
<StyledTableCell>{row.totalChars}</StyledTableCell>
<StyledTableCell>{row.totalChunks}</StyledTableCell>
<StyledTableCell>
{images && images[row.id] && (
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'start',
gap: 1
}}
>
{images[row.id]
.slice(0, images[row.id].length > 3 ? 3 : images[row.id].length)
.map((img) => (
<Box
key={img}
sx={{
width: 30,
height: 30,
borderRadius: '50%',
backgroundColor: customization.isDarkMode
? theme.palette.common.white
: theme.palette.grey[300] + 75
}}
>
<img
style={{
width: '100%',
height: '100%',
padding: 5,
objectFit: 'contain'
}}
alt=''
src={img}
/>
</Box>
))}
{images?.length > 3 && (
<Typography
sx={{
alignItems: 'center',
display: 'flex',
fontSize: '.9rem',
fontWeight: 200
}}
>
+ {images.length - 3} More
</Typography>
)}
</Box>
)}
</StyledTableCell>
</StyledTableRow>
)
})}
</>
)}
</TableBody>
</Table>
</TableContainer>
</>
)
}
DocumentStoreTable.propTypes = {
data: PropTypes.array,
isLoading: PropTypes.bool,
images: PropTypes.object,
onRowClick: PropTypes.func
}
DocumentStoreTable.displayName = 'DocumentStoreTable'
+1 -1
View File
@@ -89,7 +89,7 @@ const sanitizeDocumentStore = (DocumentStore) => {
const sanitizeExecution = (Execution) => {
try {
return Execution.map((execution) => {
execution.agentflow.workspaceId = undefined
if (execution.agentflow) execution.agentflow.workspaceId = undefined
return { ...execution, workspaceId: undefined }
})
} catch (error) {
+67 -95
View File
@@ -4,7 +4,6 @@ import 'react-datepicker/dist/react-datepicker.css'
// material-ui
import {
Pagination,
Box,
Stack,
TextField,
@@ -21,7 +20,6 @@ import {
DialogTitle,
IconButton,
Tooltip,
Typography,
useTheme
} from '@mui/material'
@@ -44,6 +42,7 @@ import { IconTrash } from '@tabler/icons-react'
import { ExecutionsListTable } from '@/ui-component/table/ExecutionsListTable'
import { ExecutionDetails } from './ExecutionDetails'
import { omit } from 'lodash'
import TablePagination, { DEFAULT_ITEMS_PER_PAGE } from '@/ui-component/pagination/TablePagination'
// ==============================|| AGENT EXECUTIONS ||============================== //
@@ -71,11 +70,6 @@ const AgentExecutions = () => {
agentflowId: '',
sessionId: ''
})
const [pagination, setPagination] = useState({
page: 1,
limit: 10,
total: 0
})
const handleFilterChange = (field, value) => {
setFilters({
@@ -94,26 +88,25 @@ const AgentExecutions = () => {
})
}
const handlePageChange = (event, newPage) => {
setPagination({
...pagination,
page: newPage
})
/* Table Pagination */
const [currentPage, setCurrentPage] = useState(1)
const [pageLimit, setPageLimit] = useState(DEFAULT_ITEMS_PER_PAGE)
const [total, setTotal] = useState(0)
const onChange = (page, pageLimit) => {
setCurrentPage(page)
setPageLimit(pageLimit)
applyFilters(page, pageLimit)
}
const handleLimitChange = (event) => {
setPagination({
...pagination,
page: 1, // Reset to first page when changing items per page
limit: parseInt(event.target.value, 10)
})
}
const applyFilters = () => {
const applyFilters = (page, limit) => {
setLoading(true)
// Ensure page and limit are numbers, not objects
const pageNum = typeof page === 'number' ? page : currentPage
const limitNum = typeof limit === 'number' ? limit : pageLimit
const params = {
page: pagination.page,
limit: pagination.limit
page: pageNum,
limit: limitNum
}
if (filters.state) params.state = filters.state
@@ -152,7 +145,8 @@ const AgentExecutions = () => {
agentflowId: '',
sessionId: ''
})
getAllExecutions.request()
setCurrentPage(1)
getAllExecutions.request({ page: 1, limit: pageLimit })
}
const handleExecutionSelectionChange = (selectedIds) => {
@@ -175,7 +169,7 @@ const AgentExecutions = () => {
}
useEffect(() => {
getAllExecutions.request()
getAllExecutions.request({ page: 1, limit: DEFAULT_ITEMS_PER_PAGE })
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
@@ -186,7 +180,7 @@ const AgentExecutions = () => {
const { data, total } = getAllExecutions.data
if (!Array.isArray(data)) return
setExecutions(data)
setPagination((prev) => ({ ...prev, total }))
setTotal(total)
} catch (e) {
console.error(e)
}
@@ -201,17 +195,12 @@ const AgentExecutions = () => {
setError(getAllExecutions.error)
}, [getAllExecutions.error])
useEffect(() => {
applyFilters()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pagination.page, pagination.limit])
useEffect(() => {
if (deleteExecutionsApi.data) {
// Refresh the executions list
getAllExecutions.request({
page: pagination.page,
limit: pagination.limit
page: currentPage,
limit: pageLimit
})
setSelectedExecutionIds([])
}
@@ -339,7 +328,12 @@ const AgentExecutions = () => {
</Grid>
<Grid item xs={12} md={4}>
<Stack direction='row' spacing={1}>
<Button variant='contained' color='primary' onClick={applyFilters} size='small'>
<Button
variant='contained'
color='primary'
onClick={() => applyFilters(currentPage, pageLimit)}
size='small'
>
Apply
</Button>
<Button variant='outlined' onClick={resetFilters} size='small'>
@@ -366,69 +360,47 @@ const AgentExecutions = () => {
</Grid>
</Box>
<ExecutionsListTable
data={executions}
isLoading={isLoading}
onSelectionChange={handleExecutionSelectionChange}
onExecutionRowClick={(execution) => {
setOpenDrawer(true)
const executionDetails =
typeof execution.executionData === 'string' ? JSON.parse(execution.executionData) : execution.executionData
setSelectedExecutionData(executionDetails)
setSelectedMetadata(omit(execution, ['executionData']))
}}
/>
{/* Pagination and Page Size Controls */}
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mt: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Typography variant='body2'>Items per page:</Typography>
<FormControl
variant='outlined'
size='small'
sx={{
minWidth: 80,
'& .MuiOutlinedInput-notchedOutline': {
borderColor: borderColor
},
'& .MuiSvgIcon-root': {
color: customization.isDarkMode ? '#fff' : 'inherit'
}
{executions?.length > 0 && (
<>
<ExecutionsListTable
data={executions}
isLoading={isLoading}
onSelectionChange={handleExecutionSelectionChange}
onExecutionRowClick={(execution) => {
setOpenDrawer(true)
const executionDetails =
typeof execution.executionData === 'string'
? JSON.parse(execution.executionData)
: execution.executionData
setSelectedExecutionData(executionDetails)
setSelectedMetadata(omit(execution, ['executionData']))
}}
>
<Select value={pagination.limit} onChange={handleLimitChange} displayEmpty>
<MenuItem value={10}>10</MenuItem>
<MenuItem value={50}>50</MenuItem>
<MenuItem value={100}>100</MenuItem>
<MenuItem value={1000}>1000</MenuItem>
</Select>
</FormControl>
</Box>
<Pagination
count={Math.ceil(pagination.total / pagination.limit)}
page={pagination.page}
onChange={handlePageChange}
color='primary'
/>
</Box>
/>
<ExecutionDetails
open={openDrawer}
execution={selectedExecutionData}
metadata={selectedMetadata}
onClose={() => setOpenDrawer(false)}
onProceedSuccess={() => {
setOpenDrawer(false)
getAllExecutions.request()
}}
onUpdateSharing={() => {
getAllExecutions.request()
}}
onRefresh={(executionId) => {
getAllExecutions.request()
getExecutionByIdApi.request(executionId)
}}
/>
{/* Pagination and Page Size Controls */}
{!isLoading && total > 0 && (
<TablePagination currentPage={currentPage} limit={pageLimit} total={total} onChange={onChange} />
)}
<ExecutionDetails
open={openDrawer}
execution={selectedExecutionData}
metadata={selectedMetadata}
onClose={() => setOpenDrawer(false)}
onProceedSuccess={() => {
setOpenDrawer(false)
getAllExecutions.request()
}}
onUpdateSharing={() => {
getAllExecutions.request()
}}
onRefresh={(executionId) => {
getAllExecutions.request()
getExecutionByIdApi.request(executionId)
}}
/>
</>
)}
{/* Delete Confirmation Dialog */}
<Dialog
+45 -26
View File
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
// material-ui
import { Chip, Box, Skeleton, Stack, ToggleButton, ToggleButtonGroup } from '@mui/material'
import { Chip, Box, Stack, ToggleButton, ToggleButtonGroup } from '@mui/material'
import { useTheme } from '@mui/material/styles'
// project imports
@@ -15,6 +15,7 @@ import { FlowListTable } from '@/ui-component/table/FlowListTable'
import ViewHeader from '@/layout/MainLayout/ViewHeader'
import ErrorBoundary from '@/ErrorBoundary'
import { StyledPermissionButton } from '@/ui-component/button/RBACButtons'
import TablePagination, { DEFAULT_ITEMS_PER_PAGE } from '@/ui-component/pagination/TablePagination'
// API
import chatflowsApi from '@/api/chatflows'
@@ -45,6 +46,25 @@ const Agentflows = () => {
const [view, setView] = useState(localStorage.getItem('flowDisplayStyle') || 'card')
const [agentflowVersion, setAgentflowVersion] = useState(localStorage.getItem('agentFlowVersion') || 'v2')
/* Table Pagination */
const [currentPage, setCurrentPage] = useState(1)
const [pageLimit, setPageLimit] = useState(DEFAULT_ITEMS_PER_PAGE)
const [total, setTotal] = useState(0)
const onChange = (page, pageLimit) => {
setCurrentPage(page)
setPageLimit(pageLimit)
refresh(page, pageLimit, agentflowVersion)
}
const refresh = (page, limit, nextView) => {
const params = {
page: page || currentPage,
limit: limit || pageLimit
}
getAllAgentflows.request(nextView === 'v2' ? 'AGENTFLOW' : 'MULTIAGENT', params)
}
const handleChange = (event, nextView) => {
if (nextView === null) return
localStorage.setItem('flowDisplayStyle', nextView)
@@ -55,7 +75,7 @@ const Agentflows = () => {
if (nextView === null) return
localStorage.setItem('agentFlowVersion', nextView)
setAgentflowVersion(nextView)
getAllAgentflows.request(nextView === 'v2' ? 'AGENTFLOW' : 'MULTIAGENT')
refresh(1, pageLimit, nextView)
}
const onSearchChange = (event) => {
@@ -87,7 +107,7 @@ const Agentflows = () => {
}
useEffect(() => {
getAllAgentflows.request(agentflowVersion === 'v2' ? 'AGENTFLOW' : 'MULTIAGENT')
refresh(currentPage, pageLimit, agentflowVersion)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
@@ -107,7 +127,8 @@ const Agentflows = () => {
useEffect(() => {
if (getAllAgentflows.data) {
try {
const agentflows = getAllAgentflows.data
const agentflows = getAllAgentflows.data?.data
setTotal(getAllAgentflows.data?.total)
const images = {}
const icons = {}
for (let i = 0; i < agentflows.length; i += 1) {
@@ -189,6 +210,7 @@ const Agentflows = () => {
<ToggleButtonGroup
sx={{ borderRadius: 2, maxHeight: 40 }}
value={view}
disabled={total === 0}
color='primary'
exclusive
onChange={handleChange}
@@ -228,17 +250,11 @@ const Agentflows = () => {
Add New
</StyledPermissionButton>
</ViewHeader>
{!view || view === 'card' ? (
{!isLoading && total > 0 && (
<>
{isLoading && !getAllAgentflows.data ? (
{!view || view === 'card' ? (
<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}>
{getAllAgentflows.data?.filter(filterFlows).map((data, index) => (
{getAllAgentflows.data?.data.filter(filterFlows).map((data, index) => (
<ItemCard
key={index}
onClick={() => goToCanvas(data)}
@@ -248,22 +264,25 @@ const Agentflows = () => {
/>
))}
</Box>
) : (
<FlowListTable
isAgentCanvas={true}
isAgentflowV2={agentflowVersion === 'v2'}
data={getAllAgentflows.data?.data}
images={images}
icons={icons}
isLoading={isLoading}
filterFunction={filterFlows}
updateFlowsApi={getAllAgentflows}
setError={setError}
/>
)}
{/* Pagination and Page Size Controls */}
<TablePagination currentPage={currentPage} limit={pageLimit} total={total} onChange={onChange} />
</>
) : (
<FlowListTable
isAgentCanvas={true}
isAgentflowV2={agentflowVersion === 'v2'}
data={getAllAgentflows.data}
images={images}
icons={icons}
isLoading={isLoading}
filterFunction={filterFlows}
updateFlowsApi={getAllAgentflows}
setError={setError}
/>
)}
{!isLoading && (!getAllAgentflows.data || getAllAgentflows.data.length === 0) && (
{!isLoading && total === 0 && (
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
<Box sx={{ p: 2, height: 'auto' }}>
<img
+129 -104
View File
@@ -33,6 +33,8 @@ import ViewHeader from '@/layout/MainLayout/ViewHeader'
import ErrorBoundary from '@/ErrorBoundary'
import { PermissionButton, StyledPermissionButton } from '@/ui-component/button/RBACButtons'
import { Available } from '@/ui-component/rbac/available'
import UploadJSONFileDialog from '@/views/apikey/UploadJSONFileDialog'
import TablePagination, { DEFAULT_ITEMS_PER_PAGE } from '@/ui-component/pagination/TablePagination'
// API
import apiKeyApi from '@/api/apikey'
@@ -59,7 +61,6 @@ import {
IconFileUpload
} from '@tabler/icons-react'
import APIEmptySVG from '@/assets/images/api_empty.svg'
import UploadJSONFileDialog from '@/views/apikey/UploadJSONFileDialog'
// ==============================|| APIKey ||============================== //
@@ -222,6 +223,26 @@ const APIKey = () => {
const [uploadDialogProps, setUploadDialogProps] = useState({})
const [search, setSearch] = useState('')
/* Table Pagination */
const [currentPage, setCurrentPage] = useState(1)
const [pageLimit, setPageLimit] = useState(DEFAULT_ITEMS_PER_PAGE)
const [total, setTotal] = useState(0)
const onChange = (page, pageLimit) => {
setCurrentPage(page)
setPageLimit(pageLimit)
refresh(page, pageLimit)
}
const refresh = (page, limit) => {
const params = {
page: page || currentPage,
limit: limit || pageLimit
}
getAllAPIKeysApi.request(params)
}
const onSearchChange = (event) => {
setSearch(event.target.value)
}
@@ -341,12 +362,11 @@ const APIKey = () => {
const onConfirm = () => {
setShowDialog(false)
setShowUploadDialog(false)
getAllAPIKeysApi.request()
refresh(currentPage, pageLimit)
}
useEffect(() => {
getAllAPIKeysApi.request()
refresh(currentPage, pageLimit)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
@@ -356,7 +376,8 @@ const APIKey = () => {
useEffect(() => {
if (getAllAPIKeysApi.data) {
setAPIKeys(getAllAPIKeysApi.data)
setAPIKeys(getAllAPIKeysApi.data?.data)
setTotal(getAllAPIKeysApi.data?.total)
}
}, [getAllAPIKeysApi.data])
@@ -395,7 +416,7 @@ const APIKey = () => {
Create Key
</StyledPermissionButton>
</ViewHeader>
{!isLoading && apiKeys.length <= 0 ? (
{!isLoading && apiKeys?.length <= 0 ? (
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
<Box sx={{ p: 2, height: 'auto' }}>
<img
@@ -407,104 +428,108 @@ const APIKey = () => {
<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>Updated</StyledTableCell>
<Available permission={'apikeys:update,apikeys:create'}>
<StyledTableCell> </StyledTableCell>
</Available>
<Available permission={'apikeys:delete'}>
<StyledTableCell> </StyledTableCell>
</Available>
</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>
<Available permission={'apikeys:update,apikeys:create'}>
<StyledTableCell> </StyledTableCell>
</Available>
<Available permission={'apikeys:delete'}>
<StyledTableCell> </StyledTableCell>
</Available>
</StyledTableRow>
<StyledTableRow>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<Available permission={'apikeys:update,apikeys:create'}>
<StyledTableCell> </StyledTableCell>
</Available>
<Available permission={'apikeys:delete'}>
<StyledTableCell> </StyledTableCell>
</Available>
</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>
<>
<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>Updated</StyledTableCell>
<Available permission={'apikeys:update,apikeys:create'}>
<StyledTableCell> </StyledTableCell>
</Available>
<Available permission={'apikeys:delete'}>
<StyledTableCell> </StyledTableCell>
</Available>
</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>
<Available permission={'apikeys:update,apikeys:create'}>
<StyledTableCell> </StyledTableCell>
</Available>
<Available permission={'apikeys:delete'}>
<StyledTableCell> </StyledTableCell>
</Available>
</StyledTableRow>
<StyledTableRow>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<Available permission={'apikeys:update,apikeys:create'}>
<StyledTableCell> </StyledTableCell>
</Available>
<Available permission={'apikeys:delete'}>
<StyledTableCell> </StyledTableCell>
</Available>
</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>
{/* Pagination and Page Size Controls */}
<TablePagination currentPage={currentPage} limit={pageLimit} total={total} onChange={onChange} />
</>
)}
</Stack>
)}
@@ -510,7 +510,8 @@ const CustomAssistantConfigurePreview = () => {
} else if (setting === 'viewMessages') {
setViewMessagesDialogProps({
title: 'View Messages',
chatflow: canvas.chatflow
chatflow: canvas.chatflow,
isChatflow: false
})
setViewMessagesDialogOpen(true)
} else if (setting === 'viewLeads') {
@@ -76,7 +76,8 @@ const CanvasHeader = ({ chatflow, isAgentCanvas, isAgentflowV2, handleSaveFlow,
} else if (setting === 'viewMessages') {
setViewMessagesDialogProps({
title: 'View Messages',
chatflow: chatflow
chatflow: chatflow,
isChatflow: isAgentflowV2 ? false : true
})
setViewMessagesDialogOpen(true)
} else if (setting === 'viewLeads') {
+49 -30
View File
@@ -15,6 +15,7 @@ import { FlowListTable } from '@/ui-component/table/FlowListTable'
import { StyledPermissionButton } from '@/ui-component/button/RBACButtons'
import ViewHeader from '@/layout/MainLayout/ViewHeader'
import ErrorBoundary from '@/ErrorBoundary'
import TablePagination, { DEFAULT_ITEMS_PER_PAGE } from '@/ui-component/pagination/TablePagination'
// API
import chatflowsApi from '@/api/chatflows'
@@ -43,6 +44,25 @@ const Chatflows = () => {
const getAllChatflowsApi = useApi(chatflowsApi.getAllChatflows)
const [view, setView] = useState(localStorage.getItem('flowDisplayStyle') || 'card')
/* Table Pagination */
const [currentPage, setCurrentPage] = useState(1)
const [pageLimit, setPageLimit] = useState(DEFAULT_ITEMS_PER_PAGE)
const [total, setTotal] = useState(0)
const onChange = (page, pageLimit) => {
setCurrentPage(page)
setPageLimit(pageLimit)
applyFilters(page, pageLimit)
}
const applyFilters = (page, limit) => {
const params = {
page: page || currentPage,
limit: limit || pageLimit
}
getAllChatflowsApi.request(params)
}
const handleChange = (event, nextView) => {
if (nextView === null) return
localStorage.setItem('flowDisplayStyle', nextView)
@@ -70,7 +90,7 @@ const Chatflows = () => {
}
useEffect(() => {
getAllChatflowsApi.request()
applyFilters(currentPage, pageLimit)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
@@ -81,7 +101,9 @@ const Chatflows = () => {
useEffect(() => {
if (getAllChatflowsApi.data) {
try {
const chatflows = getAllChatflowsApi.data
const chatflows = getAllChatflowsApi.data?.data
const total = getAllChatflowsApi.data?.total
setTotal(total)
const images = {}
for (let i = 0; i < chatflows.length; i += 1) {
const flowDataStr = chatflows[i].flowData
@@ -123,6 +145,7 @@ const Chatflows = () => {
sx={{ borderRadius: 2, maxHeight: 40 }}
value={view}
color='primary'
disabled={total === 0}
exclusive
onChange={handleChange}
>
@@ -161,41 +184,37 @@ const Chatflows = () => {
Add New
</StyledPermissionButton>
</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>
)}
{!isLoading && total > 0 && (
<>
{isLoading && !getAllChatflowsApi.data ? (
{!view || view === 'card' ? (
<Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
<Skeleton variant='rounded' height={160} />
<Skeleton variant='rounded' height={160} />
<Skeleton variant='rounded' height={160} />
{getAllChatflowsApi.data?.data?.filter(filterFlows).map((data, index) => (
<ItemCard key={index} onClick={() => goToCanvas(data)} data={data} images={images[data.id]} />
))}
</Box>
) : (
<Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
{getAllChatflowsApi.data &&
getAllChatflowsApi.data
?.filter(filterFlows)
.map((data, index) => (
<ItemCard
key={index}
onClick={() => goToCanvas(data)}
data={data}
images={images[data.id]}
/>
))}
</Box>
<FlowListTable
data={getAllChatflowsApi.data?.data}
images={images}
isLoading={isLoading}
filterFunction={filterFlows}
updateFlowsApi={getAllChatflowsApi}
setError={setError}
/>
)}
{/* Pagination and Page Size Controls */}
<TablePagination currentPage={currentPage} limit={pageLimit} total={total} onChange={onChange} />
</>
) : (
<FlowListTable
data={getAllChatflowsApi.data}
images={images}
isLoading={isLoading}
filterFunction={filterFlows}
updateFlowsApi={getAllChatflowsApi}
setError={setError}
/>
)}
{!isLoading && (!getAllChatflowsApi.data || getAllChatflowsApi.data.length === 0) && (
{!isLoading && (!getAllChatflowsApi.data?.data || getAllChatflowsApi.data?.data.length === 0) && (
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
<Box sx={{ p: 2, height: 'auto' }}>
<img
@@ -26,9 +26,11 @@ import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog'
import AddEditDatasetRowDialog from './AddEditDatasetRowDialog'
import UploadCSVFileDialog from '@/views/datasets/UploadCSVFileDialog'
import ErrorBoundary from '@/ErrorBoundary'
import { useError } from '@/store/context/ErrorContext'
import ViewHeader from '@/layout/MainLayout/ViewHeader'
import { PermissionButton, StyledPermissionButton } from '@/ui-component/button/RBACButtons'
import AddEditDatasetDialog from '@/views/datasets/AddEditDatasetDialog'
import TablePagination, { DEFAULT_ITEMS_PER_PAGE } from '@/ui-component/pagination/TablePagination'
// API
import datasetsApi from '@/api/dataset'
@@ -45,8 +47,6 @@ import empty_datasetSVG from '@/assets/images/empty_datasets.svg'
import { IconTrash, IconPlus, IconX, IconUpload, IconArrowsDownUp } from '@tabler/icons-react'
import DragIndicatorIcon from '@mui/icons-material/DragIndicator'
import { useError } from '@/store/context/ErrorContext'
// ==============================|| Dataset Items ||============================== //
const EvalDatasetRows = () => {
@@ -85,6 +85,25 @@ const EvalDatasetRows = () => {
const [startDragPos, setStartDragPos] = useState(-1)
const [endDragPos, setEndDragPos] = useState(-1)
/* Table Pagination */
const [currentPage, setCurrentPage] = useState(1)
const [pageLimit, setPageLimit] = useState(DEFAULT_ITEMS_PER_PAGE)
const [total, setTotal] = useState(0)
const onChange = (page, pageLimit) => {
setCurrentPage(page)
setPageLimit(pageLimit)
refresh(page, pageLimit)
}
const refresh = (page, limit) => {
setLoading(true)
const params = {
page: page || currentPage,
limit: limit || pageLimit
}
getDatasetRows.request(datasetId, params)
}
const handleDragStart = (e, position) => {
draggingItem.current = position
setStartDragPos(position)
@@ -242,11 +261,11 @@ const EvalDatasetRows = () => {
setShowRowDialog(false)
setShowUploadDialog(false)
setShowDatasetDialog(false)
getDatasetRows.request(datasetId)
refresh(currentPage, pageLimit)
}
useEffect(() => {
getDatasetRows.request(datasetId)
refresh(currentPage, pageLimit)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
@@ -254,6 +273,7 @@ const EvalDatasetRows = () => {
if (getDatasetRows.data) {
const dataset = getDatasetRows.data
setDataset(dataset)
setTotal(dataset.total)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getDatasetRows.data])
@@ -449,9 +469,11 @@ const EvalDatasetRows = () => {
</TableBody>
</Table>
</TableContainer>
<Typography sx={{ color: theme.palette.grey[600] }} variant='subtitle2'>
<Typography sx={{ color: theme.palette.grey[600], marginTop: -2 }} variant='subtitle2'>
<i>Use the drag icon at (extreme right) to reorder the dataset items</i>
</Typography>
{/* Pagination and Page Size Controls */}
<TablePagination currentPage={currentPage} limit={pageLimit} total={total} onChange={onChange} />
</React.Fragment>
)}
</Stack>
+140 -111
View File
@@ -29,6 +29,7 @@ import ErrorBoundary from '@/ErrorBoundary'
import { StyledTableCell, StyledTableRow } from '@/ui-component/table/TableStyles'
import { StyledPermissionButton } from '@/ui-component/button/RBACButtons'
import { Available } from '@/ui-component/rbac/available'
import TablePagination, { DEFAULT_ITEMS_PER_PAGE } from '@/ui-component/pagination/TablePagination'
// API
import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction } from '@/store/actions'
@@ -70,8 +71,27 @@ const EvalDatasets = () => {
const [datasetDialogProps, setDatasetDialogProps] = useState({})
const getAllDatasets = useApi(datasetsApi.getAllDatasets)
/* Table Pagination */
const [currentPage, setCurrentPage] = useState(1)
const [pageLimit, setPageLimit] = useState(DEFAULT_ITEMS_PER_PAGE)
const [total, setTotal] = useState(0)
const onChange = (page, pageLimit) => {
setCurrentPage(page)
setPageLimit(pageLimit)
refresh(page, pageLimit)
}
const refresh = (page, limit) => {
setLoading(true)
const params = {
page: page || currentPage,
limit: limit || pageLimit
}
getAllDatasets.request(params)
}
const goToRows = (selectedDataset) => {
navigate(`/dataset_rows/${selectedDataset.id}`)
navigate(`/dataset_rows/${selectedDataset.id}?page=1&limit=10`)
}
const onSearchChange = (event) => {
@@ -149,7 +169,7 @@ const EvalDatasets = () => {
const onConfirm = () => {
setShowDatasetDialog(false)
getAllDatasets.request()
refresh()
}
function filterDatasets(data) {
@@ -157,13 +177,14 @@ const EvalDatasets = () => {
}
useEffect(() => {
getAllDatasets.request()
refresh(currentPage, pageLimit)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
if (getAllDatasets.data) {
setDatasets(getAllDatasets.data)
setDatasets(getAllDatasets.data?.data)
setTotal(getAllDatasets.data?.total)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getAllDatasets.data])
@@ -209,118 +230,126 @@ const EvalDatasets = () => {
<div>No Datasets Yet</div>
</Stack>
) : (
<TableContainer
sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2 }}
component={Paper}
>
<Table sx={{ minWidth: 650 }}>
<TableHead
sx={{
backgroundColor: customization.isDarkMode
? theme.palette.common.black
: theme.palette.grey[100],
height: 56
}}
>
<TableRow>
<TableCell>Name</TableCell>
<TableCell>Description</TableCell>
<TableCell>Rows</TableCell>
<TableCell>Last Updated</TableCell>
<Available permission={'datasets:update,datasets:create'}>
<TableCell> </TableCell>
</Available>
<Available permission={'datasets:delete'}>
<TableCell> </TableCell>
</Available>
</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>
<Available permission={'datasets:update,datasets:create'}>
<Skeleton variant='text' />
</Available>
<Available permission={'datasets:delete'}>
<Skeleton variant='text' />
</Available>
</StyledTableRow>
<StyledTableRow>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<Available permission={'datasets:update,datasets:create'}>
<Skeleton variant='text' />
</Available>
<Available permission={'datasets:delete'}>
<Skeleton variant='text' />
</Available>
</StyledTableRow>
</>
) : (
<>
{datasets.filter(filterDatasets).map((ds, index) => (
<StyledTableRow
hover
key={index}
sx={{ cursor: 'pointer', '&:last-child td, &:last-child th': { border: 0 } }}
>
<TableCell onClick={() => goToRows(ds)} component='th' scope='row'>
{ds.name}
</TableCell>
<TableCell
onClick={() => goToRows(ds)}
style={{ wordWrap: 'break-word', flexWrap: 'wrap', width: '40%' }}
>
{truncateString(ds?.description, 200)}
</TableCell>
<TableCell onClick={() => goToRows(ds)}>{ds?.rowCount}</TableCell>
<TableCell onClick={() => goToRows(ds)}>
{moment(ds.updatedDate).format('MMMM Do YYYY, hh:mm A')}
</TableCell>
<>
<TableContainer
sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2 }}
component={Paper}
>
<Table sx={{ minWidth: 650 }}>
<TableHead
sx={{
backgroundColor: customization.isDarkMode
? theme.palette.common.black
: theme.palette.grey[100],
height: 56
}}
>
<TableRow>
<TableCell>Name</TableCell>
<TableCell>Description</TableCell>
<TableCell>Rows</TableCell>
<TableCell>Last Updated</TableCell>
<Available permission={'datasets:update,datasets:create'}>
<TableCell> </TableCell>
</Available>
<Available permission={'datasets:delete'}>
<TableCell> </TableCell>
</Available>
</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>
<Available permission={'datasets:update,datasets:create'}>
<TableCell>
<IconButton title='Edit' color='primary' onClick={() => edit(ds)}>
<IconEdit />
</IconButton>
</TableCell>
<Skeleton variant='text' />
</Available>
<Available permission={'datasets:delete'}>
<TableCell>
<IconButton title='Delete' color='error' onClick={() => deleteDataset(ds)}>
<IconTrash />
</IconButton>
</TableCell>
<Skeleton variant='text' />
</Available>
</StyledTableRow>
))}
</>
)}
</TableBody>
</Table>
</TableContainer>
<StyledTableRow>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<Available permission={'datasets:update,datasets:create'}>
<Skeleton variant='text' />
</Available>
<Available permission={'datasets:delete'}>
<Skeleton variant='text' />
</Available>
</StyledTableRow>
</>
) : (
<>
{datasets.filter(filterDatasets).map((ds, index) => (
<StyledTableRow
hover
key={index}
sx={{ cursor: 'pointer', '&:last-child td, &:last-child th': { border: 0 } }}
>
<TableCell onClick={() => goToRows(ds)} component='th' scope='row'>
{ds.name}
</TableCell>
<TableCell
onClick={() => goToRows(ds)}
style={{ wordWrap: 'break-word', flexWrap: 'wrap', width: '40%' }}
>
{truncateString(ds?.description, 200)}
</TableCell>
<TableCell onClick={() => goToRows(ds)}>{ds?.rowCount}</TableCell>
<TableCell onClick={() => goToRows(ds)}>
{moment(ds.updatedDate).format('MMMM Do YYYY, hh:mm A')}
</TableCell>
<Available permission={'datasets:update,datasets:create'}>
<TableCell>
<IconButton title='Edit' color='primary' onClick={() => edit(ds)}>
<IconEdit />
</IconButton>
</TableCell>
</Available>
<Available permission={'datasets:delete'}>
<TableCell>
<IconButton
title='Delete'
color='error'
onClick={() => deleteDataset(ds)}
>
<IconTrash />
</IconButton>
</TableCell>
</Available>
</StyledTableRow>
))}
</>
)}
</TableBody>
</Table>
</TableContainer>
{/* Pagination and Page Size Controls */}
<TablePagination currentPage={currentPage} limit={pageLimit} total={total} onChange={onChange} />
</>
)}
</Stack>
)}
+103 -203
View File
@@ -1,32 +1,18 @@
import { useEffect, useState } from 'react'
import React, { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useSelector } from 'react-redux'
// material-ui
import {
Box,
Paper,
Skeleton,
Stack,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
ToggleButton,
ToggleButtonGroup,
Typography
} from '@mui/material'
import { Box, Stack, ToggleButton, ToggleButtonGroup } from '@mui/material'
import { useTheme } from '@mui/material/styles'
// project imports
import ErrorBoundary from '@/ErrorBoundary'
import { useError } from '@/store/context/ErrorContext'
import MainCard from '@/ui-component/cards/MainCard'
import TablePagination, { DEFAULT_ITEMS_PER_PAGE } from '@/ui-component/pagination/TablePagination'
import DocumentStoreCard from '@/ui-component/cards/DocumentStoreCard'
import AddDocStoreDialog from '@/views/docstore/AddDocStoreDialog'
import ErrorBoundary from '@/ErrorBoundary'
import ViewHeader from '@/layout/MainLayout/ViewHeader'
import DocumentStoreStatus from '@/views/docstore/DocumentStoreStatus'
import { StyledPermissionButton } from '@/ui-component/button/RBACButtons'
// API
@@ -39,13 +25,12 @@ import doc_store_empty from '@/assets/images/doc_store_empty.svg'
// const
import { baseURL, gridSpacing } from '@/store/constant'
import { useError } from '@/store/context/ErrorContext'
import { DocumentStoreTable } from '@/ui-component/table/DocumentStoreTable'
// ==============================|| DOCUMENTS ||============================== //
const Documents = () => {
const theme = useTheme()
const customization = useSelector((state) => state.customization)
const navigate = useNavigate()
const getAllDocumentStores = useApi(documentsApi.getAllDocumentStores)
@@ -66,7 +51,9 @@ const Documents = () => {
}
function filterDocStores(data) {
return data.name.toLowerCase().indexOf(search.toLowerCase()) > -1
return (
data.name.toLowerCase().indexOf(search.toLowerCase()) > -1 || data.description.toLowerCase().indexOf(search.toLowerCase()) > -1
)
}
const onSearchChange = (event) => {
@@ -90,41 +77,61 @@ const Documents = () => {
const onConfirm = () => {
setShowDialog(false)
getAllDocumentStores.request()
applyFilters(currentPage, pageLimit)
}
useEffect(() => {
getAllDocumentStores.request()
applyFilters(currentPage, pageLimit)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
/* Table Pagination */
const [currentPage, setCurrentPage] = useState(1)
const [pageLimit, setPageLimit] = useState(DEFAULT_ITEMS_PER_PAGE)
const [total, setTotal] = useState(0)
const onChange = (page, pageLimit) => {
setCurrentPage(page)
setPageLimit(pageLimit)
applyFilters(page, pageLimit)
}
const applyFilters = (page, limit) => {
setLoading(true)
const params = {
page: page || currentPage,
limit: limit || pageLimit
}
getAllDocumentStores.request(params)
}
useEffect(() => {
if (getAllDocumentStores.data) {
try {
const docStores = getAllDocumentStores.data
if (!Array.isArray(docStores)) return
const { data, total } = getAllDocumentStores.data
if (!Array.isArray(data)) return
const loaderImages = {}
for (let i = 0; i < docStores.length; i += 1) {
const loaders = docStores[i].loaders ?? []
for (let i = 0; i < data.length; i += 1) {
const loaders = data[i].loaders ?? []
let totalChunks = 0
let totalChars = 0
loaderImages[docStores[i].id] = []
loaderImages[data[i].id] = []
for (let j = 0; j < loaders.length; j += 1) {
const imageSrc = `${baseURL}/api/v1/node-icon/${loaders[j].loaderId}`
if (!loaderImages[docStores[i].id].includes(imageSrc)) {
loaderImages[docStores[i].id].push(imageSrc)
if (!loaderImages[data[i].id].includes(imageSrc)) {
loaderImages[data[i].id].push(imageSrc)
}
totalChunks += loaders[j]?.totalChunks ?? 0
totalChars += loaders[j]?.totalChars ?? 0
}
docStores[i].totalDocs = loaders?.length ?? 0
docStores[i].totalChunks = totalChunks
docStores[i].totalChars = totalChars
data[i].totalDocs = loaders?.length ?? 0
data[i].totalChunks = totalChunks
data[i].totalChars = totalChars
}
setDocStores(docStores)
setDocStores(data)
setTotal(total)
setImages(loaderImages)
} catch (e) {
console.error(e)
@@ -136,6 +143,8 @@ const Documents = () => {
setLoading(getAllDocumentStores.loading)
}, [getAllDocumentStores.loading])
const hasDocStores = docStores && docStores.length > 0
return (
<MainCard>
{error ? (
@@ -144,43 +153,45 @@ const Documents = () => {
<Stack flexDirection='column' sx={{ gap: 3 }}>
<ViewHeader
onSearchChange={onSearchChange}
search={true}
search={hasDocStores}
searchPlaceholder='Search Name'
title='Document Store'
description='Store and upsert documents for LLM retrieval (RAG)'
>
<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'
{hasDocStores && (
<ToggleButtonGroup
sx={{ borderRadius: 2, maxHeight: 40 }}
value={view}
color='primary'
exclusive
onChange={handleChange}
>
<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>
<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>
)}
<StyledPermissionButton
permissionId={'documentStores:create'}
variant='contained'
@@ -192,142 +203,7 @@ const Documents = () => {
Add New
</StyledPermissionButton>
</ViewHeader>
{!view || view === 'card' ? (
<>
{isLoading && !docStores ? (
<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}>
{docStores?.filter(filterDocStores).map((data, index) => (
<DocumentStoreCard
key={index}
images={images[data.id]}
data={data}
onClick={() => goToDocumentStore(data.id)}
/>
))}
</Box>
)}
</>
) : (
<TableContainer sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2 }} component={Paper}>
<Table aria-label='documents table'>
<TableHead
sx={{
backgroundColor: customization.isDarkMode ? theme.palette.common.black : theme.palette.grey[100],
height: 56
}}
>
<TableRow>
<TableCell>&nbsp;</TableCell>
<TableCell>Name</TableCell>
<TableCell>Description</TableCell>
<TableCell>Connected flows</TableCell>
<TableCell>Total characters</TableCell>
<TableCell>Total chunks</TableCell>
<TableCell>Loader types</TableCell>
</TableRow>
</TableHead>
<TableBody>
{docStores?.filter(filterDocStores).map((data, index) => (
<TableRow
onClick={() => goToDocumentStore(data.id)}
hover
key={index}
sx={{ cursor: 'pointer', '&:last-child td, &:last-child th': { border: 0 } }}
>
<TableCell align='center'>
<DocumentStoreStatus isTableView={true} status={data.status} />
</TableCell>
<TableCell>
<Typography
sx={{
display: '-webkit-box',
WebkitLineClamp: 5,
WebkitBoxOrient: 'vertical',
textOverflow: 'ellipsis',
overflow: 'hidden'
}}
>
{data.name}
</Typography>
</TableCell>
<TableCell>
<Typography
sx={{
display: '-webkit-box',
WebkitLineClamp: 5,
WebkitBoxOrient: 'vertical',
textOverflow: 'ellipsis',
overflow: 'hidden'
}}
>
{data?.description}
</Typography>
</TableCell>
<TableCell>{data.whereUsed?.length ?? 0}</TableCell>
<TableCell>{data.totalChars}</TableCell>
<TableCell>{data.totalChunks}</TableCell>
<TableCell>
{images[data.id] && (
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'start',
gap: 1
}}
>
{images[data.id].slice(0, images.length > 3 ? 3 : images.length).map((img) => (
<Box
key={img}
sx={{
width: 30,
height: 30,
borderRadius: '50%',
backgroundColor: customization.isDarkMode
? theme.palette.common.white
: theme.palette.grey[300] + 75
}}
>
<img
style={{
width: '100%',
height: '100%',
padding: 5,
objectFit: 'contain'
}}
alt=''
src={img}
/>
</Box>
))}
{images.length > 3 && (
<Typography
sx={{
alignItems: 'center',
display: 'flex',
fontSize: '.9rem',
fontWeight: 200
}}
>
+ {images.length - 3} More
</Typography>
)}
</Box>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
{!isLoading && (!docStores || docStores.length === 0) && (
{!hasDocStores ? (
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
<Box sx={{ p: 2, height: 'auto' }}>
<img
@@ -338,6 +214,30 @@ const Documents = () => {
</Box>
<div>No Document Stores Created Yet</div>
</Stack>
) : (
<React.Fragment>
{!view || view === 'card' ? (
<Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
{docStores?.filter(filterDocStores).map((data, index) => (
<DocumentStoreCard
key={index}
images={images[data.id]}
data={data}
onClick={() => goToDocumentStore(data.id)}
/>
))}
</Box>
) : (
<DocumentStoreTable
isLoading={isLoading}
data={docStores?.filter(filterDocStores)}
images={images}
onRowClick={(row) => goToDocumentStore(row.id)}
/>
)}
{/* Pagination and Page Size Controls */}
<TablePagination currentPage={currentPage} limit={pageLimit} total={total} onChange={onChange} />
</React.Fragment>
)}
</Stack>
)}
+134 -110
View File
@@ -33,6 +33,7 @@ import useApi from '@/hooks/useApi'
// Hooks
import useConfirm from '@/hooks/useConfirm'
import useNotifier from '@/utils/useNotifier'
import { useError } from '@/store/context/ErrorContext'
// project
import MainCard from '@/ui-component/cards/MainCard'
@@ -43,6 +44,7 @@ import ViewHeader from '@/layout/MainLayout/ViewHeader'
import { StyledTableCell, StyledTableRow } from '@/ui-component/table/TableStyles'
import CreateEvaluationDialog from '@/views/evaluations/CreateEvaluationDialog'
import { StyledPermissionButton } from '@/ui-component/button/RBACButtons'
import TablePagination, { DEFAULT_ITEMS_PER_PAGE } from '@/ui-component/pagination/TablePagination'
// icons
import {
@@ -59,8 +61,6 @@ import {
} from '@tabler/icons-react'
import empty_evalSVG from '@/assets/images/empty_evals.svg'
import { useError } from '@/store/context/ErrorContext'
const EvalsEvaluation = () => {
const theme = useTheme()
const customization = useSelector((state) => state.customization)
@@ -83,6 +83,24 @@ const EvalsEvaluation = () => {
const [selected, setSelected] = useState([])
const [autoRefresh, setAutoRefresh] = useState(false)
/* Table Pagination */
const [currentPage, setCurrentPage] = useState(1)
const [pageLimit, setPageLimit] = useState(DEFAULT_ITEMS_PER_PAGE)
const [total, setTotal] = useState(0)
const onChange = (page, pageLimit) => {
setCurrentPage(page)
setPageLimit(pageLimit)
refresh(page, pageLimit)
}
const refresh = (page, limit) => {
const params = {
page: page || currentPage,
limit: limit || pageLimit
}
getAllEvaluations.request(params)
}
const onSelectAllClick = (event) => {
if (event.target.checked) {
const newSelected = rows.filter((item) => item?.latestEval).map((n) => n.id)
@@ -171,13 +189,14 @@ const EvalsEvaluation = () => {
}
useEffect(() => {
getAllEvaluations.request()
refresh(currentPage, pageLimit)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
if (getAllEvaluations.data) {
const evalRows = getAllEvaluations.data
const evalRows = getAllEvaluations.data.data
setTotal(getAllEvaluations.data.total)
if (evalRows) {
// Prepare the data for the table
for (let i = 0; i < evalRows.length; i++) {
@@ -244,7 +263,8 @@ const EvalsEvaluation = () => {
}, [createNewEvaluation.error])
const onRefresh = useCallback(() => {
getAllEvaluations.request()
refresh(currentPage, pageLimit)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getAllEvaluations])
useEffect(() => {
@@ -358,111 +378,115 @@ const EvalsEvaluation = () => {
<div>No Evaluations Yet</div>
</Stack>
) : (
<TableContainer
sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2 }}
component={Paper}
>
<Table sx={{ minWidth: 650 }}>
<TableHead
sx={{
backgroundColor: customization.isDarkMode
? theme.palette.common.black
: theme.palette.grey[100],
height: 56
}}
>
<TableRow>
<TableCell padding='checkbox'>
<Checkbox
color='primary'
checked={selected.length === (rows.filter((item) => item?.latestEval) || []).length}
onChange={onSelectAllClick}
inputProps={{
'aria-label': 'select all'
}}
/>
</TableCell>
<TableCell width={10}> </TableCell>
<TableCell>Name</TableCell>
<TableCell>Latest Version</TableCell>
<TableCell>Average Metrics</TableCell>
<TableCell>Last Evaluated</TableCell>
<TableCell>Flow(s)</TableCell>
<TableCell>Dataset</TableCell>
<TableCell> </TableCell>
</TableRow>
</TableHead>
<TableBody>
{isTableLoading ? (
<>
<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>
</>
) : (
<>
{rows
.filter((item) => item?.latestEval)
.map((item, index) => (
<EvaluationRunRow
rows={rows.filter((row) => row.name === item.name)}
item={item}
key={index}
theme={theme}
selected={selected}
customization={customization}
onRefresh={onRefresh}
handleSelect={handleSelect}
/>
))}
</>
)}
</TableBody>
</Table>
</TableContainer>
<>
<TableContainer
sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2 }}
component={Paper}
>
<Table sx={{ minWidth: 650 }}>
<TableHead
sx={{
backgroundColor: customization.isDarkMode
? theme.palette.common.black
: theme.palette.grey[100],
height: 56
}}
>
<TableRow>
<TableCell padding='checkbox'>
<Checkbox
color='primary'
checked={selected.length === (rows.filter((item) => item?.latestEval) || []).length}
onChange={onSelectAllClick}
inputProps={{
'aria-label': 'select all'
}}
/>
</TableCell>
<TableCell width={10}> </TableCell>
<TableCell>Name</TableCell>
<TableCell>Latest Version</TableCell>
<TableCell>Average Metrics</TableCell>
<TableCell>Last Evaluated</TableCell>
<TableCell>Flow(s)</TableCell>
<TableCell>Dataset</TableCell>
<TableCell> </TableCell>
</TableRow>
</TableHead>
<TableBody>
{isTableLoading ? (
<>
<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>
</>
) : (
<>
{rows
.filter((item) => item?.latestEval)
.map((item, index) => (
<EvaluationRunRow
rows={rows.filter((row) => row.name === item.name)}
item={item}
key={index}
theme={theme}
selected={selected}
customization={customization}
onRefresh={onRefresh}
handleSelect={handleSelect}
/>
))}
</>
)}
</TableBody>
</Table>
</TableContainer>
{/* Pagination and Page Size Controls */}
<TablePagination currentPage={currentPage} limit={pageLimit} total={total} onChange={onChange} />
</>
)}
</Stack>
)}
+370 -325
View File
@@ -14,6 +14,8 @@ import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog'
import AddEditEvaluatorDialog from '@/views/evaluators/AddEditEvaluatorDialog'
import { StyledTableCell, StyledTableRow } from '@/ui-component/table/TableStyles'
import { PermissionIconButton, StyledPermissionButton } from '@/ui-component/button/RBACButtons'
import TablePagination, { DEFAULT_ITEMS_PER_PAGE } from '@/ui-component/pagination/TablePagination'
import { truncateString } from '@/utils/genericHelper'
// API
import evaluatorsApi from '@/api/evaluators'
@@ -23,15 +25,14 @@ import moment from 'moment/moment'
import useNotifier from '@/utils/useNotifier'
import useConfirm from '@/hooks/useConfirm'
import useApi from '@/hooks/useApi'
import { useError } from '@/store/context/ErrorContext'
// icons
import empty_evaluatorSVG from '@/assets/images/empty_evaluators.svg'
import { IconTrash, IconPlus, IconJson, IconX, IconNumber123, IconAbc, IconAugmentedReality } from '@tabler/icons-react'
import { truncateString } from '@/utils/genericHelper'
// const
import { evaluators as evaluatorsOptions, numericOperators } from '../evaluators/evaluatorConstant'
import { useError } from '@/store/context/ErrorContext'
// ==============================|| Evaluators ||============================== //
@@ -54,6 +55,24 @@ const Evaluators = () => {
const getAllEvaluators = useApi(evaluatorsApi.getAllEvaluators)
/* Table Pagination */
const [currentPage, setCurrentPage] = useState(1)
const [pageLimit, setPageLimit] = useState(DEFAULT_ITEMS_PER_PAGE)
const [total, setTotal] = useState(0)
const onChange = (page, pageLimit) => {
setCurrentPage(page)
setPageLimit(pageLimit)
refresh(page, pageLimit)
}
const refresh = (page, limit) => {
const params = {
page: page || currentPage,
limit: limit || pageLimit
}
getAllEvaluators.request(params)
}
const onSearchChange = (event) => {
setSearch(event.target.value)
}
@@ -129,7 +148,7 @@ const Evaluators = () => {
const onConfirm = () => {
setShowEvaluatorDialog(false)
getAllEvaluators.request()
refresh(currentPage, pageLimit)
}
function filterDatasets(data) {
@@ -137,13 +156,14 @@ const Evaluators = () => {
}
useEffect(() => {
getAllEvaluators.request()
refresh(currentPage, pageLimit)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
if (getAllEvaluators.data) {
setEvaluators(getAllEvaluators.data)
setEvaluators(getAllEvaluators.data.data)
setTotal(getAllEvaluators.data.total)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getAllEvaluators.data])
@@ -189,327 +209,352 @@ const Evaluators = () => {
<div>No Evaluators Yet</div>
</Stack>
) : (
<TableContainer
sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2 }}
component={Paper}
>
<Table sx={{ minWidth: 650 }}>
<TableHead
sx={{
backgroundColor: customization.isDarkMode
? theme.palette.common.black
: theme.palette.grey[100],
height: 56
}}
>
<TableRow>
<TableCell>Type</TableCell>
<TableCell>Name</TableCell>
<TableCell>Details</TableCell>
<TableCell>Last Updated</TableCell>
<TableCell> </TableCell>
</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>
</>
) : (
<>
{evaluators.filter(filterDatasets).map((ds, index) => (
<>
<StyledTableRow
hover
key={index}
sx={{ cursor: 'pointer', '&:last-child td, &:last-child th': { border: 0 } }}
>
<TableCell onClick={() => edit(ds)}>
{ds?.type === 'numeric' && (
<Stack flexDirection='row' sx={{ alignItems: 'center' }}>
<Chip icon={<IconNumber123 />} label='Numeric' variant='outlined' />
</Stack>
)}
{ds?.type === 'text' && (
<Stack flexDirection='row' sx={{ alignItems: 'center' }}>
<Chip icon={<IconAbc />} label='Text Based' variant='outlined' />
</Stack>
)}
{ds?.type === 'json' && (
<Stack flexDirection='row' sx={{ alignItems: 'center' }}>
<Chip icon={<IconJson />} label='JSON Based' variant='outlined' />
</Stack>
)}
{ds?.type === 'llm' && (
<Stack flexDirection='row' sx={{ alignItems: 'center' }}>
<Chip
icon={<IconAugmentedReality />}
label='LLM Based'
variant='outlined'
/>
</Stack>
)}
</TableCell>
<TableCell onClick={() => edit(ds)} component='th' scope='row'>
{ds.name}
</TableCell>
<TableCell style={{ width: '40%' }} onClick={() => edit(ds)}>
{ds?.type === 'numeric' && (
<Stack
flexDirection='row'
gap={1}
sx={{ alignItems: 'center', flexWrap: 'wrap' }}
<>
<TableContainer
sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2 }}
component={Paper}
>
<Table sx={{ minWidth: 650 }}>
<TableHead
sx={{
backgroundColor: customization.isDarkMode
? theme.palette.common.black
: theme.palette.grey[100],
height: 56
}}
>
<TableRow>
<TableCell>Type</TableCell>
<TableCell>Name</TableCell>
<TableCell>Details</TableCell>
<TableCell>Last Updated</TableCell>
<TableCell> </TableCell>
</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>
</>
) : (
<>
{evaluators.filter(filterDatasets).map((ds, index) => (
<>
<StyledTableRow
hover
key={index}
sx={{
cursor: 'pointer',
'&:last-child td, &:last-child th': { border: 0 }
}}
>
<TableCell onClick={() => edit(ds)}>
{ds?.type === 'numeric' && (
<Stack flexDirection='row' sx={{ alignItems: 'center' }}>
<Chip
icon={<IconNumber123 />}
label='Numeric'
variant='outlined'
/>
</Stack>
)}
{ds?.type === 'text' && (
<Stack flexDirection='row' sx={{ alignItems: 'center' }}>
<Chip
icon={<IconAbc />}
label='Text Based'
variant='outlined'
/>
</Stack>
)}
{ds?.type === 'json' && (
<Stack flexDirection='row' sx={{ alignItems: 'center' }}>
<Chip
icon={<IconJson />}
label='JSON Based'
variant='outlined'
/>
</Stack>
)}
{ds?.type === 'llm' && (
<Stack flexDirection='row' sx={{ alignItems: 'center' }}>
<Chip
icon={<IconAugmentedReality />}
label='LLM Based'
variant='outlined'
/>
</Stack>
)}
</TableCell>
<TableCell onClick={() => edit(ds)} component='th' scope='row'>
{ds.name}
</TableCell>
<TableCell style={{ width: '40%' }} onClick={() => edit(ds)}>
{ds?.type === 'numeric' && (
<Stack
flexDirection='row'
gap={1}
sx={{ alignItems: 'center', flexWrap: 'wrap' }}
>
<Chip
variant='outlined'
size='small'
color='default'
sx={{
height: 'auto',
'& .MuiChip-label': {
display: 'block',
whiteSpace: 'normal'
},
p: 0.5
}}
label={
<span>
<b>Measure</b>:{' '}
{
[
...evaluatorsOptions,
...numericOperators
].find((item) => item.name === ds?.measure)
?.label
}
</span>
}
/>
<Chip
variant='outlined'
size='small'
color='default'
sx={{
height: 'auto',
'& .MuiChip-label': {
display: 'block',
whiteSpace: 'normal'
},
p: 0.5
}}
label={
<span>
<b>Operator</b>:{' '}
{
[
...evaluatorsOptions,
...numericOperators
].find((item) => item.name === ds?.operator)
?.label
}
</span>
}
/>
<Chip
variant='outlined'
size='small'
color='default'
sx={{
height: 'auto',
'& .MuiChip-label': {
display: 'block',
whiteSpace: 'normal'
},
p: 0.5
}}
label={
<span>
<b>Value</b>: {ds?.value}
</span>
}
/>
</Stack>
)}
{ds?.type === 'text' && (
<Stack
flexDirection='row'
gap={1}
sx={{ alignItems: 'center', flexWrap: 'wrap' }}
>
<Chip
variant='outlined'
size='small'
color='default'
sx={{
height: 'auto',
'& .MuiChip-label': {
display: 'block',
whiteSpace: 'normal'
},
p: 0.5
}}
label={
<span>
<b>Operator</b>:{' '}
{
[
...evaluatorsOptions,
...numericOperators
].find((item) => item.name === ds?.operator)
?.label
}
</span>
}
/>
<Chip
variant='outlined'
size='small'
color='default'
sx={{
height: 'auto',
'& .MuiChip-label': {
display: 'block',
whiteSpace: 'normal'
},
p: 0.5
}}
label={
<span>
<b>Value</b>: {ds?.value}
</span>
}
/>
</Stack>
)}
{ds?.type === 'json' && (
<Stack
flexDirection='row'
gap={1}
sx={{ alignItems: 'center', flexWrap: 'wrap' }}
>
<Chip
variant='outlined'
size='small'
color='default'
sx={{
height: 'auto',
'& .MuiChip-label': {
display: 'block',
whiteSpace: 'normal'
},
p: 0.5
}}
label={
<span>
<b>Operator</b>:{' '}
{
[...evaluatorsOptions].find(
(item) => item.name === ds?.operator
)?.label
}
</span>
}
/>
</Stack>
)}
{ds?.type === 'llm' && (
<Stack
flexDirection='row'
gap={1}
sx={{ alignItems: 'center', flexWrap: 'wrap' }}
>
<Chip
variant='outlined'
size='small'
color='default'
sx={{
height: 'auto',
'& .MuiChip-label': {
display: 'block',
whiteSpace: 'normal'
},
p: 0.5
}}
label={
<span>
<b>Prompt</b>: {truncateString(ds?.prompt, 100)}
</span>
}
/>
<Chip
variant='outlined'
size='small'
color='default'
sx={{
height: 'auto',
'& .MuiChip-label': {
display: 'block',
whiteSpace: 'normal'
},
p: 0.5
}}
label={
<span>
<b>Output Schema Elements</b>:{' '}
{ds?.outputSchema.length > 0
? ds?.outputSchema
.map((item) => item.property)
.join(', ')
: 'None'}
</span>
}
/>
</Stack>
)}
</TableCell>
<TableCell onClick={() => edit(ds)}>
{moment(ds.updatedDate).format('MMMM Do YYYY, hh:mm A')}
</TableCell>
<TableCell>
<PermissionIconButton
permissionId={'evaluators:delete'}
title='Delete'
color='error'
onClick={() => deleteEvaluator(ds)}
>
<Chip
variant='outlined'
size='small'
color='default'
sx={{
height: 'auto',
'& .MuiChip-label': {
display: 'block',
whiteSpace: 'normal'
},
p: 0.5
}}
label={
<span>
<b>Measure</b>:{' '}
{
[...evaluatorsOptions, ...numericOperators].find(
(item) => item.name === ds?.measure
)?.label
}
</span>
}
/>
<Chip
variant='outlined'
size='small'
color='default'
sx={{
height: 'auto',
'& .MuiChip-label': {
display: 'block',
whiteSpace: 'normal'
},
p: 0.5
}}
label={
<span>
<b>Operator</b>:{' '}
{
[...evaluatorsOptions, ...numericOperators].find(
(item) => item.name === ds?.operator
)?.label
}
</span>
}
/>
<Chip
variant='outlined'
size='small'
color='default'
sx={{
height: 'auto',
'& .MuiChip-label': {
display: 'block',
whiteSpace: 'normal'
},
p: 0.5
}}
label={
<span>
<b>Value</b>: {ds?.value}
</span>
}
/>
</Stack>
)}
{ds?.type === 'text' && (
<Stack
flexDirection='row'
gap={1}
sx={{ alignItems: 'center', flexWrap: 'wrap' }}
>
<Chip
variant='outlined'
size='small'
color='default'
sx={{
height: 'auto',
'& .MuiChip-label': {
display: 'block',
whiteSpace: 'normal'
},
p: 0.5
}}
label={
<span>
<b>Operator</b>:{' '}
{
[...evaluatorsOptions, ...numericOperators].find(
(item) => item.name === ds?.operator
)?.label
}
</span>
}
/>
<Chip
variant='outlined'
size='small'
color='default'
sx={{
height: 'auto',
'& .MuiChip-label': {
display: 'block',
whiteSpace: 'normal'
},
p: 0.5
}}
label={
<span>
<b>Value</b>: {ds?.value}
</span>
}
/>
</Stack>
)}
{ds?.type === 'json' && (
<Stack
flexDirection='row'
gap={1}
sx={{ alignItems: 'center', flexWrap: 'wrap' }}
>
<Chip
variant='outlined'
size='small'
color='default'
sx={{
height: 'auto',
'& .MuiChip-label': {
display: 'block',
whiteSpace: 'normal'
},
p: 0.5
}}
label={
<span>
<b>Operator</b>:{' '}
{
[...evaluatorsOptions].find(
(item) => item.name === ds?.operator
)?.label
}
</span>
}
/>
</Stack>
)}
{ds?.type === 'llm' && (
<Stack
flexDirection='row'
gap={1}
sx={{ alignItems: 'center', flexWrap: 'wrap' }}
>
<Chip
variant='outlined'
size='small'
color='default'
sx={{
height: 'auto',
'& .MuiChip-label': {
display: 'block',
whiteSpace: 'normal'
},
p: 0.5
}}
label={
<span>
<b>Prompt</b>: {truncateString(ds?.prompt, 100)}
</span>
}
/>
<Chip
variant='outlined'
size='small'
color='default'
sx={{
height: 'auto',
'& .MuiChip-label': {
display: 'block',
whiteSpace: 'normal'
},
p: 0.5
}}
label={
<span>
<b>Output Schema Elements</b>:{' '}
{ds?.outputSchema.length > 0
? ds?.outputSchema
.map((item) => item.property)
.join(', ')
: 'None'}
</span>
}
/>
</Stack>
)}
</TableCell>
<TableCell onClick={() => edit(ds)}>
{moment(ds.updatedDate).format('MMMM Do YYYY, hh:mm A')}
</TableCell>
<TableCell>
<PermissionIconButton
permissionId={'evaluators:delete'}
title='Delete'
color='error'
onClick={() => deleteEvaluator(ds)}
>
<IconTrash />
</PermissionIconButton>
</TableCell>
</StyledTableRow>
</>
))}
</>
)}
</TableBody>
</Table>
</TableContainer>
<IconTrash />
</PermissionIconButton>
</TableCell>
</StyledTableRow>
</>
))}
</>
)}
</TableBody>
</Table>
</TableContainer>
{/* Pagination and Page Size Controls */}
<TablePagination currentPage={currentPage} limit={pageLimit} total={total} onChange={onChange} />
</>
)}
</Stack>
)}
+45 -17
View File
@@ -12,6 +12,7 @@ import ViewHeader from '@/layout/MainLayout/ViewHeader'
import ErrorBoundary from '@/ErrorBoundary'
import { ToolsTable } from '@/ui-component/table/ToolsListTable'
import { PermissionButton, StyledPermissionButton } from '@/ui-component/button/RBACButtons'
import TablePagination, { DEFAULT_ITEMS_PER_PAGE } from '@/ui-component/pagination/TablePagination'
// API
import toolsApi from '@/api/tools'
@@ -39,6 +40,25 @@ const Tools = () => {
const inputRef = useRef(null)
/* Table Pagination */
const [currentPage, setCurrentPage] = useState(1)
const [pageLimit, setPageLimit] = useState(DEFAULT_ITEMS_PER_PAGE)
const [total, setTotal] = useState(0)
const onChange = (page, pageLimit) => {
setCurrentPage(page)
setPageLimit(pageLimit)
refresh(page, pageLimit)
}
const refresh = (page, limit) => {
const params = {
page: page || currentPage,
limit: limit || pageLimit
}
getAllToolsApi.request(params)
}
const handleChange = (event, nextView) => {
if (nextView === null) return
localStorage.setItem('toolsDisplayStyle', nextView)
@@ -102,7 +122,7 @@ const Tools = () => {
const onConfirm = () => {
setShowDialog(false)
getAllToolsApi.request()
refresh(currentPage, pageLimit)
}
const [search, setSearch] = useState('')
@@ -117,8 +137,7 @@ const Tools = () => {
}
useEffect(() => {
getAllToolsApi.request()
refresh(currentPage, pageLimit)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
@@ -126,6 +145,12 @@ const Tools = () => {
setLoading(getAllToolsApi.loading)
}, [getAllToolsApi.loading])
useEffect(() => {
if (getAllToolsApi.data) {
setTotal(getAllToolsApi.data.total)
}
}, [getAllToolsApi.data])
return (
<>
<MainCard>
@@ -144,6 +169,7 @@ const Tools = () => {
sx={{ borderRadius: 2, maxHeight: 40 }}
value={view}
color='primary'
disabled={total === 0}
exclusive
onChange={handleChange}
>
@@ -203,27 +229,29 @@ const Tools = () => {
</StyledPermissionButton>
</ButtonGroup>
</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>
)}
{!isLoading && total > 0 && (
<>
{isLoading ? (
{!view || view === 'card' ? (
<Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
<Skeleton variant='rounded' height={160} />
<Skeleton variant='rounded' height={160} />
<Skeleton variant='rounded' height={160} />
{getAllToolsApi.data?.data?.filter(filterTools).map((data, index) => (
<ItemCard data={data} key={index} onClick={() => edit(data)} />
))}
</Box>
) : (
<Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
{getAllToolsApi.data &&
getAllToolsApi.data
?.filter(filterTools)
.map((data, index) => <ItemCard data={data} key={index} onClick={() => edit(data)} />)}
</Box>
<ToolsTable data={getAllToolsApi.data.data} isLoading={isLoading} onSelect={edit} />
)}
{/* Pagination and Page Size Controls */}
<TablePagination currentPage={currentPage} limit={pageLimit} total={total} onChange={onChange} />
</>
) : (
<ToolsTable data={getAllToolsApi.data} isLoading={isLoading} onSelect={edit} />
)}
{!isLoading && (!getAllToolsApi.data || getAllToolsApi.data.length === 0) && (
{!isLoading && total === 0 && (
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
<Box sx={{ p: 2, height: 'auto' }}>
<img
+165 -137
View File
@@ -33,6 +33,7 @@ import ErrorBoundary from '@/ErrorBoundary'
import { StyledPermissionButton } from '@/ui-component/button/RBACButtons'
import { Available } from '@/ui-component/rbac/available'
import { refreshVariablesCache } from '@/ui-component/input/suggestionOption'
import TablePagination, { DEFAULT_ITEMS_PER_PAGE } from '@/ui-component/pagination/TablePagination'
// API
import variablesApi from '@/api/variables'
@@ -91,8 +92,27 @@ const Variables = () => {
const { confirm } = useConfirm()
const getAllVariables = useApi(variablesApi.getAllVariables)
const [search, setSearch] = useState('')
/* Table Pagination */
const [currentPage, setCurrentPage] = useState(1)
const [pageLimit, setPageLimit] = useState(DEFAULT_ITEMS_PER_PAGE)
const [total, setTotal] = useState(0)
const onChange = (page, pageLimit) => {
setCurrentPage(page)
setPageLimit(pageLimit)
refresh(page, pageLimit)
}
const refresh = (page, limit) => {
const params = {
page: page || currentPage,
limit: limit || pageLimit
}
getAllVariables.request(params)
}
const onSearchChange = (event) => {
setSearch(event.target.value)
}
@@ -172,12 +192,12 @@ const Variables = () => {
const onConfirm = () => {
setShowVariableDialog(false)
getAllVariables.request()
refresh(currentPage, pageLimit)
refreshVariablesCache()
}
useEffect(() => {
getAllVariables.request()
refresh(currentPage, pageLimit)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
@@ -187,7 +207,8 @@ const Variables = () => {
useEffect(() => {
if (getAllVariables.data) {
setVariables(getAllVariables.data)
setVariables(getAllVariables.data.data)
setTotal(getAllVariables.data.total)
}
}, [getAllVariables.data])
@@ -231,162 +252,169 @@ const Variables = () => {
<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
}}
>
<TableRow>
<StyledTableCell>Name</StyledTableCell>
<StyledTableCell>Value</StyledTableCell>
<StyledTableCell>Type</StyledTableCell>
<StyledTableCell>Last Updated</StyledTableCell>
<StyledTableCell>Created</StyledTableCell>
<Available permissionId={'variables:update'}>
<StyledTableCell> </StyledTableCell>
</Available>
<Available permissionId={'variables:delete'}>
<StyledTableCell> </StyledTableCell>
</Available>
</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>
<Available permission={'variables:create,variables:update'}>
<>
<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>Name</StyledTableCell>
<StyledTableCell>Value</StyledTableCell>
<StyledTableCell>Type</StyledTableCell>
<StyledTableCell>Last Updated</StyledTableCell>
<StyledTableCell>Created</StyledTableCell>
<Available permissionId={'variables:update'}>
<StyledTableCell> </StyledTableCell>
</Available>
<Available permissionId={'variables:delete'}>
<StyledTableCell> </StyledTableCell>
</Available>
</TableRow>
</TableHead>
<TableBody>
{isLoading ? (
<>
<StyledTableRow>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
</Available>
<Available permission={'variables:delete'}>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
</Available>
</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>
<Available permission={'variables:create,variables:update'}>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
</Available>
<Available permission={'variables:delete'}>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
</Available>
</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 HH:mm:ss')}
</StyledTableCell>
<StyledTableCell>
{moment(variable.createdDate).format('MMMM Do, YYYY HH:mm:ss')}
<Skeleton variant='text' />
</StyledTableCell>
<Available permission={'variables:create,variables:update'}>
<StyledTableCell>
<IconButton title='Edit' color='primary' onClick={() => edit(variable)}>
<IconEdit />
</IconButton>
<Skeleton variant='text' />
</StyledTableCell>
</Available>
<Available permission={'variables:delete'}>
<StyledTableCell>
<IconButton
title='Delete'
color='error'
onClick={() => deleteVariable(variable)}
>
<IconTrash />
</IconButton>
<Skeleton variant='text' />
</StyledTableCell>
</Available>
</StyledTableRow>
))}
</>
)}
</TableBody>
</Table>
</TableContainer>
<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>
<Available permission={'variables:create,variables:update'}>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
</Available>
<Available permission={'variables:delete'}>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
</Available>
</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 HH:mm:ss')}
</StyledTableCell>
<StyledTableCell>
{moment(variable.createdDate).format('MMMM Do, YYYY HH:mm:ss')}
</StyledTableCell>
<Available permission={'variables:create,variables:update'}>
<StyledTableCell>
<IconButton title='Edit' color='primary' onClick={() => edit(variable)}>
<IconEdit />
</IconButton>
</StyledTableCell>
</Available>
<Available permission={'variables:delete'}>
<StyledTableCell>
<IconButton
title='Delete'
color='error'
onClick={() => deleteVariable(variable)}
>
<IconTrash />
</IconButton>
</StyledTableCell>
</Available>
</StyledTableRow>
))}
</>
)}
</TableBody>
</Table>
</TableContainer>
{/* Pagination and Page Size Controls */}
<TablePagination currentPage={currentPage} limit={pageLimit} total={total} onChange={onChange} />
</>
)}
</Stack>
)}