1- import { useCallback , useEffect , useState } from 'react'
2- import { AlertTriangle } from 'lucide-react'
31import { useParams } from 'next/navigation'
4- import { createLogger } from '@/lib/logs/console/logger '
2+ import { Badge } from '@/components/emcn '
53import { parseCronToHumanReadable } from '@/lib/workflows/schedules/utils'
4+ import { useRedeployWorkflowSchedule , useScheduleQuery } from '@/hooks/queries/schedules'
65import { useSubBlockStore } from '@/stores/workflows/subblock/store'
7-
8- const logger = createLogger ( 'ScheduleStatus' )
6+ import { MAX_CONSECUTIVE_FAILURES } from '@/triggers/constants'
97
108interface ScheduleInfoProps {
119 blockId : string
@@ -20,172 +18,93 @@ interface ScheduleInfoProps {
2018export function ScheduleInfo ( { blockId, isPreview = false } : ScheduleInfoProps ) {
2119 const params = useParams ( )
2220 const workflowId = params . workflowId as string
23- const [ scheduleStatus , setScheduleStatus ] = useState < 'active' | 'disabled' | null > ( null )
24- const [ nextRunAt , setNextRunAt ] = useState < Date | null > ( null )
25- const [ lastRanAt , setLastRanAt ] = useState < Date | null > ( null )
26- const [ failedCount , setFailedCount ] = useState < number > ( 0 )
27- const [ isLoadingStatus , setIsLoadingStatus ] = useState ( true )
28- const [ savedCronExpression , setSavedCronExpression ] = useState < string | null > ( null )
29- const [ isRedeploying , setIsRedeploying ] = useState ( false )
30- const [ hasSchedule , setHasSchedule ] = useState ( false )
3121
3222 const scheduleTimezone = useSubBlockStore ( ( state ) => state . getValue ( blockId , 'timezone' ) )
3323
34- const fetchScheduleStatus = useCallback ( async ( ) => {
35- if ( isPreview ) return
36-
37- setIsLoadingStatus ( true )
38- try {
39- const response = await fetch ( `/api/schedules?workflowId=${ workflowId } &blockId=${ blockId } ` )
40- if ( response . ok ) {
41- const data = await response . json ( )
42- if ( data . schedule ) {
43- setHasSchedule ( true )
44- setScheduleStatus ( data . schedule . status )
45- setNextRunAt ( data . schedule . nextRunAt ? new Date ( data . schedule . nextRunAt ) : null )
46- setLastRanAt ( data . schedule . lastRanAt ? new Date ( data . schedule . lastRanAt ) : null )
47- setFailedCount ( data . schedule . failedCount || 0 )
48- setSavedCronExpression ( data . schedule . cronExpression || null )
49- } else {
50- // No schedule exists (workflow not deployed or no schedule block)
51- setHasSchedule ( false )
52- setScheduleStatus ( null )
53- setNextRunAt ( null )
54- setLastRanAt ( null )
55- setFailedCount ( 0 )
56- setSavedCronExpression ( null )
57- }
58- }
59- } catch ( error ) {
60- logger . error ( 'Error fetching schedule status' , { error } )
61- } finally {
62- setIsLoadingStatus ( false )
63- }
64- } , [ workflowId , blockId , isPreview ] )
65-
66- useEffect ( ( ) => {
67- if ( ! isPreview ) {
68- fetchScheduleStatus ( )
69- }
70- } , [ isPreview , fetchScheduleStatus ] )
24+ const { data : schedule , isLoading } = useScheduleQuery ( workflowId , blockId , {
25+ enabled : ! isPreview ,
26+ } )
7127
72- /**
73- * Handles redeploying the workflow when schedule is disabled due to failures.
74- * Redeploying will recreate the schedule with reset failure count.
75- */
76- const handleRedeploy = async ( ) => {
77- if ( isPreview || isRedeploying ) return
28+ const redeployMutation = useRedeployWorkflowSchedule ( )
7829
79- setIsRedeploying ( true )
80- try {
81- const response = await fetch ( `/api/workflows/${ workflowId } /deploy` , {
82- method : 'POST' ,
83- headers : { 'Content-Type' : 'application/json' } ,
84- body : JSON . stringify ( { deployChatEnabled : false } ) ,
85- } )
86-
87- if ( response . ok ) {
88- // Refresh schedule status after redeploy
89- await fetchScheduleStatus ( )
90- logger . info ( 'Workflow redeployed successfully to reset schedule' , { workflowId, blockId } )
91- } else {
92- const errorData = await response . json ( )
93- logger . error ( 'Failed to redeploy workflow' , { error : errorData . error } )
94- }
95- } catch ( error ) {
96- logger . error ( 'Error redeploying workflow' , { error } )
97- } finally {
98- setIsRedeploying ( false )
99- }
30+ const handleRedeploy = ( ) => {
31+ if ( isPreview || redeployMutation . isPending ) return
32+ redeployMutation . mutate ( { workflowId, blockId } )
10033 }
10134
102- // Don't render anything if there's no deployed schedule
103- if ( ! hasSchedule && ! isLoadingStatus ) {
35+ if ( ! schedule || isLoading ) {
10436 return null
10537 }
10638
39+ const timezone = scheduleTimezone || schedule ?. timezone || 'UTC'
40+ const failedCount = schedule ?. failedCount || 0
41+ const isDisabled = schedule ?. status === 'disabled'
42+ const nextRunAt = schedule ?. nextRunAt ? new Date ( schedule . nextRunAt ) : null
43+
10744 return (
108- < div className = 'mt-2' >
109- { isLoadingStatus ? (
110- < div className = 'flex items-center gap-2 text-muted-foreground text-sm' >
111- < div className = 'h-4 w-4 animate-spin rounded-full border-[1.5px] border-current border-t-transparent' />
112- Loading schedule status...
113- </ div >
114- ) : (
45+ < div className = 'space-y-1.5' >
46+ { /* Status badges */ }
47+ { ( failedCount > 0 || isDisabled ) && (
11548 < div className = 'space-y-1' >
116- { /* Failure badge with redeploy action */ }
117- { failedCount >= 10 && scheduleStatus === 'disabled' && (
118- < button
119- type = 'button'
120- onClick = { handleRedeploy }
121- disabled = { isRedeploying }
122- className = 'flex w-full cursor-pointer items-center gap-2 rounded-md bg-destructive/10 px-3 py-2 text-left text-destructive text-sm transition-colors hover:bg-destructive/20 disabled:cursor-not-allowed disabled:opacity-50'
123- >
124- { isRedeploying ? (
125- < div className = 'h-4 w-4 animate-spin rounded-full border-[1.5px] border-current border-t-transparent' />
126- ) : (
127- < AlertTriangle className = 'h-4 w-4 flex-shrink-0' />
128- ) }
129- < span >
130- { isRedeploying
131- ? 'Redeploying...'
132- : `Schedule disabled after ${ failedCount } failures - Click to redeploy` }
133- </ span >
134- </ button >
135- ) }
136-
137- { /* Show warning for failed runs under threshold */ }
138- { failedCount > 0 && failedCount < 10 && (
139- < div className = 'flex items-center gap-2' >
140- < span className = 'text-destructive text-sm' >
141- ⚠️ { failedCount } failed run{ failedCount !== 1 ? 's' : '' }
142- </ span >
143- </ div >
144- ) }
145-
146- { /* Cron expression human-readable description */ }
147- { savedCronExpression && (
148- < p className = 'text-muted-foreground text-sm' >
149- Runs{ ' ' }
150- { parseCronToHumanReadable (
151- savedCronExpression ,
152- scheduleTimezone || 'UTC'
153- ) . toLowerCase ( ) }
49+ < div className = 'flex flex-wrap items-center gap-2' >
50+ { failedCount >= MAX_CONSECUTIVE_FAILURES && isDisabled ? (
51+ < Badge
52+ variant = 'outline'
53+ className = 'cursor-pointer'
54+ style = { {
55+ borderColor : 'var(--warning)' ,
56+ color : 'var(--warning)' ,
57+ } }
58+ onClick = { handleRedeploy }
59+ >
60+ { redeployMutation . isPending ? 'redeploying...' : 'disabled' }
61+ </ Badge >
62+ ) : failedCount > 0 ? (
63+ < Badge
64+ variant = 'outline'
65+ style = { {
66+ borderColor : 'var(--warning)' ,
67+ color : 'var(--warning)' ,
68+ } }
69+ >
70+ { failedCount } failed
71+ </ Badge >
72+ ) : null }
73+ </ div >
74+ { failedCount >= MAX_CONSECUTIVE_FAILURES && isDisabled && (
75+ < p className = 'text-[12px] text-[var(--text-tertiary)]' >
76+ Disabled after { MAX_CONSECUTIVE_FAILURES } consecutive failures
15477 </ p >
15578 ) }
156-
157- { /* Next run time */ }
158- { nextRunAt && (
159- < p className = 'text-sm' >
160- < span className = 'font-medium' > Next run:</ span > { ' ' }
161- { nextRunAt . toLocaleString ( 'en-US' , {
162- timeZone : scheduleTimezone || 'UTC' ,
163- year : 'numeric' ,
164- month : 'numeric' ,
165- day : 'numeric' ,
166- hour : 'numeric' ,
167- minute : '2-digit' ,
168- hour12 : true ,
169- } ) } { ' ' }
170- { scheduleTimezone || 'UTC' }
79+ { redeployMutation . isError && (
80+ < p className = 'text-[12px] text-[var(--text-error)]' >
81+ Failed to redeploy. Please try again.
17182 </ p >
17283 ) }
84+ </ div >
85+ ) }
17386
174- { /* Last ran time */ }
175- { lastRanAt && (
176- < p className = 'text-muted-foreground text-sm' >
177- < span className = 'font-medium' > Last ran:</ span > { ' ' }
178- { lastRanAt . toLocaleString ( 'en-US' , {
179- timeZone : scheduleTimezone || 'UTC' ,
180- year : 'numeric' ,
181- month : 'numeric' ,
182- day : 'numeric' ,
183- hour : 'numeric' ,
184- minute : '2-digit' ,
185- hour12 : true ,
186- } ) } { ' ' }
187- { scheduleTimezone || 'UTC' }
188- </ p >
87+ { /* Schedule info - only show when active */ }
88+ { ! isDisabled && (
89+ < div className = 'text-[12px] text-[var(--text-tertiary)]' >
90+ { schedule ?. cronExpression && (
91+ < span > { parseCronToHumanReadable ( schedule . cronExpression , timezone ) } </ span >
92+ ) }
93+ { nextRunAt && (
94+ < >
95+ { schedule ?. cronExpression && < span className = 'mx-1' > ·</ span > }
96+ < span >
97+ Next:{ ' ' }
98+ { nextRunAt . toLocaleString ( 'en-US' , {
99+ timeZone : timezone ,
100+ month : 'short' ,
101+ day : 'numeric' ,
102+ hour : 'numeric' ,
103+ minute : '2-digit' ,
104+ hour12 : true ,
105+ } ) }
106+ </ span >
107+ </ >
189108 ) }
190109 </ div >
191110 ) }
0 commit comments