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