Skip to content

Commit a177a14

Browse files
committed
improvement(schedules): use tanstack query to fetch schedule data, cleanup ui on schedule info component
1 parent d79696b commit a177a14

File tree

3 files changed

+282
-244
lines changed

3 files changed

+282
-244
lines changed

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/schedule-info/schedule-info.tsx

Lines changed: 68 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
1-
import { useCallback, useEffect, useState } from 'react'
2-
import { AlertTriangle } from 'lucide-react'
31
import { useParams } from 'next/navigation'
4-
import { createLogger } from '@/lib/logs/console/logger'
2+
import { Badge } from '@/components/emcn'
53
import { parseCronToHumanReadable } from '@/lib/workflows/schedules/utils'
4+
import { useRedeployWorkflowSchedule, useScheduleQuery } from '@/hooks/queries/schedules'
65
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
76

8-
const logger = createLogger('ScheduleStatus')
9-
107
interface ScheduleInfoProps {
118
blockId: string
129
isPreview?: boolean
@@ -20,172 +17,88 @@ interface ScheduleInfoProps {
2017
export function ScheduleInfo({ blockId, isPreview = false }: ScheduleInfoProps) {
2118
const params = useParams()
2219
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)
3120

3221
const scheduleTimezone = useSubBlockStore((state) => state.getValue(blockId, 'timezone'))
3322

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])
23+
const { data: schedule, isLoading } = useScheduleQuery(workflowId, blockId, {
24+
enabled: !isPreview,
25+
})
7126

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
27+
const redeployMutation = useRedeployWorkflowSchedule()
7828

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-
}
29+
const handleRedeploy = () => {
30+
if (isPreview || redeployMutation.isPending) return
31+
redeployMutation.mutate({ workflowId, blockId })
10032
}
10133

102-
// Don't render anything if there's no deployed schedule
103-
if (!hasSchedule && !isLoadingStatus) {
34+
if (!schedule || isLoading) {
10435
return null
10536
}
10637

38+
const timezone = scheduleTimezone || schedule?.timezone || 'UTC'
39+
const failedCount = schedule?.failedCount || 0
40+
const isDisabled = schedule?.status === 'disabled'
41+
const nextRunAt = schedule?.nextRunAt ? new Date(schedule.nextRunAt) : null
42+
10743
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-
) : (
44+
<div className='space-y-1.5'>
45+
{/* Status badges */}
46+
{(failedCount > 0 || isDisabled) && (
11547
<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()}
48+
<div className='flex flex-wrap items-center gap-2'>
49+
{failedCount >= 10 && isDisabled ? (
50+
<Badge
51+
variant='outline'
52+
className='cursor-pointer'
53+
style={{
54+
borderColor: 'var(--warning)',
55+
color: 'var(--warning)',
56+
}}
57+
onClick={handleRedeploy}
58+
>
59+
{redeployMutation.isPending ? 'redeploying...' : 'disabled'}
60+
</Badge>
61+
) : failedCount > 0 ? (
62+
<Badge
63+
variant='outline'
64+
style={{
65+
borderColor: 'var(--warning)',
66+
color: 'var(--warning)',
67+
}}
68+
>
69+
{failedCount} failed
70+
</Badge>
71+
) : null}
72+
</div>
73+
{failedCount >= 10 && isDisabled && (
74+
<p className='text-[12px] text-[var(--text-tertiary)]'>
75+
Disabled after 10 consecutive failures
15476
</p>
15577
)}
78+
</div>
79+
)}
15680

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'}
171-
</p>
81+
{/* Schedule info - only show when active */}
82+
{!isDisabled && (
83+
<div className='text-[12px] text-[var(--text-tertiary)]'>
84+
{schedule?.cronExpression && (
85+
<span>{parseCronToHumanReadable(schedule.cronExpression, timezone)}</span>
17286
)}
173-
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+
{nextRunAt && (
88+
<>
89+
{schedule?.cronExpression && <span className='mx-1'>·</span>}
90+
<span>
91+
Next:{' '}
92+
{nextRunAt.toLocaleString('en-US', {
93+
timeZone: timezone,
94+
month: 'short',
95+
day: 'numeric',
96+
hour: 'numeric',
97+
minute: '2-digit',
98+
hour12: true,
99+
})}
100+
</span>
101+
</>
189102
)}
190103
</div>
191104
)}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/hooks/use-schedule-info.ts

Lines changed: 30 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { useCallback, useEffect, useState } from 'react'
2-
import { createLogger } from '@/lib/logs/console/logger'
3-
import { parseCronToHumanReadable } from '@/lib/workflows/schedules/utils'
1+
import { useCallback } from 'react'
2+
import {
3+
useReactivateSchedule,
4+
useScheduleInfo as useScheduleInfoQuery,
5+
} from '@/hooks/queries/schedules'
46
import type { ScheduleInfo } from '../types'
57

6-
const logger = createLogger('useScheduleInfo')
7-
88
/**
99
* Return type for the useScheduleInfo hook
1010
*/
@@ -18,7 +18,7 @@ export interface UseScheduleInfoReturn {
1818
}
1919

2020
/**
21-
* Custom hook for fetching schedule information
21+
* Custom hook for fetching schedule information using TanStack Query
2222
*
2323
* @param blockId - The ID of the block
2424
* @param blockType - The type of the block
@@ -30,96 +30,37 @@ export function useScheduleInfo(
3030
blockType: string,
3131
workflowId: string
3232
): UseScheduleInfoReturn {
33-
const [isLoading, setIsLoading] = useState(false)
34-
const [scheduleInfo, setScheduleInfo] = useState<ScheduleInfo | null>(null)
35-
36-
const fetchScheduleInfo = useCallback(
37-
async (wfId: string) => {
38-
if (!wfId) return
39-
40-
try {
41-
setIsLoading(true)
42-
43-
const params = new URLSearchParams({
44-
workflowId: wfId,
45-
blockId,
46-
})
47-
48-
const response = await fetch(`/api/schedules?${params}`, {
49-
cache: 'no-store',
50-
headers: { 'Cache-Control': 'no-cache' },
51-
})
52-
53-
if (!response.ok) {
54-
setScheduleInfo(null)
55-
return
56-
}
57-
58-
const data = await response.json()
59-
60-
if (!data.schedule) {
61-
setScheduleInfo(null)
62-
return
63-
}
64-
65-
const schedule = data.schedule
66-
const scheduleTimezone = schedule.timezone || 'UTC'
67-
68-
setScheduleInfo({
69-
scheduleTiming: schedule.cronExpression
70-
? parseCronToHumanReadable(schedule.cronExpression, scheduleTimezone)
71-
: 'Unknown schedule',
72-
nextRunAt: schedule.nextRunAt,
73-
lastRanAt: schedule.lastRanAt,
74-
timezone: scheduleTimezone,
75-
status: schedule.status,
76-
isDisabled: schedule.status === 'disabled',
77-
failedCount: schedule.failedCount || 0,
78-
id: schedule.id,
79-
})
80-
} catch (error) {
81-
logger.error('Error fetching schedule info:', error)
82-
setScheduleInfo(null)
83-
} finally {
84-
setIsLoading(false)
85-
}
86-
},
87-
[blockId]
33+
const { scheduleInfo: queryScheduleInfo, isLoading } = useScheduleInfoQuery(
34+
workflowId,
35+
blockId,
36+
blockType
8837
)
8938

39+
const reactivateMutation = useReactivateSchedule()
40+
9041
const reactivateSchedule = useCallback(
9142
async (scheduleId: string) => {
92-
try {
93-
const response = await fetch(`/api/schedules/${scheduleId}`, {
94-
method: 'PUT',
95-
headers: { 'Content-Type': 'application/json' },
96-
body: JSON.stringify({ action: 'reactivate' }),
97-
})
98-
99-
if (response.ok && workflowId) {
100-
await fetchScheduleInfo(workflowId)
101-
} else {
102-
logger.error('Failed to reactivate schedule')
103-
}
104-
} catch (error) {
105-
logger.error('Error reactivating schedule:', error)
106-
}
43+
await reactivateMutation.mutateAsync({
44+
scheduleId,
45+
workflowId,
46+
blockId,
47+
})
10748
},
108-
[workflowId, fetchScheduleInfo]
49+
[reactivateMutation, workflowId, blockId]
10950
)
11051

111-
useEffect(() => {
112-
if (blockType === 'schedule' && workflowId) {
113-
fetchScheduleInfo(workflowId)
114-
} else {
115-
setScheduleInfo(null)
116-
setIsLoading(false)
117-
}
118-
119-
return () => {
120-
setIsLoading(false)
121-
}
122-
}, [blockType, workflowId, fetchScheduleInfo])
52+
const scheduleInfo: ScheduleInfo | null = queryScheduleInfo
53+
? {
54+
scheduleTiming: queryScheduleInfo.scheduleTiming,
55+
nextRunAt: queryScheduleInfo.nextRunAt,
56+
lastRanAt: queryScheduleInfo.lastRanAt,
57+
timezone: queryScheduleInfo.timezone,
58+
status: queryScheduleInfo.status,
59+
isDisabled: queryScheduleInfo.isDisabled,
60+
failedCount: queryScheduleInfo.failedCount,
61+
id: queryScheduleInfo.id,
62+
}
63+
: null
12364

12465
return {
12566
scheduleInfo,

0 commit comments

Comments
 (0)