-

Run Node List

PreviousNext

A workflow run timeline showing node status, duration, and execution summaries.

Installation

pnpm
pnpm dlx shadcn@latest add https://ui.zgi.ai/r/run-node-list.json
npm
pnpm dlx shadcn@latest add https://ui.zgi.ai/r/run-node-list.json
yarn
yarn dlx shadcn@latest add https://ui.zgi.ai/r/run-node-list.json

Component

components/ui/run-node-list.tsx
"use client"

import * as React from "react"
import {
  AlertCircleIcon,
  CheckCircle2Icon,
  Clock3Icon,
  Loader2Icon,
} from "lucide-react"

import { cn } from "@/lib/utils"
import { Badge } from "@/components/ui/badge"

export type RunNodeStatus = "pending" | "running" | "success" | "error"

export interface RunNodeItem {
  id: string
  title: React.ReactNode
  type?: React.ReactNode
  status: RunNodeStatus
  duration?: React.ReactNode
  summary?: React.ReactNode
}

export interface RunNodeListProps
  extends Omit<React.ComponentProps<"div">, "title"> {
  nodes: RunNodeItem[]
  title?: React.ReactNode
  activeId?: string
}

const statusConfig: Record<
  RunNodeStatus,
  {
    label: string
    icon: React.ComponentType<{ className?: string }>
    className: string
  }
> = {
  pending: {
    label: "Pending",
    icon: Clock3Icon,
    className: "border-border bg-muted text-muted-foreground",
  },
  running: {
    label: "Running",
    icon: Loader2Icon,
    className:
      "border-blue-500/20 bg-blue-500/10 text-blue-700 dark:text-blue-300",
  },
  success: {
    label: "Success",
    icon: CheckCircle2Icon,
    className:
      "border-emerald-500/20 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300",
  },
  error: {
    label: "Error",
    icon: AlertCircleIcon,
    className: "border-destructive/20 bg-destructive/10 text-destructive",
  },
}

export function RunNodeList({
  nodes,
  title = "Run nodes",
  activeId,
  className,
  ...props
}: RunNodeListProps) {
  return (
    <div
      className={cn(
        "border-border bg-card text-card-foreground overflow-hidden rounded-lg border shadow-sm",
        className
      )}
      {...props}
    >
      <div className="border-border flex min-h-11 items-center justify-between border-b px-3">
        <p className="text-sm font-medium">{title}</p>
        <Badge variant="secondary" className="rounded-md">
          {nodes.length}
        </Badge>
      </div>
      <div className="divide-border divide-y">
        {nodes.map((node, index) => {
          const config = statusConfig[node.status]
          const Icon = config.icon

          return (
            <div
              key={node.id}
              className={cn(
                "flex gap-3 p-3",
                activeId === node.id && "bg-accent/40"
              )}
            >
              <div className="flex flex-col items-center">
                <span
                  className={cn(
                    "flex size-7 items-center justify-center rounded-full border",
                    config.className
                  )}
                >
                  <Icon
                    className={cn(
                      "size-3.5",
                      node.status === "running" && "animate-spin"
                    )}
                  />
                </span>
                {index < nodes.length - 1 ? (
                  <span className="bg-border mt-2 h-full w-px" />
                ) : null}
              </div>
              <div className="min-w-0 flex-1 pb-1">
                <div className="flex min-w-0 items-center gap-2">
                  <p className="truncate text-sm font-medium">{node.title}</p>
                  {node.type ? (
                    <Badge variant="outline" className="h-6 rounded-md px-2">
                      {node.type}
                    </Badge>
                  ) : null}
                  {node.duration ? (
                    <span className="text-muted-foreground ml-auto shrink-0 text-xs">
                      {node.duration}
                    </span>
                  ) : null}
                </div>
                {node.summary ? (
                  <p className="text-muted-foreground mt-1 line-clamp-2 text-xs leading-5">
                    {node.summary}
                  </p>
                ) : null}
              </div>
            </div>
          )
        })}
      </div>
    </div>
  )
}

Usage

import { RunNodeList } from "@/components/ui/run-node-list"
 
export function Example() {
  return (
    <RunNodeList
      nodes={[{ id: "start", title: "Start", status: "success" }]}
    />
  )
}