140 lines
4.9 KiB
TypeScript
140 lines
4.9 KiB
TypeScript
import { Clock3, Pencil, RefreshCw, Trash2, Webhook, Zap } from "lucide-react";
|
|
import type { RoutineTrigger } from "@paperclipai/shared";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { ToggleSwitch } from "@/components/ui/toggle-switch";
|
|
import { describeSchedule } from "./ScheduleEditor";
|
|
import { timeAgo } from "../lib/timeAgo";
|
|
|
|
interface TriggerListCardProps {
|
|
trigger: RoutineTrigger;
|
|
onEdit: () => void;
|
|
onDelete: () => void;
|
|
onToggleEnabled: (enabled: boolean) => void;
|
|
onRotateSecret?: () => void;
|
|
togglePending?: boolean;
|
|
}
|
|
|
|
export function TriggerListCard({
|
|
trigger,
|
|
onEdit,
|
|
onDelete,
|
|
onToggleEnabled,
|
|
onRotateSecret,
|
|
togglePending,
|
|
}: TriggerListCardProps) {
|
|
const isSchedule = trigger.kind === "schedule";
|
|
const isWebhook = trigger.kind === "webhook";
|
|
const Icon = isSchedule ? Clock3 : isWebhook ? Webhook : Zap;
|
|
|
|
const summary = isSchedule && trigger.cronExpression
|
|
? describeSchedule(trigger.cronExpression)
|
|
: isWebhook
|
|
? `Webhook${trigger.publicId ? ` · ${trigger.publicId}` : ""}`
|
|
: "API trigger";
|
|
|
|
const nextRun = isSchedule && trigger.enabled && trigger.nextRunAt
|
|
? new Date(trigger.nextRunAt).toLocaleString(undefined, {
|
|
weekday: "short",
|
|
day: "numeric",
|
|
month: "short",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
})
|
|
: trigger.enabled ? "—" : "Disabled";
|
|
|
|
const lastFired = trigger.lastFiredAt ? timeAgo(trigger.lastFiredAt) : "Never";
|
|
|
|
const resultIsError = typeof trigger.lastResult === "string" && /error|fail/i.test(trigger.lastResult);
|
|
|
|
return (
|
|
<div
|
|
className={`rounded-lg border border-border p-4 transition-colors ${trigger.enabled ? "bg-card" : "bg-muted/40"}`}
|
|
>
|
|
<div className="flex items-start gap-3">
|
|
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground">
|
|
<Icon className="h-4 w-4" />
|
|
</div>
|
|
|
|
<div className="min-w-0 flex-1">
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
<span className={`text-sm font-medium ${trigger.enabled ? "" : "text-muted-foreground"}`}>
|
|
{trigger.label || (isSchedule ? "Schedule" : isWebhook ? "Webhook" : "Trigger")}
|
|
</span>
|
|
<Badge variant="outline" className="text-[11px]">
|
|
{trigger.kind}
|
|
</Badge>
|
|
{!trigger.enabled && (
|
|
<Badge variant="secondary" className="text-[11px] text-muted-foreground">
|
|
paused
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
<div className="text-sm mt-1.5">{summary}</div>
|
|
{isSchedule && trigger.cronExpression && (
|
|
<div className="text-xs text-muted-foreground mt-1 font-mono">
|
|
{trigger.cronExpression}
|
|
{trigger.timezone ? ` · ${trigger.timezone}` : ""}
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid gap-3 sm:grid-cols-3 mt-4 text-xs">
|
|
<div>
|
|
<div className="text-muted-foreground mb-0.5">Next run</div>
|
|
<div>{nextRun}</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-muted-foreground mb-0.5">Last fired</div>
|
|
<div>{lastFired}</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-muted-foreground mb-0.5">Last result</div>
|
|
<div>
|
|
{trigger.lastResult ? (
|
|
<Badge
|
|
variant={resultIsError ? "destructive" : "secondary"}
|
|
className="text-[11px] max-w-full truncate"
|
|
title={trigger.lastResult}
|
|
>
|
|
{trigger.lastResult}
|
|
</Badge>
|
|
) : (
|
|
<span className="text-muted-foreground">—</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-col items-end gap-3 shrink-0">
|
|
<ToggleSwitch
|
|
checked={trigger.enabled}
|
|
onCheckedChange={onToggleEnabled}
|
|
disabled={togglePending}
|
|
aria-label={trigger.enabled ? "Disable trigger" : "Enable trigger"}
|
|
/>
|
|
<div className="flex gap-1">
|
|
{isWebhook && onRotateSecret && (
|
|
<Button variant="ghost" size="xs" onClick={onRotateSecret} title="Rotate secret">
|
|
<RefreshCw className="h-3.5 w-3.5" />
|
|
</Button>
|
|
)}
|
|
<Button variant="ghost" size="xs" onClick={onEdit} title="Edit">
|
|
<Pencil className="h-3.5 w-3.5" />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="xs"
|
|
onClick={onDelete}
|
|
title="Delete"
|
|
className="text-muted-foreground hover:text-destructive"
|
|
>
|
|
<Trash2 className="h-3.5 w-3.5" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|