Skip to content

Commit 3201aba

Browse files
authored
improvement(schedules): use tanstack query to fetch schedule data, cleanup ui on schedule info component (#2584)
* improvement(schedules): use tanstack query to fetch schedule data, cleanup ui on schedule info component * update trigger-save UI, increase auto disable to 100 consecutive from 10 * updated docs * consolidate consts
1 parent d79696b commit 3201aba

File tree

16 files changed

+336
-307
lines changed

16 files changed

+336
-307
lines changed

apps/docs/content/docs/de/triggers/schedule.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ Sie müssen Ihren Workflow bereitstellen, damit der Zeitplan mit der Ausführung
5656

5757
## Automatische Deaktivierung
5858

59-
Zeitpläne werden nach **10 aufeinanderfolgenden Fehlschlägen** automatisch deaktiviert, um unkontrollierte Fehler zu verhindern. Bei Deaktivierung:
59+
Zeitpläne werden nach **100 aufeinanderfolgenden Fehlschlägen** automatisch deaktiviert, um unkontrollierte Fehler zu verhindern. Bei Deaktivierung:
6060

6161
- Erscheint ein Warnhinweis auf dem Zeitplan-Block
6262
- Die Ausführung des Zeitplans wird gestoppt

apps/docs/content/docs/en/triggers/schedule.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ You must deploy your workflow for the schedule to start running. Configure the s
5656

5757
## Automatic Disabling
5858

59-
Schedules automatically disable after **10 consecutive failures** to prevent runaway errors. When disabled:
59+
Schedules automatically disable after **100 consecutive failures** to prevent runaway errors. When disabled:
6060

6161
- A warning badge appears on the schedule block
6262
- The schedule stops executing

apps/docs/content/docs/es/triggers/schedule.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ Debes desplegar tu flujo de trabajo para que la programación comience a ejecuta
5656

5757
## Desactivación automática
5858

59-
Las programaciones se desactivan automáticamente después de **10 fallos consecutivos** para evitar errores descontrolados. Cuando se desactiva:
59+
Las programaciones se desactivan automáticamente después de **100 fallos consecutivos** para evitar errores descontrolados. Cuando se desactiva:
6060

6161
- Aparece una insignia de advertencia en el bloque de programación
6262
- La programación deja de ejecutarse

apps/docs/content/docs/fr/triggers/schedule.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ Vous devez déployer votre workflow pour que la planification commence à s'exé
5656

5757
## Désactivation automatique
5858

59-
Les planifications se désactivent automatiquement après **10 échecs consécutifs** pour éviter les erreurs incontrôlées. Lorsqu'elle est désactivée :
59+
Les planifications se désactivent automatiquement après **100 échecs consécutifs** pour éviter les erreurs incontrôlées. Lorsqu'elle est désactivée :
6060

6161
- Un badge d'avertissement apparaît sur le bloc de planification
6262
- La planification cesse de s'exécuter

apps/docs/content/docs/ja/triggers/schedule.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ import { Image } from '@/components/ui/image'
5656

5757
## 自動無効化
5858

59-
スケジュールは**10回連続で失敗**すると、エラーの連鎖を防ぐため自動的に無効化されます。無効化されると:
59+
スケジュールは**100回連続で失敗**すると、エラーの連鎖を防ぐため自動的に無効化されます。無効化されると:
6060

6161
- スケジュールブロックに警告バッジが表示されます
6262
- スケジュールの実行が停止します

apps/docs/content/docs/zh/triggers/schedule.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ import { Image } from '@/components/ui/image'
5656

5757
## 自动禁用
5858

59-
计划在连续 **10 次失败** 后会自动禁用,以防止错误持续发生。禁用后:
59+
计划在连续 **100 次失败** 后会自动禁用,以防止错误持续发生。禁用后:
6060

6161
- 计划块上会显示警告徽章
6262
- 计划将停止执行

apps/sim/app/api/schedules/route.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ describe('Schedule GET API', () => {
144144
it('indicates disabled schedule with failures', async () => {
145145
mockDbChain([
146146
[{ userId: 'user-1', workspaceId: null }],
147-
[{ id: 'sched-1', status: 'disabled', failedCount: 10 }],
147+
[{ id: 'sched-1', status: 'disabled', failedCount: 100 }],
148148
])
149149

150150
const res = await GET(createRequest('http://test/api/schedules?workflowId=wf-1'))

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

Lines changed: 73 additions & 154 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +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'
7-
8-
const logger = createLogger('ScheduleStatus')
6+
import { MAX_CONSECUTIVE_FAILURES } from '@/triggers/constants'
97

108
interface ScheduleInfoProps {
119
blockId: string
@@ -20,172 +18,93 @@ interface ScheduleInfoProps {
2018
export 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

Comments
 (0)