diff --git a/packages/ui/src/ui-component/cards/ItemCard.jsx b/packages/ui/src/ui-component/cards/ItemCard.jsx index 53b7e6c74..fa113ee22 100644 --- a/packages/ui/src/ui-component/cards/ItemCard.jsx +++ b/packages/ui/src/ui-component/cards/ItemCard.jsx @@ -8,6 +8,7 @@ import { Box, Grid, Tooltip, Typography, useTheme } from '@mui/material' // project imports import MainCard from '@/ui-component/cards/MainCard' import MoreItemsTooltip from '../tooltip/MoreItemsTooltip' +import ScheduleStatusBadge from '@/ui-component/extended/ScheduleStatusBadge' const CardWrapper = styled(MainCard)(({ theme }) => ({ background: theme.palette.card.main, @@ -30,7 +31,7 @@ const CardWrapper = styled(MainCard)(({ theme }) => ({ // ===========================|| CONTRACT CARD ||=========================== // -const ItemCard = ({ data, images, icons, onClick }) => { +const ItemCard = ({ data, images, icons, scheduleStatus, onClick }) => { const theme = useTheme() const customization = useSelector((state) => state.customization) @@ -107,74 +108,88 @@ const ItemCard = ({ data, images, icons, onClick }) => { )} - {(images?.length > 0 || icons?.length > 0) && ( + {(images?.length > 0 || icons?.length > 0 || scheduleStatus?.isScheduled) && ( - {[ - ...(images || []).map((img) => ({ type: 'image', src: img.imageSrc, label: img.label })), - ...(icons || []).map((ic) => ({ type: 'icon', icon: ic.icon, color: ic.color, label: ic.name })) - ] - .slice(0, 3) - .map((item, index) => ( - - {item.type === 'image' ? ( - - - - ) : ( -
- -
- )} -
- ))} + + {[ + ...(images || []).map((img) => ({ type: 'image', src: img.imageSrc, label: img.label })), + ...(icons || []).map((ic) => ({ type: 'icon', icon: ic.icon, color: ic.color, label: ic.name })) + ] + .slice(0, 3) + .map((item, index) => ( + + {item.type === 'image' ? ( + + + + ) : ( +
+ +
+ )} +
+ ))} - {(images?.length || 0) + (icons?.length || 0) > 3 && ( - ({ label: ic.name })) - ]} - > - 3 && ( + ({ + label: ic.name + })) + ]} > - + {(images?.length || 0) + (icons?.length || 0) - 3} More - - - )} + + + {(images?.length || 0) + (icons?.length || 0) - 3} More + + + )} +
+
)} @@ -187,6 +202,7 @@ ItemCard.propTypes = { data: PropTypes.object, images: PropTypes.array, icons: PropTypes.array, + scheduleStatus: PropTypes.object, onClick: PropTypes.func } diff --git a/packages/ui/src/ui-component/extended/ScheduleStatusBadge.jsx b/packages/ui/src/ui-component/extended/ScheduleStatusBadge.jsx new file mode 100644 index 000000000..dc2439812 --- /dev/null +++ b/packages/ui/src/ui-component/extended/ScheduleStatusBadge.jsx @@ -0,0 +1,105 @@ +import PropTypes from 'prop-types' +import moment from 'moment' +import { useSelector } from 'react-redux' +import { Box, CircularProgress, Tooltip } from '@mui/material' +import { useTheme } from '@mui/material/styles' +import { IconClock } from '@tabler/icons-react' + +const ACTIVE = { + light: { bg: '#dcfce7', text: '#15803d', border: '#86efac', dot: '#22c55e' }, + dark: { bg: 'rgba(34, 197, 94, 0.16)', text: '#86efac', border: 'rgba(134, 239, 172, 0.35)', dot: '#22c55e' } +} + +const PAUSED = { + light: { bg: 'rgba(0, 0, 0, 0.04)', text: 'rgba(0, 0, 0, 0.6)', border: 'rgba(0, 0, 0, 0.12)' }, + dark: { bg: 'rgba(255, 255, 255, 0.06)', text: 'rgba(255, 255, 255, 0.65)', border: 'rgba(255, 255, 255, 0.18)' } +} + +const ScheduleStatusBadge = ({ scheduleStatus, size = 'md' }) => { + const theme = useTheme() + const customization = useSelector((state) => state.customization) + + if (!scheduleStatus?.isScheduled) return null + + const isLoading = scheduleStatus.loading === true + const isActive = !isLoading && scheduleStatus.enabled === true + const palette = customization.isDarkMode ? 'dark' : 'light' + const colors = isActive ? ACTIVE[palette] : PAUSED[palette] + + const tooltipText = isLoading + ? 'Checking schedule status…' + : isActive + ? scheduleStatus.nextRunAt + ? `Schedule active — next run ${moment(scheduleStatus.nextRunAt).format('MMM D, YYYY h:mm A')}` + : 'Schedule active' + : 'Schedule configured but turned off' + + const dims = + size === 'sm' + ? { px: 0.875, py: 0.25, fontSize: '0.65rem', dot: 5, icon: 10, gap: 0.625 } + : { px: 1, py: 0.375, fontSize: '0.7rem', dot: 6, icon: 11, gap: 0.75 } + + return ( + + e.stopPropagation()} + sx={{ + display: 'inline-flex', + alignItems: 'center', + gap: dims.gap, + px: dims.px, + py: dims.py, + borderRadius: '999px', + fontSize: dims.fontSize, + fontWeight: 600, + letterSpacing: '0.02em', + lineHeight: 1, + color: colors.text, + backgroundColor: colors.bg, + border: `1px solid ${colors.border}`, + cursor: 'default', + userSelect: 'none', + flexShrink: 0, + transition: theme.transitions.create(['background-color', 'border-color']) + }} + > + {isLoading ? ( + + ) : isActive ? ( + + ) : ( + + )} + {isLoading ? 'Loading…' : isActive ? 'Scheduled' : 'Paused'} + + + ) +} + +ScheduleStatusBadge.propTypes = { + scheduleStatus: PropTypes.shape({ + isScheduled: PropTypes.bool, + enabled: PropTypes.bool, + nextRunAt: PropTypes.string, + cronExpression: PropTypes.string, + loading: PropTypes.bool + }), + size: PropTypes.oneOf(['sm', 'md']) +} + +export default ScheduleStatusBadge diff --git a/packages/ui/src/ui-component/table/FlowListTable.jsx b/packages/ui/src/ui-component/table/FlowListTable.jsx index 2d1e05704..aa7e6daa9 100644 --- a/packages/ui/src/ui-component/table/FlowListTable.jsx +++ b/packages/ui/src/ui-component/table/FlowListTable.jsx @@ -26,6 +26,7 @@ import { Link } from 'react-router-dom' import { useAuth } from '@/hooks/useAuth' import MoreItemsTooltip from '../tooltip/MoreItemsTooltip' +import ScheduleStatusBadge from '@/ui-component/extended/ScheduleStatusBadge' const StyledTableCell = styled(TableCell)(({ theme }) => ({ borderColor: theme.palette.grey[900] + 25, @@ -54,6 +55,7 @@ export const FlowListTable = ({ data, images = {}, icons = {}, + scheduleStatuses = {}, isLoading, filterFunction, updateFlowsApi, @@ -191,23 +193,27 @@ export const FlowListTable = ({ {sortedData.filter(filterFunction).map((row, index) => ( - - - - {row.templateName || row.name} - - - + + + + + {row.templateName || row.name} + + + + +
{ const [isLoading, setLoading] = useState(true) const [images, setImages] = useState({}) const [icons, setIcons] = useState({}) + const [scheduleStatuses, setScheduleStatuses] = useState({}) const [search, setSearch] = useState('') const { error, setError } = useError() @@ -139,30 +140,70 @@ const Agentflows = () => { setTotal(getAllAgentflows.data?.total) const images = {} const icons = {} + const scheduleConfiguredIds = [] for (let i = 0; i < agentflows.length; i += 1) { const flowDataStr = agentflows[i].flowData const flowData = JSON.parse(flowDataStr) const nodes = flowData.nodes || [] images[agentflows[i].id] = [] icons[agentflows[i].id] = [] + let isScheduleFlow = false for (let j = 0; j < nodes.length; j += 1) { - if (nodes[j].data.name === 'stickyNote' || nodes[j].data.name === 'stickyNoteAgentflow') continue - const foundIcon = AGENTFLOW_ICONS.find((icon) => icon.name === nodes[j].data.name) + const node = nodes[j] + if (node.data?.name === 'startAgentflow' && node.data?.inputs?.startInputType === 'scheduleInput') { + isScheduleFlow = true + } + if (node.data.name === 'stickyNote' || node.data.name === 'stickyNoteAgentflow') continue + const foundIcon = AGENTFLOW_ICONS.find((icon) => icon.name === node.data.name) if (foundIcon) { icons[agentflows[i].id].push(foundIcon) } else { - const imageSrc = `${baseURL}/api/v1/node-icon/${nodes[j].data.name}` + const imageSrc = `${baseURL}/api/v1/node-icon/${node.data.name}` if (!images[agentflows[i].id].some((img) => img.imageSrc === imageSrc)) { images[agentflows[i].id].push({ imageSrc, - label: nodes[j].data.label + label: node.data.label }) } } } + if (isScheduleFlow) scheduleConfiguredIds.push(agentflows[i].id) } setImages(images) setIcons(icons) + + const initialStatuses = {} + scheduleConfiguredIds.forEach((id) => { + initialStatuses[id] = { isScheduled: true, enabled: false, loading: true } + }) + setScheduleStatuses(initialStatuses) + + if (scheduleConfiguredIds.length > 0) { + Promise.all( + scheduleConfiguredIds.map((id) => + chatflowsApi + .getScheduleStatus(id) + .then((res) => ({ id, data: res.data })) + .catch(() => ({ id, error: true })) + ) + ).then((results) => { + setScheduleStatuses((prev) => { + const next = { ...prev } + results.forEach(({ id, data }) => { + if (next[id]) { + next[id] = { + ...next[id], + enabled: data?.enabled === true, + nextRunAt: data?.record?.nextRunAt || null, + cronExpression: data?.record?.cronExpression || null, + loading: false + } + } + }) + return next + }) + }) + } } catch (e) { console.error(e) } @@ -312,6 +353,7 @@ const Agentflows = () => { data={data} images={images[data.id]} icons={icons[data.id]} + scheduleStatus={scheduleStatuses[data.id]} /> ))} @@ -322,6 +364,7 @@ const Agentflows = () => { data={getAllAgentflows.data?.data} images={images} icons={icons} + scheduleStatuses={scheduleStatuses} isLoading={isLoading} filterFunction={filterFlows} updateFlowsApi={getAllAgentflows}