Ghost

struxadotcloud/struxa

public
Fork0
CodeIssues0Pull requests0
1branch
struxaappswebsrcapp(panel)servers[id]schedulespage.tsx
feat: global unslop6531421a month ago703 lines · 29.4 KB
1"use client";
2
3import { useEffect, useState
4import { use }
5import { useRouter }
6import { useQuery, useMutation
7import { Clock, ListChecks
8import type { LucideIcon
9import {
10 Dialog,
11 DialogPopup,
12 DialogHeader,
13 DialogFooter,
14 DialogTitle,
15 DialogDescription,
16 DialogClose,
17} from "@struxa/ui/components/dialog";
18import {
19 DropdownMenu,
20 DropdownMenuContent,
21 DropdownMenuItem,
22 DropdownMenuTrigger,
23} from "@struxa/ui/components/dropdown-menu";
24import { useTranslations }
25import { toast }
26import { orpc }
27import { authClient }
28import Loader from "@/components/loader
29
30function StatRow({ icon
31 return (
32 <div className="flex flex-col border-b border-border last:border-b-0
33 <div className="flex flex-col gap-1.5 px-4 pt-3 pb-2.5
34 <div className="flex items-center gap-1.5 text-[10px] font-medium uppercase tracking-widest text-muted-foreground
35 <Icon className="
36 {label}
37 </div>
38 {children}
39 </div>
40 </div>
41 );
42}
43
44function fmtDate(d:
45 if (!d)
46 return new Date
47}
48
49function formatCron(sch:
50 cronMinute: string
51 cronHour: string
52 cronDayOfMonth: string
53 cronDayOfWeek: string
54}): string {
55 return `${sch.
56}
57
58const DOW_KEYS = [
59
60// eslint-disable-next-line @typescript-eslint/no-explicit-any
61type CronT
62
63function describeCron(minute:
64 const minStep =
65 const hrStep =
66 const minFixed =
67 const hrFixed =
68 const domFixed =
69 const dowFixed =
70
71 if (minute ===
72 if (minStep &&
73 return t("cronEveryNMinutes
74 if (minFixed ===
75 if (minFixed ===
76 return t("cronEveryNHours
77
78 const timeStr =
79 hrFixed !== null &&
80 ? (() =>
81 const h =
82 const ampm =
83 const m =
84 return `${h}
85 })()
86 : null;
87
88 if (timeStr) {
89 if (dom ===
90 if (dom ===
91 if (domFixed !==
92 if (domFixed !==
93 return t("cronOnDayOrWeekday
94 }
95
96 return `${minute}
97}
98
99function CronField({
100 label,
101 placeholder,
102 value,
103 onChange,
104 hint,
105}: {
106 label: string
107 placeholder: string
108 value: string
109 onChange: (v
110 hint?: string
111}) {
112 return (
113 <div className="flex flex-col gap-1
114 <label className="text-xs font-medium text-foreground
115 <input
116 value={value}
117 onChange={(e)
118 placeholder={placeholder}
119 className="w-full rounded-lg border border-border bg-background px-2.5 py-1.5 font-mono text-sm text-foreground outline-none placeholder:text-muted-foreground/50 focus:border-ring transition-colors"
120 />
121 {hint && <span
122 </div>
123 );
124}
125
126type TaskAction
127
128interface ScheduleTask {
129 id: string;
130 scheduleId: string;
131 sequenceId: number;
132 action: string;
133 payload: string;
134 timeOffset: number;
135 continueOnFailure: boolean;
136 isQueued: boolean;
137}
138
139const ACTION_LABEL_KEYS: Record
140 command: "actionCommand",
141 power: "actionPower",
142 backup: "actionBackup",
143};
144
145const ACTION_COLORS: Record
146 command: "bg-blue-500/10 text-blue-500",
147 power: "bg-amber-500/10 text-amber-500",
148 backup: "bg-purple-500/10 text-purple-500",
149};
150
151const EMPTY_TASK_FORM = {
152 action: "command" as
153 payload: "",
154 timeOffset: 0,
155 continueOnFailure: false,
156};
157
158function TasksPanel({
159 scheduleId,
160 serverId,
161 tasks,
162 queryClient: qc,
163}: {
164 scheduleId: string
165 serverId: string
166 tasks: ScheduleTask
167 queryClient: ReturnType
168}) {
169 const t =
170 const tc =
171 const [showModal,
172 const [taskForm,
173
174 const createTaskMutation =
175 ...orpc.schedules.
176 onSuccess: () =>
177 void qc.invalidateQueries(
178 setShowModal(false);
179 setTaskForm(EMPTY_TASK_FORM);
180 toast.success(tc
181 },
182 });
183
184 const deleteTaskMutation =
185 ...orpc.schedules.
186 onSuccess: () =>
187 void qc.invalidateQueries(
188 toast.success(tc
189 },
190 });
191
192 const nextSeq =
193
194 function closeModal() {
195 setShowModal(false);
196 setTaskForm(EMPTY_TASK_FORM);
197 }
198
199 async function handleAddTask
200 const payload =
201 taskForm.action ===
202 ? ""
203 : taskForm.action
204 ? taskForm.payload
205 : taskForm.payload;
206 await createTaskMutation.mutateAsync
207 serverId,
208 scheduleId,
209 sequenceId: nextSeq,
210 action: taskForm.action
211 payload,
212 timeOffset: taskForm.timeOffset
213 continueOnFailure: taskForm.continueOnFailure
214 });
215 }
216
217 return (
218 <>
219 <Dialog open={
220 <DialogPopup showCloseButton={
221 <DialogHeader>
222 <DialogTitle>{t(
223 <DialogDescription>
224 {t("addTaskDescription")
225 </DialogDescription>
226 </DialogHeader>
227 <div className="flex flex-col gap-4 px-5 py-4
228 <div className="flex flex-col gap-1.5
229 <label className="text-xs font-medium text-foreground
230 <DropdownMenu>
231 <DropdownMenuTrigger className="
232 <span>
233 {taskForm.action ===
234 {taskForm.action ===
235 {taskForm.action ===
236 </span>
237 <ChevronDown className="
238 </DropdownMenuTrigger>
239 <DropdownMenuContent align="
240 {(["command", "power
241 <DropdownMenuItem
242 key={a}
243 onClick={() =>
244 className="flex cursor-pointer items-center gap-2.5 rounded-lg px-3 py-2 text-sm text-muted-foreground hover:bg-muted hover:text-foreground focus:bg-muted focus:text-foreground"
245 >
246 <span className={`
247 {a === "command
248 {a === "power
249 {a === "backup
250 </DropdownMenuItem>
251 ))}
252 </DropdownMenuContent>
253 </DropdownMenu>
254 </div>
255
256 {taskForm.action ===
257 <div className="flex flex-col gap-1
258 <label className="text-xs font-medium text-foreground
259 <input
260 autoFocus
261 value={taskForm.payload
262 onChange={(e)
263 placeholder="say Hello World"
264 className="w-full rounded-lg border border-border bg-background px-3 py-2 font-mono text-sm text-foreground outline-none placeholder:text-muted-foreground/40 focus:border-ring transition-colors"
265 onKeyDown={(e)
266 />
267 </div>
268 )}
269
270 {taskForm.action ===
271 <div className="flex flex-col gap-1.5
272 <label className="text-xs font-medium text-foreground
273 <DropdownMenu>
274 <DropdownMenuTrigger className="
275 <span>{taskForm.payload
276 <ChevronDown className="
277 </DropdownMenuTrigger>
278 <DropdownMenuContent align="
279 {(["start", "stop
280 <DropdownMenuItem
281 key={p}
282 onClick={() =>
283 className="flex cursor-pointer items-center gap-2.5 rounded-lg px-3 py-2 text-sm capitalize text-muted-foreground hover:bg-muted hover:text-foreground focus:bg-muted focus:text-foreground"
284 >
285 <span className={`
286 {p}
287 </DropdownMenuItem>
288 ))}
289 </DropdownMenuContent>
290 </DropdownMenu>
291 </div>
292 )}
293
294 <div className="grid grid-cols-2 gap-3
295 <div className="flex flex-col gap-1
296 <label className="text-xs font-medium text-foreground
297 <input
298 type="number"
299 min={0}
300 value={taskForm.timeOffset
301 onChange={(e)
302 setTaskForm((f)
303 }
304 className="rounded-lg border border-border bg-background px-3 py-2 text-sm text-foreground outline-none focus:border-ring transition-colors"
305 />
306 </div>
307 <div className="flex flex-col justify-end gap-1
308 <label className="flex cursor-pointer items-center gap-2 rounded-lg border border-border px-3 py-2 text-sm text-foreground hover:bg-muted/40 transition-colors
309 <input
310 type="checkbox"
311 checked={taskForm.continueOnFailure
312 onChange={(e)
313 className="rounded"
314 />
315 {t("continueOnFailure")
316 </label>
317 </div>
318 </div>
319
320 </div>
321 <DialogFooter>
322 <DialogClose
323 className="rounded-lg px-4 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
324 disabled={createTaskMutation.isPending
325 >
326 {t("cancel")
327 </DialogClose>
328 <button
329 type="button"
330 onClick={() =>
331 disabled={
332 createTaskMutation.isPending ||
333 (taskForm.action ===
334 }
335 className="rounded-lg bg-foreground px-4 py-1.5 text-xs font-medium text-background transition-opacity hover:opacity-80 disabled:opacity-40"
336 >
337 {createTaskMutation.isPending ?
338 </button>
339 </DialogFooter>
340 </DialogPopup>
341 </Dialog>
342
343 <div className="border-t border-border/60 bg-muted/10
344 {tasks.length ===
345 <div className="flex items-center gap-3 px-10 py-3 text-xs text-muted-foreground
346 {t("noTasks")
347 </div>
348 ) : (
349 <div className="divide-y divide-border/40
350 {tasks
351 .slice()
352 .sort((a,
353 .map((task)
354 const action =
355 return (
356 <div
357 key={task.id
358 className="flex items-center gap-3 px-10 py-2.5 hover:bg-muted/30 transition-colors"
359 >
360 <span className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-muted text-[10px] font-bold text-muted-foreground
361 {task.sequenceId}
362 </span>
363 <span
364 className={`rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide ${ACTION_COLORS
365 >
366 {ACTION_LABEL_KEYS[action]
367 </span>
368 <span className="flex-1 font-mono text-xs text-foreground truncate
369 {task.payload ||
370 </span>
371 {task.timeOffset >
372 <span className="text-[11px] text-muted-foreground
373 )}
374 {task.continueOnFailure &&
375 <span className="text-[10px] text-muted-foreground/60
376 )}
377 <button
378 type="button"
379 onClick={() =>
380 disabled={deleteTaskMutation.isPending
381 className="rounded p-0.5 text-muted-foreground/40 hover:bg-muted hover:text-destructive transition-colors disabled:opacity-30"
382 >
383 <Trash2 className="
384 </button>
385 </div>
386 );
387 })}
388 </div>
389 )}
390
391 <div className="border-t border-border/60 px-10 py-2
392 <button
393 type="button"
394 onClick={() =>
395 className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
396 >
397 <Plus className="
398 {t("addTask")
399 </button>
400 </div>
401 </div>
402 </>
403 );
404}
405
406export default function SchedulesPage
407 const t =
408 const tc =
409 const router =
410 const { data:
411 const { id
412 const queryClient =
413 const [showCreate,
414 const [expandedId,
415 const [form,
416 name: "",
417 cronMinute: "*/5",
418 cronHour: "*",
419 cronDayOfMonth: "*",
420 cronDayOfWeek: "*",
421 });
422
423 useEffect(() =>
424 if (!isPending &&
425 }, [isPending,
426
427 const { data:
428 const serverId =
429
430 const { data:
431 ...orpc.schedules.
432 enabled: !!serverId,
433 });
434
435 const createMutation =
436 const toggleMutation =
437 onSuccess: () =>
438 void queryClient.invalidateQueries(
439 toast.success(tc
440 },
441 }));
442 const deleteMutation =
443 ...orpc.schedules.
444 onSuccess: () =>
445 void queryClient.invalidateQueries(
446 orpc.schedules.list
447 );
448 toast.success(tc
449 },
450 });
451
452 function closeCreate() {
453 setShowCreate(false);
454 setForm({ name:
455 }
456
457 async function handleCreate
458 if (!serverId ||
459 const result =
460 serverId,
461 name: form.name
462 isActive: true,
463 cronSecond: "0",
464 cronMinute: form.cronMinute
465 cronHour: form.cronHour
466 cronDayOfMonth: form.cronDayOfMonth
467 cronDayOfWeek: form.cronDayOfWeek
468 });
469 void queryClient.invalidateQueries(
470 toast.success(tc
471 closeCreate();
472 if (result?.id
473 }
474
475 if (isPending ||
476
477 const enabled =
478 const disabled =
479 const soonest =
480 .slice()
481 .sort((a,
482 a.nextRunAt &&
483 ? new Date(
484 : 0,
485 )[0];
486
487 const cronDescription =
488 form.cronMinute ||
489 form.cronHour ||
490 form.cronDayOfMonth ||
491 form.cronDayOfWeek ||
492 t,
493 );
494
495 return (
496 <>
497 <Dialog open={
498 <DialogPopup showCloseButton={
499 <DialogHeader>
500 <DialogTitle>{t(
501 <DialogDescription>{t(
502 </DialogHeader>
503 <div className="flex flex-col gap-4 px-5 py-4
504 <div>
505 <label className="mb-1.5 block text-xs font-medium text-foreground
506 <input
507 autoFocus
508 value={form.name
509 onChange={(e)
510 placeholder={t("
511 className="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm text-foreground outline-none placeholder:text-muted-foreground/50 focus:border-ring transition-colors"
512 />
513 </div>
514 <div>
515 <p className="mb-2 text-xs font-medium text-foreground
516 <div className="grid grid-cols-4 gap-2
517 <CronField
518 label={t("
519 placeholder="*/5"
520 value={form.cronMinute
521 onChange={(v)
522 hint="0–59"
523 />
524 <CronField
525 label={t("
526 placeholder="*"
527 value={form.cronHour
528 onChange={(v)
529 hint="0–23"
530 />
531 <CronField
532 label={t("
533 placeholder="*"
534 value={form.cronDayOfMonth
535 onChange={(v)
536 hint="1–31"
537 />
538 <CronField
539 label={t("
540 placeholder="*"
541 value={form.cronDayOfWeek
542 onChange={(v)
543 hint="0–7"
544 />
545 </div>
546 <div className="mt-2 rounded-lg border border-border bg-muted/20 px-3 py-2.5
547 <p className="text-sm font-medium text-foreground
548 <code className="mt-0.5 block font-mono text-[11px] text-muted-foreground
549 {form.cronMinute ||
550 {form.cronDayOfWeek ||
551 </code>
552 </div>
553 </div>
554 </div>
555 <DialogFooter>
556 <DialogClose
557 className="rounded-lg px-4 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
558 disabled={createMutation.isPending
559 >
560 {t("cancel")
561 </DialogClose>
562 <button
563 type="button"
564 onClick={() =>
565 disabled={!form.
566 className="rounded-lg bg-foreground px-4 py-1.5 text-xs font-medium text-background transition-opacity hover:opacity-80 disabled:opacity-40"
567 >
568 {createMutation.isPending ?
569 </button>
570 </DialogFooter>
571 </DialogPopup>
572 </Dialog>
573
574 <div className="flex flex-1 flex-col gap-3 overflow-auto px-4 py-4 md:flex-row md:overflow-hidden
575 <div className="flex flex-1 flex-col overflow-hidden rounded-xl border border-border bg-card
576 <div className="flex items-center justify-between border-b border-border px-4 py-3
577 <p className="text-sm font-medium text-foreground
578 <button
579 type="button"
580 onClick={() =>
581 className="flex items-center gap-1.5 rounded-lg bg-foreground px-3 py-1.5 text-xs font-medium text-background transition-opacity hover:opacity-80"
582 >
583 <Plus className="
584 {t("newSchedule")
585 </button>
586 </div>
587 <div className="grid grid-cols-[28px_1fr_160px_180px_180px_48px] border-b border-border bg-muted/40 px-4 py-2.5
588 <span />
589 <span className="text-xs font-medium text-muted-foreground
590 <span className="text-xs font-medium text-muted-foreground
591 <span className="text-xs font-medium text-muted-foreground
592 <span className="text-xs font-medium text-muted-foreground
593 <span />
594 </div>
595 <div className="flex-1 overflow-y-auto
596 {schedulesPending ? (
597 <div className="flex items-center justify-center py-12 text-sm text-muted-foreground
598 {t("loading")
599 </div>
600 ) : schedules.length
601 <div className="flex flex-col items-center justify-center py-12 gap-2
602 <Clock className="
603 <p className="text-sm text-muted-foreground
604 <button
605 type="button"
606 onClick={() =>
607 className="mt-1 rounded-lg border border-border px-3 py-1.5 text-xs font-medium text-muted-foreground hover:bg-muted hover:text-foreground transition-colors"
608 >
609 {t("createFirst")
610 </button>
611 </div>
612 ) : (
613 schedules.map((sch
614 const tasks = (
615 const isExpanded =
616 return (
617 <div key={sch
618 <div
619 className="grid grid-cols-[28px_1fr_160px_180px_180px_48px] items-center px-4 py-3 transition-colors hover:bg-muted/40 cursor-pointer"
620 onClick={() =>
621 >
622 <button
623 type="button"
624 title={sch.isActive
625 onClick={(e)
626 e.stopPropagation();
627 serverId &&
628 toggleMutation.mutate({
629 }}
630 className="flex items-center"
631 >
632 <span
633 className={`h-2 w-2 shrink-0 rounded-full ${sch
634 />
635 </button>
636 <div className="flex items-center gap-2
637 {isExpanded ? (
638 <ChevronDown className="
639 ) : (
640 <ChevronRight className="
641 )}
642 <div className="flex flex-col gap-0.5
643 <span className="text-sm font-medium text-foreground
644 <span className="text-xs text-muted-foreground
645 {t("taskCount",
646 </span>
647 </div>
648 </div>
649 <span className="font-mono text-xs text-muted-foreground
650 <span className="text-xs text-muted-foreground
651 <span className="text-xs text-muted-foreground
652 <div className="flex items-center justify-end pr-1
653 <button
654 type="button"
655 onClick={(e)
656 e.stopPropagation();
657 serverId && deleteMutation.
658 }}
659 className="rounded p-0.5 text-muted-foreground/50 hover:bg-muted hover:text-destructive transition-colors"
660 >
661 <Trash2 className="
662 </button>
663 </div>
664 </div>
665
666 {isExpanded && serverId
667 <TasksPanel
668 scheduleId={sch.id
669 serverId={serverId}
670 tasks={tasks}
671 queryClient={queryClient}
672 />
673 )}
674 </div>
675 );
676 })
677 )}
678 </div>
679 </div>
680
681 <aside className="flex w-full shrink-0 flex-col overflow-hidden rounded-xl border border-border bg-card md:w-[220px]
682 <div className="overflow-y-auto
683 <StatRow icon={
684 <span className="text-xl font-bold text-foreground
685 </StatRow>
686 <StatRow icon={
687 <span className="text-xl font-bold text-green-500
688 </StatRow>
689 <StatRow icon={
690 <span className="text-xl font-bold text-muted-foreground
691 </StatRow>
692 <StatRow icon={
693 <span className="text-sm font-semibold text-foreground leading-snug
694 {soonest ? fmtDate
695 </span>
696 {soonest && <span
697 </StatRow>
698 </div>
699 </aside>
700 </div>
701 </>
702 );
703}
}
from
"
react
"
;
from
"
react
"
;
from
"
next/navigation
"
;
,
useQueryClient
}
from
"
@tanstack/react-query
"
;
,
Trash2
,
Plus
,
ChevronRight
,
ChevronDown
}
from
"
lucide-react
"
;
}
from
"
lucide-react
"
;
from
"
next-intl
"
;
from
"
sonner
"
;
from
"
@/utils/orpc
"
;
from
"
@/lib/auth-client
"
;
"
;
:
Icon
,
label
,
children
}
:
{
icon
:
LucideIcon
;
label
:
string
;
children
:
React
.
ReactNode
})
{
"
>
"
>
"
>
h-3 w-3
"
/>
Date
|
string
|
null
)
:
string
{
return
"
-
"
;
(
d
)
.
toLocaleString
()
;
{
;
;
;
;
cronMinute
}
${
sch
.
cronHour
}
${
sch
.
cronDayOfMonth
}
*
${
sch
.
cronDayOfWeek
}
`
;
"
sunday
"
,
"
monday
"
,
"
tuesday
"
,
"
wednesday
"
,
"
thursday
"
,
"
friday
"
,
"
saturday
"
]
as
const
;
=
(
key
:
any
,
values
?
:
any
)
=>
string
;
string
,
hour
:
string
,
dom
:
string
,
dow
:
string
,
t
:
CronT
)
:
string
{
minute
.
match
(
/
^
\*\/
(
\d
+
)
$
/
)
?.
[
1
]
;
hour
.
match
(
/
^
\*\/
(
\d
+
)
$
/
)
?.
[
1
]
;
/
^
\d
+
$
/
.
test
(
minute
)
?
parseInt
(
minute
)
:
null
;
/
^
\d
+
$
/
.
test
(
hour
)
?
parseInt
(
hour
)
:
null
;
/
^
\d
+
$
/
.
test
(
dom
)
?
parseInt
(
dom
)
:
null
;
/
^
\d
+
$
/
.
test
(
dow
)
?
parseInt
(
dow
)
%
7
:
null
;
"
*
"
&&
hour
===
"
*
"
&&
dom
===
"
*
"
&&
dow
===
"
*
"
)
return
t
(
"
cronEveryMinute
"
)
;
hour
===
"
*
"
&&
dom
===
"
*
"
&&
dow
===
"
*
"
)
"
,
{
n
:
parseInt
(
minStep
)
}
)
;
0
&&
hour
===
"
*
"
&&
dom
===
"
*
"
&&
dow
===
"
*
"
)
return
t
(
"
cronEveryHour
"
)
;
0
&&
hrStep
&&
dom
===
"
*
"
&&
dow
===
"
*
"
)
"
,
{
n
:
parseInt
(
hrStep
)
}
)
;
minFixed
!==
null
{
hrFixed
%
12
||
12
;
hrFixed
<
12
?
"
AM
"
:
"
PM
"
;
minFixed
.
toString
()
.
padStart
(
2
,
"
0
"
)
;
:
${
m
}
${
ampm
}
`
;
"
*
"
&&
dow
===
"
*
"
)
return
t
(
"
cronEveryDayAt
"
,
{
time
:
timeStr
}
)
;
"
*
"
&&
dowFixed
!==
null
)
return
t
(
"
cronEveryWeekdayAt
"
,
{
weekday
:
t
(
`
dow.
${
DOW_KEYS
[
dowFixed
]
}
`
)
,
time
:
timeStr
}
)
;
null
&&
dow
===
"
*
"
)
return
t
(
"
cronMonthlyOnDay
"
,
{
day
:
domFixed
,
time
:
timeStr
}
)
;
null
&&
dowFixed
!==
null
)
"
,
{
day
:
domFixed
,
weekday
:
t
(
`
dow.
${
DOW_KEYS
[
dowFixed
]
}
`
)
,
time
:
timeStr
}
)
;
${
hour
}
${
dom
}
*
${
dow
}
`
;
;
;
;
:
string
)
=>
void
;
;
"
>
"
>
{
label
}
</
label
>
=>
onChange
(
e
.
target
.
value
)
}
className
=
"
text-[10px] text-muted-foreground
"
>
{
hint
}
</
span
>
}
=
"
command
"
|
"
power
"
|
"
backup
"
;
<
TaskAction
,
string
>
=
{
<
TaskAction
,
string
>
=
{
TaskAction
,
;
;
[]
;
<
typeof
useQueryClient
>
;
useTranslations
(
"
panel.schedules
"
)
;
useTranslations
(
"
common
"
)
;
setShowModal
]
=
useState
(
false
)
;
setTaskForm
]
=
useState
(
EMPTY_TASK_FORM
)
;
useMutation
(
{
createTask
.
mutationOptions
()
,
{
orpc
.
schedules
.
list
.
queryOptions
(
{
input
:
{
serverId
}
}
))
;
(
"
created
"
))
;
useMutation
(
{
deleteTask
.
mutationOptions
()
,
{
orpc
.
schedules
.
list
.
queryOptions
(
{
input
:
{
serverId
}
}
))
;
(
"
deleted
"
))
;
tasks
.
length
>
0
?
Math
.
max
(
...
tasks
.
map
(
(
t
)
=>
t
.
sequenceId
))
+
1
:
1
;
()
{
"
backup
"
===
"
power
"
||
"
restart
"
(
{
,
,
,
showModal
}
onOpenChange
=
{
(
open
)
=>
{
if
(
!
open
)
closeModal
()
;
}
}
>
false
}
>
"
addTaskTitle
"
)
}
</
DialogTitle
>
}
"
>
"
>
"
>
{
t
(
"
actionLabel
"
)
}
</
label
>
flex w-full items-center justify-between rounded-lg border border-border bg-background px-3 py-1.5 text-sm text-foreground outline-none transition-colors hover:border-ring data-[popup-open]:border-ring
"
>
"
command
"
&&
t
(
"
actionCommandDesc
"
)
}
"
power
"
&&
t
(
"
actionPowerDesc
"
)
}
"
backup
"
&&
t
(
"
actionBackupDesc
"
)
}
ml-2 h-3.5 w-3.5 shrink-0 text-muted-foreground
"
/>
start
"
sideOffset
=
{
4
}
className
=
"
rounded-xl border border-border bg-card p-1 shadow-lg
"
style
=
{
{
width
:
"
var(--radix-dropdown-menu-trigger-width)
"
}
}
>
"
,
"
backup
"
]
as
TaskAction
[]
)
.
map
(
(
a
)
=>
(
setTaskForm
(
(
f
)
=>
(
{
...
f
,
action
:
a
,
payload
:
""
}
))
}
h-1.5 w-1.5 shrink-0 rounded-full
${
taskForm
.
action
===
a
?
"
bg-blue-500
"
:
"
bg-transparent
"
}
`
}
/>
"
&&
t
(
"
actionCommandDesc
"
)
}
"
&&
t
(
"
actionPowerDesc
"
)
}
"
&&
t
(
"
actionBackupDesc
"
)
}
"
command
"
&&
(
"
>
"
>
{
t
(
"
commandLabel
"
)
}
</
label
>
}
=>
setTaskForm
(
(
f
)
=>
(
{
...
f
,
payload
:
e
.
target
.
value
}
))
}
=>
{
if
(
e
.
key
===
"
Enter
"
)
void
handleAddTask
()
;
}
}
"
power
"
&&
(
"
>
"
>
{
t
(
"
powerActionLabel
"
)
}
</
label
>
flex w-full items-center justify-between rounded-lg border border-border bg-background px-3 py-1.5 text-sm text-foreground outline-none transition-colors hover:border-ring data-[popup-open]:border-ring capitalize
"
>
||
"
restart
"
}
</
span
>
ml-2 h-3.5 w-3.5 shrink-0 text-muted-foreground
"
/>
start
"
sideOffset
=
{
4
}
className
=
"
rounded-xl border border-border bg-card p-1 shadow-lg
"
style
=
{
{
width
:
"
var(--radix-dropdown-menu-trigger-width)
"
}
}
>
"
,
"
restart
"
,
"
kill
"
]
as
const
)
.
map
(
(
p
)
=>
(
setTaskForm
(
(
f
)
=>
(
{
...
f
,
payload
:
p
}
))
}
h-1.5 w-1.5 shrink-0 rounded-full
${
(
taskForm
.
payload
||
"
restart
"
)
===
p
?
"
bg-blue-500
"
:
"
bg-transparent
"
}
`
}
/>
"
>
"
>
"
>
{
t
(
"
delayLabel
"
)
}
</
label
>
}
=>
=>
(
{
...
f
,
timeOffset
:
Math
.
max
(
0
,
parseInt
(
e
.
target
.
value
)
||
0
)
}
))
"
>
"
>
}
=>
setTaskForm
(
(
f
)
=>
(
{
...
f
,
continueOnFailure
:
e
.
target
.
checked
}
))
}
}
}
}
void
handleAddTask
()
}
"
command
"
&&
!
taskForm
.
payload
.
trim
())
t
(
"
adding
"
)
:
t
(
"
addTask
"
)
}
"
>
0
?
(
"
>
}
"
>
b
)
=>
a
.
sequenceId
-
b
.
sequenceId
)
=>
{
task
.
action
as
TaskAction
;
}
"
>
[
action
]
??
"
bg-muted text-muted-foreground
"
}
`
}
?
t
(
ACTION_LABEL_KEYS
[
action
])
:
action
}
"
>
"
-
"
}
0
&&
(
"
>
+
{
task
.
timeOffset
}
s
</
span
>
(
"
>
{
t
(
"
continueOnFailShort
"
)
}
</
span
>
deleteTaskMutation
.
mutate
(
{
serverId
,
taskId
:
task
.
id
}
)
}
}
h-3 w-3
"
/>
"
>
setShowModal
(
true
)
}
h-3 w-3
"
/>
}
(
{
params
}
:
{
params
:
Promise
<
{
id
:
string
}
>
})
{
useTranslations
(
"
panel.schedules
"
)
;
useTranslations
(
"
common
"
)
;
useRouter
()
;
session
,
isPending
}
=
authClient
.
useSession
()
;
}
=
use
(
params
)
;
useQueryClient
()
;
setShowCreate
]
=
useState
(
false
)
;
setExpandedId
]
=
useState
<
string
|
null
>
(
null
)
;
setForm
]
=
useState
(
{
{
!
session
)
router
.
replace
(
"
/login
"
)
;
session
,
router
])
;
server
}
=
useQuery
(
orpc
.
servers
.
get
.
queryOptions
(
{
input
:
{
id
}
}
))
;
server
?.
id
;
schedules
=
[]
,
isPending
:
schedulesPending
}
=
useQuery
(
{
list
.
queryOptions
(
{
input
:
{
serverId
:
serverId
??
""
}
}
)
,
useMutation
(
orpc
.
schedules
.
create
.
mutationOptions
())
;
useMutation
(
orpc
.
schedules
.
update
.
mutationOptions
(
{
{
orpc
.
schedules
.
list
.
queryOptions
(
{
input
:
{
serverId
:
serverId
??
""
}
}
))
;
(
"
saved
"
))
;
useMutation
(
{
delete
.
mutationOptions
()
,
{
.
queryOptions
(
{
input
:
{
serverId
:
serverId
??
""
}
}
)
,
(
"
deleted
"
))
;
""
,
cronMinute
:
"
*/5
"
,
cronHour
:
"
*
"
,
cronDayOfMonth
:
"
*
"
,
cronDayOfWeek
:
"
*
"
}
)
;
()
{
!
form
.
name
.
trim
())
return
;
await
createMutation
.
mutateAsync
(
{
.
trim
()
,
||
"
*
"
,
||
"
*
"
,
||
"
*
"
,
||
"
*
"
,
orpc
.
schedules
.
list
.
queryOptions
(
{
input
:
{
serverId
}
}
))
;
(
"
created
"
))
;
)
setExpandedId
(
result
.
id
)
;
!
session
)
return
<
Loader
/>
;
schedules
.
filter
(
(
s
)
=>
s
.
isActive
)
;
schedules
.
filter
(
(
s
)
=>
!
s
.
isActive
)
;
enabled
b
)
=>
b
.
nextRunAt
a
.
nextRunAt
)
.
getTime
()
-
new
Date
(
b
.
nextRunAt
)
.
getTime
()
describeCron
(
"
*
"
,
"
*
"
,
"
*
"
,
"
*
"
,
showCreate
}
onOpenChange
=
{
(
open
)
=>
{
if
(
!
open
)
closeCreate
()
;
}
}
>
false
}
>
"
createTitle
"
)
}
</
DialogTitle
>
"
createDescription
"
)
}
</
DialogDescription
>
"
>
"
>
{
t
(
"
nameLabel
"
)
}
</
label
>
}
=>
setForm
(
(
f
)
=>
(
{
...
f
,
name
:
e
.
target
.
value
}
))
}
namePlaceholder
"
)
}
"
>
{
t
(
"
cronExpression
"
)
}
</
p
>
"
>
cronMinute
"
)
}
}
=>
setForm
(
(
f
)
=>
(
{
...
f
,
cronMinute
:
v
}
))
}
cronHour
"
)
}
}
=>
setForm
(
(
f
)
=>
(
{
...
f
,
cronHour
:
v
}
))
}
cronDay
"
)
}
}
=>
setForm
(
(
f
)
=>
(
{
...
f
,
cronDayOfMonth
:
v
}
))
}
cronWeekday
"
)
}
}
=>
setForm
(
(
f
)
=>
(
{
...
f
,
cronDayOfWeek
:
v
}
))
}
"
>
"
>
{
cronDescription
}
</
p
>
"
>
"
*
"
}
{
form
.
cronHour
||
"
*
"
}
{
form
.
cronDayOfMonth
||
"
*
"
}
*
{
"
"
}
"
*
"
}
}
}
void
handleCreate
()
}
name
.
trim
()
||
createMutation
.
isPending
}
t
(
"
creating
"
)
:
t
(
"
createSchedule
"
)
}
"
>
"
>
"
>
"
>
{
t
(
"
sectionTitle
"
)
}
</
p
>
setShowCreate
(
true
)
}
h-3.5 w-3.5
"
/>
}
"
>
"
>
{
t
(
"
colName
"
)
}
</
span
>
"
>
{
t
(
"
colCron
"
)
}
</
span
>
"
>
{
t
(
"
colLastRun
"
)
}
</
span
>
"
>
{
t
(
"
colNextRun
"
)
}
</
span
>
"
>
"
>
}
===
0
?
(
"
>
h-8 w-8 text-muted-foreground/30
"
/>
"
>
{
t
(
"
empty
"
)
}
</
p
>
setShowCreate
(
true
)
}
}
)
=>
{
sch
.
tasks
??
[])
as
ScheduleTask
[]
;
expandedId
===
sch
.
id
;
.
id
}
className
=
"
border-b border-border last:border-b-0
"
>
setExpandedId
(
isExpanded
?
null
:
sch
.
id
)
}
?
t
(
"
disable
"
)
:
t
(
"
enable
"
)
}
=>
{
serverId
,
scheduleId
:
sch
.
id
,
isActive
:
!
sch
.
isActive
}
)
;
.
isActive
?
"
bg-green-500
"
:
"
bg-muted-foreground/30
"
}
`
}
"
>
h-3 w-3 shrink-0 text-muted-foreground
"
/>
h-3 w-3 shrink-0 text-muted-foreground
"
/>
"
>
"
>
{
sch
.
name
}
</
span
>
"
>
{
count
:
tasks
.
length
}
)
}
"
>
{
formatCron
(
sch
)
}
</
span
>
"
>
{
fmtDate
(
sch
.
lastRunAt
)
}
</
span
>
"
>
{
fmtDate
(
sch
.
nextRunAt
)
}
</
span
>
"
>
=>
{
mutate
(
{
serverId
,
scheduleId
:
sch
.
id
}
)
;
h-3.5 w-3.5
"
/>
&&
(
}
"
>
"
>
ListChecks
}
label
=
{
t
(
"
statSchedules
"
)
}
>
"
>
{
schedules
.
length
}
</
span
>
Clock
}
label
=
{
t
(
"
statEnabled
"
)
}
>
"
>
{
enabled
.
length
}
</
span
>
Clock
}
label
=
{
t
(
"
statDisabled
"
)
}
>
"
>
{
disabled
.
length
}
</
span
>
Clock
}
label
=
{
t
(
"
statNextRun
"
)
}
>
"
>
(
soonest
.
nextRunAt
)
:
"
-
"
}
className
=
"
text-xs text-muted-foreground
"
>
{
soonest
.
name
}
</
span
>
}