feat: add Gateway page and Runs page with real-time updates

- Implemented a new Gateway page that provides a uniform ingress for deployed agents.
- Created a Runs page that lists recent pipeline executions with auto-refresh and error handling.
- Added a RunsListener component to handle real-time notifications for new runs via SSE.
- Updated the AppSidebar to include links to the new Gateway and Runs pages.
- Enhanced FlowNode component to improve status display and styling.
- Introduced EmptySection component for consistent empty state presentation.
- Updated API utility to include a new endpoint for the runs stream.
This commit is contained in:
patel-lyzr
2026-05-13 22:15:08 +05:30
parent 0284ff768a
commit b6647a537d
25 changed files with 1764 additions and 509 deletions
+88 -27
View File
@@ -28,33 +28,34 @@ export function FlowNode({ data, selected }: NodeProps) {
const outputs = entry?.outputs ?? 1;
const isTrigger = pn.type === "flow-nodes-base.trigger";
// Per-type accent colour for the TYPE label. Keeps the existing
// catalog `color` (full bg) but extracts the hue for header text.
const typeAccent = typeAccentClass(entry?.color);
return (
<div
className={cn(
"min-w-[200px] rounded-lg border bg-card text-card-foreground shadow-sm transition-shadow",
"min-w-[260px] rounded-xl border bg-card px-4 py-3 text-card-foreground shadow-sm transition-shadow",
selected ? "ring-2 ring-primary shadow-md" : "hover:shadow-md",
!entry && "border-destructive/60",
status === "running" && "ring-2 ring-sky-500 animate-pulse",
status === "success" && "ring-2 ring-emerald-500",
status === "running" && "ring-2 ring-sky-500/60",
status === "success" && "ring-1 ring-emerald-500/40",
status === "failed" && "ring-2 ring-rose-500",
status === "paused" && "ring-2 ring-amber-500"
)}
>
<div
className={cn(
"flex items-center gap-2 rounded-t-lg px-3 py-2 text-xs font-medium text-white",
entry?.color ?? "bg-destructive"
)}
>
<Icon className="size-3.5" />
<span className="truncate">{entry?.label ?? "Unsupported"}</span>
</div>
<div className="px-3 py-2">
<div className="flex items-center gap-1.5">
<span className="truncate text-sm font-medium">{pn.name}</span>
<StatusIcon status={status} />
<div className="flex items-center justify-between gap-3">
<div className={cn("flex items-center gap-1.5 text-[11px] font-bold uppercase tracking-widest", typeAccent)}>
<Icon className="size-3.5" />
<span>{(entry?.label ?? "Unsupported")}</span>
</div>
<div className="truncate text-[11px] text-muted-foreground">{pn.type}</div>
<StatusPill status={status} />
</div>
<div className="mt-1.5 flex items-center gap-2">
<span className="truncate text-xl font-semibold leading-tight">
{pn.name}
</span>
</div>
{/* Input handle: triggers have no inputs */}
@@ -85,15 +86,75 @@ export function FlowNode({ data, selected }: NodeProps) {
);
}
function StatusIcon({ status }: { status?: NodeRunStatus }) {
if (!status || status === "pending") return null;
if (status === "running")
return <Loader2 className="size-3.5 animate-spin text-sky-500" />;
if (status === "success")
return <CheckCircle2 className="size-3.5 text-emerald-500" />;
if (status === "failed")
return <XCircle className="size-3.5 text-rose-500" />;
if (status === "paused")
return <PauseCircle className="size-3.5 text-amber-500" />;
function StatusPill({ status }: { status?: NodeRunStatus }) {
if (!status || status === "pending") {
return (
<span className="rounded-full border bg-muted/40 px-2.5 py-0.5 text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
pending
</span>
);
}
if (status === "running") {
return (
<span className="inline-flex items-center gap-1 rounded-full border border-sky-500/40 bg-sky-500/15 px-2.5 py-0.5 text-[10px] font-bold uppercase tracking-widest text-sky-700 dark:text-sky-400">
<Loader2 className="size-3 animate-spin" />
running
</span>
);
}
if (status === "success") {
return (
<span className="inline-flex items-center gap-1 rounded-full border border-emerald-500/40 bg-emerald-500/15 px-2.5 py-0.5 text-[10px] font-bold uppercase tracking-widest text-emerald-700 dark:text-emerald-400">
<CheckCircle2 className="size-3" />
succeeded
</span>
);
}
if (status === "failed") {
return (
<span className="inline-flex items-center gap-1 rounded-full border border-rose-500/40 bg-rose-500/15 px-2.5 py-0.5 text-[10px] font-bold uppercase tracking-widest text-rose-700 dark:text-rose-400">
<XCircle className="size-3" />
failed
</span>
);
}
if (status === "paused") {
return (
<span className="inline-flex items-center gap-1 rounded-full border border-amber-500/40 bg-amber-500/15 px-2.5 py-0.5 text-[10px] font-bold uppercase tracking-widest text-amber-700 dark:text-amber-400">
<PauseCircle className="size-3" />
paused
</span>
);
}
return null;
}
// typeAccentClass picks a tailwind text-color class that pairs with the
// node's catalog `color` (bg-X-500) so the TYPE row reads as a distinct
// accent against the card background.
function typeAccentClass(bg?: string): string {
switch (bg) {
case "bg-emerald-500":
return "text-emerald-500";
case "bg-amber-500":
return "text-amber-500";
case "bg-sky-500":
return "text-sky-500";
case "bg-violet-500":
return "text-violet-500";
case "bg-indigo-500":
return "text-indigo-500";
case "bg-fuchsia-500":
return "text-fuchsia-500";
case "bg-rose-500":
case "bg-red-600":
return "text-rose-500";
case "bg-orange-500":
return "text-orange-500";
case "bg-slate-500":
case "bg-slate-400":
return "text-slate-500";
default:
return "text-primary";
}
}