@gravito/zenith 1.1.2 → 1.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +95 -22
  2. package/README.zh-TW.md +88 -0
  3. package/dist/bin.js +54699 -39316
  4. package/dist/client/assets/index-C80c1frR.css +1 -0
  5. package/dist/client/assets/index-CrWem9u3.js +434 -0
  6. package/dist/client/index.html +2 -2
  7. package/dist/server/index.js +54699 -39316
  8. package/package.json +20 -9
  9. package/CHANGELOG.md +0 -47
  10. package/Dockerfile +0 -46
  11. package/Dockerfile.demo-worker +0 -29
  12. package/ECOSYSTEM_EXPANSION_RFC.md +0 -130
  13. package/bin/flux-console.ts +0 -2
  14. package/dist/client/assets/index-BSMp8oq_.js +0 -436
  15. package/dist/client/assets/index-BwxlHx-_.css +0 -1
  16. package/docker-compose.yml +0 -40
  17. package/docs/ALERTING_GUIDE.md +0 -71
  18. package/docs/DEPLOYMENT.md +0 -157
  19. package/docs/DOCS_INTERNAL.md +0 -73
  20. package/docs/LARAVEL_ZENITH_ROADMAP.md +0 -109
  21. package/docs/QUASAR_MASTER_PLAN.md +0 -140
  22. package/docs/QUICK_TEST_GUIDE.md +0 -72
  23. package/docs/ROADMAP.md +0 -85
  24. package/docs/integrations/LARAVEL.md +0 -207
  25. package/postcss.config.js +0 -6
  26. package/scripts/debug_redis_keys.ts +0 -24
  27. package/scripts/flood-logs.ts +0 -21
  28. package/scripts/seed.ts +0 -213
  29. package/scripts/verify-throttle.ts +0 -49
  30. package/scripts/worker.ts +0 -124
  31. package/specs/PULSE_SPEC.md +0 -86
  32. package/src/bin.ts +0 -6
  33. package/src/client/App.tsx +0 -72
  34. package/src/client/Layout.tsx +0 -672
  35. package/src/client/Sidebar.tsx +0 -112
  36. package/src/client/ThroughputChart.tsx +0 -144
  37. package/src/client/WorkerStatus.tsx +0 -226
  38. package/src/client/components/BrandIcons.tsx +0 -168
  39. package/src/client/components/ConfirmDialog.tsx +0 -126
  40. package/src/client/components/JobInspector.tsx +0 -554
  41. package/src/client/components/LogArchiveModal.tsx +0 -432
  42. package/src/client/components/NotificationBell.tsx +0 -212
  43. package/src/client/components/PageHeader.tsx +0 -47
  44. package/src/client/components/Toaster.tsx +0 -90
  45. package/src/client/components/UserProfileDropdown.tsx +0 -186
  46. package/src/client/contexts/AuthContext.tsx +0 -105
  47. package/src/client/contexts/NotificationContext.tsx +0 -128
  48. package/src/client/index.css +0 -174
  49. package/src/client/index.html +0 -12
  50. package/src/client/main.tsx +0 -15
  51. package/src/client/pages/LoginPage.tsx +0 -162
  52. package/src/client/pages/MetricsPage.tsx +0 -417
  53. package/src/client/pages/OverviewPage.tsx +0 -517
  54. package/src/client/pages/PulsePage.tsx +0 -488
  55. package/src/client/pages/QueuesPage.tsx +0 -379
  56. package/src/client/pages/SchedulesPage.tsx +0 -540
  57. package/src/client/pages/SettingsPage.tsx +0 -1020
  58. package/src/client/pages/WorkersPage.tsx +0 -394
  59. package/src/client/pages/index.ts +0 -8
  60. package/src/client/utils.ts +0 -15
  61. package/src/server/config/ServerConfigManager.ts +0 -90
  62. package/src/server/index.ts +0 -860
  63. package/src/server/middleware/auth.ts +0 -127
  64. package/src/server/services/AlertService.ts +0 -321
  65. package/src/server/services/CommandService.ts +0 -137
  66. package/src/server/services/LogStreamProcessor.ts +0 -93
  67. package/src/server/services/MaintenanceScheduler.ts +0 -78
  68. package/src/server/services/PulseService.ts +0 -91
  69. package/src/server/services/QueueMetricsCollector.ts +0 -138
  70. package/src/server/services/QueueService.ts +0 -631
  71. package/src/shared/types.ts +0 -198
  72. package/tailwind.config.js +0 -73
  73. package/tests/placeholder.test.ts +0 -7
  74. package/tsconfig.json +0 -38
  75. package/tsconfig.node.json +0 -12
  76. package/vite.config.ts +0 -27
@@ -1,540 +0,0 @@
1
- import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
2
- import { format } from 'date-fns'
3
- import { AnimatePresence, motion } from 'framer-motion'
4
- import {
5
- Activity,
6
- AlertCircle,
7
- ArrowRight,
8
- Calendar,
9
- CheckCircle2,
10
- ChevronDown,
11
- Clock,
12
- Filter,
13
- Play,
14
- Plus,
15
- RefreshCcw,
16
- Search,
17
- Trash2,
18
- X,
19
- } from 'lucide-react'
20
- import { useState } from 'react'
21
- import { ConfirmDialog } from '../components/ConfirmDialog'
22
- import { useNotifications } from '../contexts/NotificationContext'
23
-
24
- // Update interface to match actual API response
25
- interface ScheduleInfo {
26
- id: string
27
- cron: string
28
- queue: string
29
- job: {
30
- className?: string
31
- name?: string
32
- data?: any
33
- payload?: any
34
- }
35
- lastRun?: number | string
36
- nextRun?: number | string
37
- }
38
-
39
- interface QueueListItem {
40
- name: string
41
- waiting: number
42
- delayed: number
43
- failed: number
44
- active: number
45
- paused: boolean
46
- }
47
-
48
- /**
49
- * Task Schedules Page.
50
- *
51
- * View and manage all recurring jobs and cron tasks. Users can register new
52
- * schedules, manually trigger execution, or delete existing schedules.
53
- *
54
- * @public
55
- * @since 3.0.0
56
- */
57
- export function SchedulesPage() {
58
- const queryClient = useQueryClient()
59
- const [isModalOpen, setIsModalOpen] = useState(false)
60
- const [formData, setFormData] = useState({
61
- id: '',
62
- cron: '',
63
- queue: 'default',
64
- className: '',
65
- payload: '{}',
66
- })
67
-
68
- const [confirmRunId, setConfirmRunId] = useState<string | null>(null)
69
- const [confirmDeleteId, setConfirmDeleteId] = useState<string | null>(null)
70
-
71
- const { addNotification } = useNotifications()
72
-
73
- const { data: queueData } = useQuery<{ queues: QueueListItem[] }>({
74
- queryKey: ['queues'],
75
- queryFn: () => fetch('/api/queues').then((res) => res.json()),
76
- })
77
-
78
- const { data, isLoading } = useQuery<{ schedules: ScheduleInfo[] }>({
79
- queryKey: ['schedules'],
80
- queryFn: () => fetch('/api/schedules').then((res) => res.json()),
81
- })
82
-
83
- const runMutation = useMutation({
84
- mutationFn: (id: string) => fetch(`/api/schedules/run/${id}`, { method: 'POST' }),
85
- onSuccess: (_, id) => {
86
- queryClient.invalidateQueries({ queryKey: ['queues'] })
87
- addNotification({
88
- type: 'success',
89
- title: 'Schedule Triggered',
90
- message: `Job ${id} has been manually pushed to the queue.`,
91
- })
92
- setConfirmRunId(null)
93
- },
94
- onError: (err: any) => {
95
- addNotification({
96
- type: 'error',
97
- title: 'Trigger Failed',
98
- message: err.message || 'Failed to run schedule manually.',
99
- })
100
- setConfirmRunId(null)
101
- },
102
- })
103
-
104
- const deleteMutation = useMutation({
105
- mutationFn: (id: string) => fetch(`/api/schedules/${id}`, { method: 'DELETE' }),
106
- onSuccess: (_, id) => {
107
- queryClient.invalidateQueries({ queryKey: ['schedules'] })
108
- addNotification({
109
- type: 'info',
110
- title: 'Schedule Deleted',
111
- message: `Schedule ${id} was removed.`,
112
- })
113
- setConfirmDeleteId(null)
114
- },
115
- })
116
-
117
- const registerMutation = useMutation({
118
- mutationFn: (body: any) =>
119
- fetch('/api/schedules', {
120
- method: 'POST',
121
- headers: { 'Content-Type': 'application/json' },
122
- body: JSON.stringify(body),
123
- }),
124
- onSuccess: () => {
125
- queryClient.invalidateQueries({ queryKey: ['schedules'] })
126
- setIsModalOpen(false)
127
- setFormData({ id: '', cron: '', queue: 'default', className: '', payload: '{}' })
128
- addNotification({
129
- type: 'success',
130
- title: 'Schedule Registered',
131
- message: 'New recurring task is now active.',
132
- })
133
- },
134
- })
135
-
136
- const schedules = data?.schedules || []
137
- const queues = queueData?.queues || []
138
-
139
- const handleSubmit = (e: React.FormEvent) => {
140
- e.preventDefault()
141
- try {
142
- const payload = JSON.parse(formData.payload)
143
- registerMutation.mutate({
144
- id: formData.id,
145
- cron: formData.cron,
146
- queue: formData.queue,
147
- job: {
148
- className: formData.className,
149
- payload,
150
- },
151
- })
152
- } catch (_err) {
153
- alert('Invalid JSON payload')
154
- }
155
- }
156
-
157
- return (
158
- <div className="space-y-8 max-w-6xl">
159
- {/* Header */}
160
- <div className="flex justify-between items-end">
161
- <div>
162
- <h1 className="text-4xl font-black tracking-tighter">Schedules</h1>
163
- <p className="text-muted-foreground mt-2 text-sm font-bold opacity-60 uppercase tracking-widest text-[10px]">
164
- Manage recurring tasks and cron workflows.
165
- </p>
166
- </div>
167
- <button
168
- type="button"
169
- onClick={() => setIsModalOpen(true)}
170
- className="px-6 py-3 bg-primary text-primary-foreground rounded-xl flex items-center gap-2 text-sm font-black uppercase tracking-widest shadow-lg shadow-primary/20 hover:scale-[1.02] active:scale-95 transition-all"
171
- >
172
- <Plus size={18} />
173
- New Schedule
174
- </button>
175
- </div>
176
-
177
- {/* Stats Overview */}
178
- <div className="grid grid-cols-3 gap-6">
179
- <div className="card-premium p-6 flex items-center gap-4">
180
- <div className="w-12 h-12 rounded-2xl bg-primary/10 flex items-center justify-center text-primary">
181
- <Activity size={24} />
182
- </div>
183
- <div>
184
- <p className="text-[10px] font-black text-muted-foreground uppercase tracking-widest">
185
- Active Schedules
186
- </p>
187
- <p className="text-2xl font-black">{isLoading ? '...' : schedules.length}</p>
188
- </div>
189
- </div>
190
- <div className="card-premium p-6 flex items-center gap-4">
191
- <div className="w-12 h-12 rounded-2xl bg-indigo-500/10 flex items-center justify-center text-indigo-500">
192
- <Calendar size={24} />
193
- </div>
194
- <div>
195
- <p className="text-[10px] font-black text-muted-foreground uppercase tracking-widest">
196
- Total Executions
197
- </p>
198
- <p className="text-2xl font-black">---</p>
199
- </div>
200
- </div>
201
- <div className="card-premium p-6 flex items-center gap-4">
202
- <div className="w-12 h-12 rounded-2xl bg-green-500/10 flex items-center justify-center text-green-500">
203
- <CheckCircle2 size={24} />
204
- </div>
205
- <div>
206
- <p className="text-[10px] font-black text-muted-foreground uppercase tracking-widest">
207
- Health Score
208
- </p>
209
- <p className="text-2xl font-black text-green-500">99.8%</p>
210
- </div>
211
- </div>
212
- </div>
213
-
214
- {/* Toolbar */}
215
- <div className="flex gap-4 items-center">
216
- <div className="relative flex-1">
217
- <Search
218
- className="absolute left-4 top-1/2 -translate-y-1/2 text-muted-foreground/50"
219
- size={18}
220
- />
221
- <input
222
- type="text"
223
- placeholder="Search schedules by name, cron or ID..."
224
- className="w-full bg-muted/40 border-border/50 rounded-xl pl-12 pr-4 py-3 text-sm focus:ring-1 focus:ring-primary/30 outline-none transition-all"
225
- />
226
- </div>
227
- <button
228
- type="button"
229
- className="p-3 bg-muted/40 border border-border/50 rounded-xl hover:bg-muted/60 transition-all"
230
- >
231
- <Filter size={18} />
232
- </button>
233
- </div>
234
-
235
- {/* Loading State */}
236
- {isLoading && (
237
- <div className="space-y-4">
238
- {[1, 2, 3].map((i) => (
239
- <div key={i} className="card-premium p-6 h-32 animate-pulse bg-muted/20" />
240
- ))}
241
- </div>
242
- )}
243
-
244
- {/* Implementation Empty State if zero */}
245
- {schedules.length === 0 && !isLoading && (
246
- <div className="card-premium p-20 flex flex-col items-center justify-center text-center opacity-60">
247
- <div className="w-20 h-20 rounded-3xl bg-muted flex items-center justify-center mb-6">
248
- <Clock size={32} className="text-muted-foreground/30" />
249
- </div>
250
- <h3 className="text-xl font-bold mb-2">No Scheduled Tasks Yet</h3>
251
- <p className="text-sm text-muted-foreground max-w-sm mb-8">
252
- Register recurring jobs using the @gravito/stream scheduler to see them appear here.
253
- </p>
254
- </div>
255
- )}
256
-
257
- {/* Schedules Grid */}
258
- <div className="grid grid-cols-1 gap-4">
259
- {schedules.map((schedule: ScheduleInfo) => (
260
- <div
261
- key={schedule.id}
262
- className="card-premium p-6 group hover:border-primary/30 transition-all"
263
- >
264
- <div className="flex items-start justify-between">
265
- <div className="flex gap-5">
266
- <div className="w-14 h-14 rounded-2xl bg-muted flex items-center justify-center text-muted-foreground group-hover:bg-primary/10 group-hover:text-primary transition-all">
267
- <Clock size={28} />
268
- </div>
269
- <div className="space-y-1">
270
- <div className="flex items-center gap-3">
271
- <h3 className="text-lg font-bold">{schedule.id}</h3>
272
- <span className="px-2 py-0.5 bg-green-500/10 text-green-500 text-[10px] font-black uppercase tracking-widest rounded">
273
- Enabled
274
- </span>
275
- </div>
276
- <div className="flex items-center gap-4 text-xs font-bold text-muted-foreground">
277
- <span className="flex items-center gap-1.5">
278
- <Clock size={12} /> {schedule.cron}
279
- </span>
280
- <span className="flex items-center gap-1.5">
281
- <ArrowRight size={12} /> {schedule.queue}
282
- </span>
283
- <span className="px-2 py-0.5 bg-muted rounded font-mono text-[10px] uppercase font-black">
284
- {schedule.job.className}
285
- </span>
286
- </div>
287
- </div>
288
- </div>
289
-
290
- <div className="flex items-center gap-2">
291
- <button
292
- type="button"
293
- disabled={runMutation.isPending}
294
- onClick={() => setConfirmRunId(schedule.id)}
295
- className="px-4 py-2 bg-muted hover:bg-primary hover:text-primary-foreground rounded-lg text-[10px] font-black uppercase tracking-widest transition-all active:scale-95 flex items-center gap-2 disabled:opacity-50"
296
- >
297
- {runMutation.isPending && runMutation.variables === schedule.id ? (
298
- <>
299
- <RefreshCcw size={12} className="animate-spin" />
300
- Running...
301
- </>
302
- ) : (
303
- 'Run Now'
304
- )}
305
- </button>
306
- <button
307
- type="button"
308
- className="p-2 hover:bg-muted rounded-lg transition-all text-muted-foreground"
309
- >
310
- <AlertCircle size={18} />
311
- </button>
312
- <button
313
- type="button"
314
- onClick={() => setConfirmDeleteId(schedule.id)}
315
- className="p-2 hover:bg-red-500/10 hover:text-red-500 rounded-lg transition-all text-muted-foreground"
316
- >
317
- <Trash2 size={18} />
318
- </button>
319
- </div>
320
- </div>
321
-
322
- <div className="mt-6 grid grid-cols-2 gap-4 pt-6 border-t border-border/30">
323
- <div className="flex items-center gap-3">
324
- <div className="w-8 h-8 rounded-lg bg-muted flex items-center justify-center text-muted-foreground/60">
325
- <Calendar size={14} />
326
- </div>
327
- <div>
328
- <p className="text-[10px] font-black text-muted-foreground/40 uppercase tracking-widest leading-none mb-1">
329
- Last Run
330
- </p>
331
- <p className="text-xs font-bold">
332
- {schedule.lastRun
333
- ? format(new Date(schedule.lastRun), 'HH:mm:ss MMM dd')
334
- : 'Never'}
335
- </p>
336
- </div>
337
- </div>
338
- <div className="flex items-center gap-3">
339
- <div className="w-8 h-8 rounded-lg bg-primary/10 flex items-center justify-center text-primary">
340
- <Play size={14} />
341
- </div>
342
- <div>
343
- <p className="text-[10px] font-black text-muted-foreground/40 uppercase tracking-widest leading-none mb-1">
344
- Next Run
345
- </p>
346
- <p className="text-xs font-bold">
347
- {schedule.nextRun
348
- ? format(new Date(schedule.nextRun), 'HH:mm:ss MMM dd')
349
- : 'Scheduled'}
350
- </p>
351
- </div>
352
- </div>
353
- </div>
354
- </div>
355
- ))}
356
- </div>
357
-
358
- {/* New Schedule Modal */}
359
- <AnimatePresence>
360
- {isModalOpen && (
361
- <div className="fixed inset-0 z-50 flex items-center justify-center p-4">
362
- <motion.div
363
- initial={{ opacity: 0 }}
364
- animate={{ opacity: 1 }}
365
- exit={{ opacity: 0 }}
366
- onClick={() => setIsModalOpen(false)}
367
- className="absolute inset-0 bg-black/60 backdrop-blur-sm"
368
- />
369
- <motion.div
370
- initial={{ opacity: 0, scale: 0.95, y: 20 }}
371
- animate={{ opacity: 1, scale: 1, y: 0 }}
372
- exit={{ opacity: 0, scale: 0.95, y: 20 }}
373
- className="relative w-full max-w-lg bg-card border border-border/50 rounded-3xl shadow-2xl overflow-hidden"
374
- >
375
- <div className="p-6 border-b flex items-center justify-between bg-muted/5">
376
- <div className="flex items-center gap-3">
377
- <div className="w-10 h-10 rounded-xl bg-primary/10 flex items-center justify-center text-primary">
378
- <Plus size={20} />
379
- </div>
380
- <div>
381
- <h2 className="text-lg font-bold leading-none">Register Schedule</h2>
382
- <p className="text-[10px] font-black text-muted-foreground uppercase tracking-widest mt-1">
383
- Add new recurring job
384
- </p>
385
- </div>
386
- </div>
387
- <button
388
- type="button"
389
- onClick={() => setIsModalOpen(false)}
390
- className="p-2 hover:bg-muted rounded-full transition-all"
391
- >
392
- <X size={20} />
393
- </button>
394
- </div>
395
-
396
- <form onSubmit={handleSubmit} className="p-6 space-y-5">
397
- <div className="space-y-2">
398
- <label
399
- htmlFor="schedule-id"
400
- className="text-[10px] font-black uppercase tracking-widest text-muted-foreground ml-1"
401
- >
402
- Schedule Identifier (ID)
403
- </label>
404
- <input
405
- id="schedule-id"
406
- required
407
- type="text"
408
- placeholder="daily-cleanup-job"
409
- className="w-full bg-muted/40 border-border/50 rounded-xl px-4 py-3 text-sm focus:ring-1 focus:ring-primary/30 outline-none transition-all"
410
- value={formData.id}
411
- onChange={(e) => setFormData({ ...formData, id: e.target.value })}
412
- />
413
- </div>
414
-
415
- <div className="grid grid-cols-2 gap-4">
416
- <div className="space-y-2">
417
- <label
418
- htmlFor="cron-expression"
419
- className="text-[10px] font-black uppercase tracking-widest text-muted-foreground ml-1"
420
- >
421
- Cron Expression
422
- </label>
423
- <input
424
- id="cron-expression"
425
- required
426
- type="text"
427
- placeholder="* * * * *"
428
- className="w-full bg-muted/40 border-border/50 rounded-xl px-4 py-3 text-sm font-mono focus:ring-1 focus:ring-primary/30 outline-none transition-all"
429
- value={formData.cron}
430
- onChange={(e) => setFormData({ ...formData, cron: e.target.value })}
431
- />
432
- </div>
433
- <div className="space-y-2">
434
- <label
435
- htmlFor="target-queue"
436
- className="text-[10px] font-black uppercase tracking-widest text-muted-foreground ml-1"
437
- >
438
- Target Queue
439
- </label>
440
- <div className="relative">
441
- <select
442
- id="target-queue"
443
- className="w-full bg-muted/40 border-border/50 rounded-xl px-4 py-3 text-sm focus:ring-1 focus:ring-primary/30 outline-none transition-all appearance-none cursor-pointer"
444
- value={formData.queue}
445
- onChange={(e) => setFormData({ ...formData, queue: e.target.value })}
446
- >
447
- {queues.map((q: any) => (
448
- <option key={q.name} value={q.name}>
449
- {q.name}
450
- </option>
451
- ))}
452
- </select>
453
- <ChevronDown
454
- className="absolute right-4 top-1/2 -translate-y-1/2 text-muted-foreground pointer-events-none"
455
- size={16}
456
- />
457
- </div>
458
- </div>
459
- </div>
460
-
461
- <div className="space-y-2">
462
- <label
463
- htmlFor="job-class-name"
464
- className="text-[10px] font-black uppercase tracking-widest text-muted-foreground ml-1"
465
- >
466
- Job Class Name
467
- </label>
468
- <input
469
- id="job-class-name"
470
- required
471
- type="text"
472
- placeholder="CleanupJob"
473
- className="w-full bg-muted/40 border-border/50 rounded-xl px-4 py-3 text-sm focus:ring-1 focus:ring-primary/30 outline-none transition-all font-mono"
474
- value={formData.className}
475
- onChange={(e) => setFormData({ ...formData, className: e.target.value })}
476
- />
477
- </div>
478
-
479
- <div className="space-y-2">
480
- <label
481
- htmlFor="job-payload"
482
- className="text-[10px] font-black uppercase tracking-widest text-muted-foreground ml-1"
483
- >
484
- Job Payload (JSON)
485
- </label>
486
- <textarea
487
- id="job-payload"
488
- rows={3}
489
- className="w-full bg-muted/40 border-border/50 rounded-xl px-4 py-3 text-sm font-mono focus:ring-1 focus:ring-primary/30 outline-none transition-all resize-none"
490
- value={formData.payload}
491
- onChange={(e) => setFormData({ ...formData, payload: e.target.value })}
492
- />
493
- </div>
494
-
495
- <div className="pt-4 flex gap-3">
496
- <button
497
- type="button"
498
- onClick={() => setIsModalOpen(false)}
499
- className="flex-1 py-3 border border-border/50 rounded-xl text-xs font-black uppercase tracking-widest hover:bg-muted transition-all"
500
- >
501
- Cancel
502
- </button>
503
- <button
504
- type="submit"
505
- disabled={registerMutation.isPending}
506
- className="flex-1 py-3 bg-primary text-primary-foreground rounded-xl text-xs font-black uppercase tracking-widest shadow-lg shadow-primary/20 hover:scale-[1.02] active:scale-95 transition-all disabled:opacity-50"
507
- >
508
- {registerMutation.isPending ? 'Registering...' : 'Register Schedule'}
509
- </button>
510
- </div>
511
- </form>
512
- </motion.div>
513
- </div>
514
- )}
515
- </AnimatePresence>
516
-
517
- <ConfirmDialog
518
- open={!!confirmRunId}
519
- title="Manual trigger confirmation"
520
- message={`Are you sure you want to run "${confirmRunId}" immediately?\nThis will bypass the next scheduled time and push a job to the "${schedules.find((s) => s.id === confirmRunId)?.queue}" queue.`}
521
- confirmText="Run Now"
522
- variant="info"
523
- isProcessing={runMutation.isPending}
524
- onConfirm={() => confirmRunId && runMutation.mutate(confirmRunId)}
525
- onCancel={() => setConfirmRunId(null)}
526
- />
527
-
528
- <ConfirmDialog
529
- open={!!confirmDeleteId}
530
- title="Delete Schedule"
531
- message={`Are you sure you want to delete "${confirmDeleteId}"?\nThis action cannot be undone and recurring jobs for this schedule will stop.`}
532
- confirmText="Delete"
533
- variant="danger"
534
- isProcessing={deleteMutation.isPending}
535
- onConfirm={() => confirmDeleteId && deleteMutation.mutate(confirmDeleteId)}
536
- onCancel={() => setConfirmDeleteId(null)}
537
- />
538
- </div>
539
- )
540
- }