Infrastructure & DevOps

Pod Grid

A Kubernetes replica grid with running, pending, and crashloop status cells and a hover or focus detail panel.

Install

npx shadcn@latest add @paragon/pod-grid

pod-grid.tsx

"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>
  );
}