"use client"; import clsx from "clsx"; import { AlertTriangle, CheckCircle2, CircleDashed, Info, XCircle, type LucideIcon } from "lucide-react"; import Link from "next/link"; import type { ReactNode } from "react"; import { isApiError } from "@/lib/api"; // ---------------------------------------------------------------- page header export function PageHeader({ title, subtitle, actions }: { title: string; subtitle?: ReactNode; actions?: ReactNode }) { return (

{title}

{subtitle &&

{subtitle}

}
{actions &&
{actions}
}
); } export function Card({ title, children, className, actions, id }: { title?: ReactNode; children: ReactNode; className?: string; actions?: ReactNode; id?: string }) { return (
{(title || actions) && (
{title}
{actions &&
{actions}
}
)} {children}
); } // ---------------------------------------------------------------- status badge type Tone = "ok" | "warn" | "serious" | "crit" | "neutral" | "accent"; const TONE: Record = { ok: "bg-ok-soft text-ok-ink", warn: "bg-warn-soft text-warn-ink", serious: "bg-serious-soft text-serious-ink", crit: "bg-crit-soft text-crit-ink", neutral: "bg-surface-2 text-ink-2", accent: "bg-accent-soft text-accent-ink", }; const TONE_ICON: Record = { ok: CheckCircle2, warn: AlertTriangle, serious: AlertTriangle, crit: XCircle, neutral: CircleDashed, accent: Info }; export function toneFor(status: string | null | undefined): Tone { switch ((status ?? "").toLowerCase()) { case "ok": case "active": case "succeeded": case "ready": case "running": case "verified": case "allowed": case "success": return status?.toLowerCase() === "running" ? "accent" : "ok"; case "warning": case "planned": case "queued": case "paused": case "pending_configuration": case "pending": case "stale": case "expired": case "cancelled": return "warn"; case "degraded": case "failed": case "error": case "dead": case "denied": case "unsupported": case "critical": return "crit"; case "serious": case "high": return "serious"; default: return "neutral"; } } export function StatusBadge({ status, label, icon = true, className }: { status: string | null | undefined; label?: string; icon?: boolean; className?: string }) { const tone = toneFor(status); const Icon = TONE_ICON[tone]; const text = label ?? (status ?? "unknown").replace(/_/g, " "); return ( {icon && } {text} ); } export function SeverityBadge({ severity }: { severity: string | null | undefined }) { const s = (severity ?? "").toLowerCase(); const tone: Tone = s === "error" || s === "critical" || s === "high" ? "crit" : s === "warning" || s === "medium" ? "warn" : s === "info" || s === "low" ? "accent" : "neutral"; const Icon = TONE_ICON[tone]; return ( {s || "—"} ); } // ---------------------------------------------------------------- KPI tile export function KpiTile({ label, value, sub, tone, href, className }: { label: string; value: ReactNode; sub?: ReactNode; tone?: Tone; href?: string; className?: string }) { const body = ( <>
{label}
{value}
{sub &&
{sub}
} ); const cls = clsx("card min-w-0", href && "transition hover:border-accent/60", className); return href ? ( {body} ) : (
{body}
); } // ---------------------------------------------------------------- misc export function Banner({ tone = "accent", children, className }: { tone?: Tone; children: ReactNode; className?: string }) { const Icon = TONE_ICON[tone]; const border = { ok: "border-ok/50", warn: "border-warn/60", serious: "border-serious/60", crit: "border-crit/60", neutral: "border-border", accent: "border-accent/40" }[tone]; return (
{children}
); } export function ErrorState({ error, retry }: { error: unknown; retry?: () => void }) { const msg = isApiError(error) ? `${error.message} (${error.code}, HTTP ${error.status})` : error instanceof Error ? error.message : "Unknown error"; const rid = isApiError(error) ? error.request_id : null; const forbidden = isApiError(error) && error.status === 403; return (
{forbidden ? "Not allowed" : "Could not load"}
{msg}
{rid &&
request_id {rid}
}
{retry && ( )}
); } export function Empty({ children = "Nothing here yet." }: { children?: ReactNode }) { return
{children}
; } export function Skeleton({ rows = 4, className }: { rows?: number; className?: string }) { return (
{Array.from({ length: rows }).map((_, i) => (
))}
); } export function Field({ label, children, hint, className }: { label: string; children: ReactNode; hint?: ReactNode; className?: string }) { return ( ); } export function KV({ items, className }: { items: [ReactNode, ReactNode][]; className?: string }) { return (
{items.map(([k, v], i) => (
{k}
{v ?? "—"}
))}
); } export function Mono({ children, className }: { children: ReactNode; className?: string }) { return {children}; } export function Tabs({ value, onChange, items }: { value: T; onChange: (v: T) => void; items: { value: T; label: string }[] }) { return (
{items.map((it) => ( ))}
); }