mirror of
https://github.com/FlowiseAI/Flowise.git
synced 2026-10-12 02:02:17 +00:00
feat/show scheduled flow status (#6330)
* show scheduled flow status * enhance ScheduleStatusBadge to display loading state and update tooltip text. Refactor schedule status retrieval in Agentflows to handle errors and improve state management.
This commit is contained in:
@@ -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 }) => {
|
||||
</span>
|
||||
)}
|
||||
</Box>
|
||||
{(images?.length > 0 || icons?.length > 0) && (
|
||||
{(images?.length > 0 || icons?.length > 0 || scheduleStatus?.isScheduled) && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'start',
|
||||
justifyContent: 'space-between',
|
||||
gap: 1
|
||||
}}
|
||||
>
|
||||
{[
|
||||
...(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) => (
|
||||
<Tooltip key={item.src || index} title={item.label} placement='top'>
|
||||
{item.type === 'image' ? (
|
||||
<Box
|
||||
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={item.src}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
width: 30,
|
||||
height: 30,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<item.icon size={25} color={item.color} />
|
||||
</div>
|
||||
)}
|
||||
</Tooltip>
|
||||
))}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
>
|
||||
{[
|
||||
...(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) => (
|
||||
<Tooltip key={item.src || index} title={item.label} placement='top'>
|
||||
{item.type === 'image' ? (
|
||||
<Box
|
||||
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={item.src}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
width: 30,
|
||||
height: 30,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<item.icon size={25} color={item.color} />
|
||||
</div>
|
||||
)}
|
||||
</Tooltip>
|
||||
))}
|
||||
|
||||
{(images?.length || 0) + (icons?.length || 0) > 3 && (
|
||||
<MoreItemsTooltip
|
||||
images={[
|
||||
...(images?.slice(3) || []),
|
||||
...(icons?.slice(Math.max(0, 3 - (images?.length || 0))) || []).map((ic) => ({ label: ic.name }))
|
||||
]}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
fontSize: '.9rem',
|
||||
fontWeight: 200
|
||||
}}
|
||||
{(images?.length || 0) + (icons?.length || 0) > 3 && (
|
||||
<MoreItemsTooltip
|
||||
images={[
|
||||
...(images?.slice(3) || []),
|
||||
...(icons?.slice(Math.max(0, 3 - (images?.length || 0))) || []).map((ic) => ({
|
||||
label: ic.name
|
||||
}))
|
||||
]}
|
||||
>
|
||||
+ {(images?.length || 0) + (icons?.length || 0) - 3} More
|
||||
</Typography>
|
||||
</MoreItemsTooltip>
|
||||
)}
|
||||
<Typography
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
fontSize: '.9rem',
|
||||
fontWeight: 200
|
||||
}}
|
||||
>
|
||||
+ {(images?.length || 0) + (icons?.length || 0) - 3} More
|
||||
</Typography>
|
||||
</MoreItemsTooltip>
|
||||
)}
|
||||
</Box>
|
||||
<ScheduleStatusBadge scheduleStatus={scheduleStatus} />
|
||||
</Box>
|
||||
)}
|
||||
</Grid>
|
||||
@@ -187,6 +202,7 @@ ItemCard.propTypes = {
|
||||
data: PropTypes.object,
|
||||
images: PropTypes.array,
|
||||
icons: PropTypes.array,
|
||||
scheduleStatus: PropTypes.object,
|
||||
onClick: PropTypes.func
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<Tooltip title={tooltipText} placement='top' arrow>
|
||||
<Box
|
||||
onClick={(e) => 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 ? (
|
||||
<CircularProgress size={dims.icon} thickness={5} sx={{ color: colors.text }} />
|
||||
) : isActive ? (
|
||||
<Box
|
||||
component='span'
|
||||
sx={{
|
||||
width: dims.dot,
|
||||
height: dims.dot,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: colors.dot,
|
||||
boxShadow: `0 0 0 0 ${colors.dot}`,
|
||||
animation: 'scheduleStatusPulse 1.8s ease-out infinite',
|
||||
'@keyframes scheduleStatusPulse': {
|
||||
'0%': { boxShadow: `0 0 0 0 ${colors.dot}99` },
|
||||
'70%': { boxShadow: `0 0 0 6px ${colors.dot}00` },
|
||||
'100%': { boxShadow: `0 0 0 0 ${colors.dot}00` }
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<IconClock size={dims.icon} stroke={2} />
|
||||
)}
|
||||
{isLoading ? 'Loading…' : isActive ? 'Scheduled' : 'Paused'}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
@@ -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) => (
|
||||
<StyledTableRow key={index}>
|
||||
<StyledTableCell key='0'>
|
||||
<Tooltip title={row.templateName || row.name}>
|
||||
<Typography
|
||||
sx={{
|
||||
display: '-webkit-box',
|
||||
fontSize: 14,
|
||||
fontWeight: 500,
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
>
|
||||
<Link to={onFlowClick(row)} style={{ color: '#2196f3', textDecoration: 'none' }}>
|
||||
{row.templateName || row.name}
|
||||
</Link>
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
<Stack direction='row' spacing={1} alignItems='center' sx={{ minWidth: 0 }}>
|
||||
<Tooltip title={row.templateName || row.name}>
|
||||
<Typography
|
||||
sx={{
|
||||
display: '-webkit-box',
|
||||
fontSize: 14,
|
||||
fontWeight: 500,
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
minWidth: 0
|
||||
}}
|
||||
>
|
||||
<Link to={onFlowClick(row)} style={{ color: '#2196f3', textDecoration: 'none' }}>
|
||||
{row.templateName || row.name}
|
||||
</Link>
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
<ScheduleStatusBadge scheduleStatus={scheduleStatuses[row.id]} size='sm' />
|
||||
</Stack>
|
||||
</StyledTableCell>
|
||||
<StyledTableCell key='1'>
|
||||
<div
|
||||
@@ -354,6 +360,7 @@ FlowListTable.propTypes = {
|
||||
data: PropTypes.array,
|
||||
images: PropTypes.object,
|
||||
icons: PropTypes.object,
|
||||
scheduleStatuses: PropTypes.object,
|
||||
isLoading: PropTypes.bool,
|
||||
filterFunction: PropTypes.func,
|
||||
updateFlowsApi: PropTypes.object,
|
||||
|
||||
@@ -41,6 +41,7 @@ const Agentflows = () => {
|
||||
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]}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
@@ -322,6 +364,7 @@ const Agentflows = () => {
|
||||
data={getAllAgentflows.data?.data}
|
||||
images={images}
|
||||
icons={icons}
|
||||
scheduleStatuses={scheduleStatuses}
|
||||
isLoading={isLoading}
|
||||
filterFunction={filterFlows}
|
||||
updateFlowsApi={getAllAgentflows}
|
||||
|
||||
Reference in New Issue
Block a user