Pipeline/frontend/src/components/dashboard/GatewayCronPanel.tsx

124 lines
5.2 KiB
TypeScript
Raw Normal View History

"use client";
import { CheckCircle2, AlertCircle, Clock, XCircle, Loader2 } from "lucide-react";
import type { CronStatusResponse } from "@/api/generated/model";
import { DashboardSection } from "./DashboardSection";
import { DashboardEmptyState } from "./DashboardEmptyState";
interface GatewayCronPanelProps {
cron: CronStatusResponse | null;
isLoading?: boolean;
hasGateways: boolean;
}
type StatusKey = "ok" | "error" | "running" | "pending" | "disabled" | "unknown";
const STATUS_META: Record<StatusKey, { icon: React.ElementType; cls: string; label: string }> = {
ok: { icon: CheckCircle2, cls: "text-[color:var(--success)]", label: "OK" },
error: { icon: AlertCircle, cls: "text-[color:var(--danger)]", label: "Error" },
running: { icon: Loader2, cls: "text-[color:var(--accent)] animate-spin", label: "Running" },
pending: { icon: Clock, cls: "text-muted", label: "Pending" },
disabled: { icon: XCircle, cls: "text-muted opacity-40", label: "Disabled" },
unknown: { icon: Clock, cls: "text-muted", label: "Unknown" },
};
function fmtMs(ms: number | null | undefined): string {
if (ms == null) return "";
if (ms < 1000) return `${ms}ms`;
return `${(ms / 1000).toFixed(1)}s`;
}
function relativeTime(iso: string | null | undefined): string {
if (!iso) return "never";
try {
const diff = Date.now() - new Date(iso).getTime();
if (diff < 60_000) return "just now";
if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`;
if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago`;
return `${Math.floor(diff / 86_400_000)}d ago`;
} catch {
return "—";
}
}
export function GatewayCronPanel({
cron,
isLoading = false,
hasGateways,
}: GatewayCronPanelProps) {
if (!hasGateways) return null;
const jobs = cron?.jobs ?? [];
return (
<DashboardSection
title="Cron Jobs"
action={jobs.length > 0 ? { label: `${jobs.length}`, href: "#" } : undefined}
>
{isLoading && !cron ? (
2026-05-27 19:22:55 -05:00
<div className="space-y-1.5">
{[0, 1].map((i) => (
<div
key={i}
className="flex min-w-0 items-start gap-2 rounded-lg border border-[color:var(--border)] bg-[color:var(--surface-muted)] px-3 py-2"
>
<div className="mt-0.5 h-3.5 w-3.5 shrink-0 animate-pulse rounded-full bg-[color:var(--surface-strong)]" />
<div className="min-w-0 flex-1 space-y-1.5">
<div className="flex items-baseline gap-2">
<div className="h-3.5 w-32 animate-pulse rounded bg-[color:var(--surface-strong)]" />
<div className="h-3 w-20 animate-pulse rounded bg-[color:var(--surface-strong)] opacity-60" />
</div>
<div className="flex gap-3">
<div className="h-3 w-16 animate-pulse rounded bg-[color:var(--surface-strong)] opacity-50" />
<div className="h-3 w-16 animate-pulse rounded bg-[color:var(--surface-strong)] opacity-50" />
</div>
</div>
<div className="h-3 w-8 shrink-0 animate-pulse rounded bg-[color:var(--surface-strong)] opacity-60" />
</div>
))}
</div>
) : jobs.length === 0 ? (
<DashboardEmptyState message="No cron jobs found on this gateway." />
) : (
<div className="space-y-1.5">
{jobs.map((job) => {
const meta = STATUS_META[(job.status as StatusKey) ?? "unknown"] ?? STATUS_META.unknown;
const Icon = meta.icon;
return (
<div
key={job.name}
className="flex min-w-0 items-start gap-2 rounded-lg border border-[color:var(--border)] bg-[color:var(--surface-muted)] px-3 py-2"
>
<Icon className={`mt-0.5 h-3.5 w-3.5 shrink-0 ${meta.cls}`} />
<div className="min-w-0 flex-1">
<div className="flex items-baseline gap-2">
<p className="truncate text-sm font-medium text-strong">{job.name}</p>
{job.schedule && (
<code className="shrink-0 text-[11px] text-muted">{job.schedule}</code>
)}
</div>
<div className="mt-0.5 flex flex-wrap gap-x-3 gap-y-0.5 text-[11px] text-muted">
<span>Last: {relativeTime(job.last_run)}</span>
{job.next_run && <span>Next: {relativeTime(job.next_run)}</span>}
{job.last_duration_ms != null && (
<span>{fmtMs(job.last_duration_ms)}</span>
)}
{job.last_error && (
<span className="text-[color:var(--danger)] truncate max-w-[180px]" title={job.last_error}>
{job.last_error}
</span>
)}
</div>
</div>
<span className={`shrink-0 text-[11px] font-medium ${meta.cls}`}>
{meta.label}
</span>
</div>
);
})}
</div>
)}
</DashboardSection>
);
}