A Kubernetes replica grid with running, pending, and crashloop status cells and a hover or focus detail panel.
npx shadcn@latest add @paragon/pod-grid"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
export type PodStatus = "running" | "pending" | "crashloop" | "terminating";
export interface Pod {
name: string;
status: PodStatus;
node: string;
restarts: number;
age: string;
}
export interface PodGridProps extends React.ComponentProps<"div"> {
pods?: Pod[];
/** Group label, e.g. the deployment / replicaset name. */
title?: string;
}
const STATUS_VAR: Record<PodStatus, string> = {
running: "var(--color-success)",
pending: "var(--color-warning)",
crashloop: "var(--color-destructive)",
terminating: "var(--color-muted-foreground)",
};
const STATUS_LABEL: Record<PodStatus, string> = {
running: "Running",
pending: "Pending",
crashloop: "CrashLoopBackOff",
terminating: "Terminating",
};
function makePods(): Pod[] {
// Deterministic layout: 24 pods, mostly running.
const statuses: PodStatus[] = Array.from({ length: 24 }, (_, i) => {
if (i === 7 || i === 18) return "crashloop";
if (i === 3 || i === 21) return "pending";
if (i === 14) return "terminating";
return "running";
});
const nodes = ["ip-10-0-1-42", "ip-10-0-2-88", "ip-10-0-3-17"];
return statuses.map((status, i) => ({
name: `checkout-7d9f-${(i + 100).toString(36)}${i}`,
status,
node: nodes[i % nodes.length],
restarts: status === "crashloop" ? 6 + (i % 3) : 0,
age: `${2 + (i % 9)}h${(i * 7) % 60}m`,
}));
}
const DEFAULT_PODS = makePods();
export function PodGrid({
pods = DEFAULT_PODS,
title = "checkout-7d9f",
className,
...props
}: PodGridProps) {
const [active, setActive] = React.useState<number | null>(null);
const counts = pods.reduce(
(acc, p) => {
acc[p.status] = (acc[p.status] ?? 0) + 1;
return acc;
},
{} as Record<PodStatus, number>,
);
return (
<div
className={cn(
"w-full max-w-md rounded-xl bg-card p-4 text-card-foreground shadow-border",
className,
)}
{...props}
>
<div className="mb-3 flex items-center justify-between">
<span className="font-mono text-sm font-medium">{title}</span>
<span className="text-xs tabular-nums text-muted-foreground">
{counts.running ?? 0}/{pods.length} ready
</span>
</div>
<div className="relative">
<div className="grid grid-cols-8 gap-1.5">
{pods.map((pod, i) => {
const isActive = active === i;
return (
<button
key={i}
type="button"
aria-label={`${pod.name} — ${STATUS_LABEL[pod.status]}`}
onMouseEnter={() => setActive(i)}
onMouseLeave={() => setActive((a) => (a === i ? null : a))}
onFocus={() => setActive(i)}
onBlur={() => setActive((a) => (a === i ? null : a))}
className="group relative aspect-square rounded-[5px] outline-none transition-[scale] duration-150 ease-out focus-visible:ring-2 focus-visible:ring-ring active:scale-[0.9]"
style={{
background: `color-mix(in oklch, ${STATUS_VAR[pod.status]} ${
isActive ? 100 : 22
}%, var(--color-secondary))`,
}}
>
<span
aria-hidden
className="absolute inset-0 rounded-[5px] transition-opacity duration-150"
style={{
boxShadow: `inset 0 0 0 1px ${STATUS_VAR[pod.status]}`,
opacity: isActive ? 1 : 0.5,
}}
/>
</button>
);
})}
</div>
</div>
{/* detail */}
<div className="mt-3 min-h-[52px] rounded-lg border border-border bg-background p-2.5">
{active != null ? (
<PodDetail pod={pods[active]} />
) : (
<div className="flex flex-wrap gap-x-4 gap-y-1 text-[11px] text-muted-foreground">
{(Object.keys(STATUS_LABEL) as PodStatus[])
.filter((s) => counts[s])
.map((s) => (
<span key={s} className="flex items-center gap-1.5">
<span
className="size-2 rounded-full"
style={{ background: STATUS_VAR[s] }}
/>
{STATUS_LABEL[s]}
<span className="tabular-nums">{counts[s]}</span>
</span>
))}
</div>
)}
</div>
</div>
);
}
function PodDetail({ pod }: { pod: Pod }) {
return (
<div className="flex flex-col gap-1">
<div className="flex items-center gap-1.5">
<span
className="size-2 shrink-0 rounded-full"
style={{ background: STATUS_VAR[pod.status] }}
/>
<span className="truncate font-mono text-xs font-medium">{pod.name}</span>
</div>
<div className="flex flex-wrap gap-x-3 gap-y-0.5 pl-3.5 text-[11px] text-muted-foreground">
<span>{STATUS_LABEL[pod.status]}</span>
<span>{pod.node}</span>
<span className="tabular-nums">restarts {pod.restarts}</span>
<span className="tabular-nums">age {pod.age}</span>
</div>
</div>
);
}