UIPackage
Menu

Framework

Change language

Boilerplate repo

Deployment Pipeline

blockdevops

CI/CD deployment history, active environment status, execution stepper, collapsible build log terminal with stdout timestamps, and filterable deployments table.

Also available for Vue ->

Installation

$npx shadcn@latest add https://uipkge.dev/r/react/deployment-pipeline.json
Named registry:npx shadcn@latest add @uipkge-react/deployment-pipelineInstalls to:components/blocks/

Variants

Loading interactive previews…

Props

NameType / ValuesDefaultRequired
initialEnvDeploymentEnvironmentoptional
initialLogsOpenbooleanoptional
classNamestringoptional

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

DeploymentRecord
interface DeploymentRecord {
  id: string
  commitMessage: string
  commitHash: string
  branch: string
  environment: 'production' | 'preview' | 'staging'
  status: DeploymentStatus
  duration: string
  deployer: {
    name: string
    initials: string
  }
  trigger: DeploymentTrigger
  deployedAt: string
  url: string
}
PipelineStep
interface PipelineStep {
  id: string
  name: string
  duration: string
  description: string
  status: 'completed' | 'in-progress' | 'pending'
}

Files installed (1)

  • components/blocks/DeploymentPipeline.tsx28.7 kB
    'use client'
    
    import * as React from 'react'
    import {
      Check,
      CheckCircle2,
      ChevronDown,
      ChevronUp,
      Clock,
      Copy,
      ExternalLink,
      GitBranch,
      GitCommit,
      Layers,
      Loader2,
      MoreHorizontal,
      Play,
      RotateCcw,
      RotateCw,
      Terminal,
      Undo2,
      XCircle,
    } from 'lucide-react'
    import { cn } from '@/lib/utils'
    import { Avatar, AvatarFallback } from '@/components/ui/avatar'
    import { Badge } from '@/components/ui/badge'
    import { Button } from '@/components/ui/button'
    import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
    import {
      DropdownMenu,
      DropdownMenuContent,
      DropdownMenuItem,
      DropdownMenuLabel,
      DropdownMenuSeparator,
      DropdownMenuTrigger,
    } from '@/components/ui/dropdown-menu'
    import { Separator } from '@/components/ui/separator'
    import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
    import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
    
    export type DeploymentEnvironment = 'all' | 'production' | 'preview' | 'staging'
    export type DeploymentStatus = 'ready' | 'building' | 'failed' | 'rolled-back'
    export type DeploymentTrigger = 'push' | 'manual' | 'rollback'
    
    export interface DeploymentRecord {
      id: string
      commitMessage: string
      commitHash: string
      branch: string
      environment: 'production' | 'preview' | 'staging'
      status: DeploymentStatus
      duration: string
      deployer: {
        name: string
        initials: string
      }
      trigger: DeploymentTrigger
      deployedAt: string
      url: string
    }
    
    export interface PipelineStep {
      id: string
      name: string
      duration: string
      description: string
      status: 'completed' | 'in-progress' | 'pending'
    }
    
    export interface DeploymentPipelineProps {
      initialEnv?: DeploymentEnvironment
      initialLogsOpen?: boolean
      className?: string
    }
    
    const branches = ['main', 'staging', 'preview/safari-tabs', 'preview/e2e-tests']
    
    const pipelineSteps: PipelineStep[] = [
      { id: 'step-1', name: 'Queued', duration: '0.2s', description: 'Worker allocated', status: 'completed' },
      { id: 'step-2', name: 'Build & Bundle', duration: '42s', description: 'Vite & TS compilation', status: 'completed' },
      {
        id: 'step-3',
        name: 'Static Generation',
        duration: '18s',
        description: '42 SSG pages rendered',
        status: 'completed',
      },
      { id: 'step-4', name: 'Edge Propagation', duration: '3s', description: 'Synced to 310 PoPs', status: 'completed' },
      { id: 'step-5', name: 'Ready', duration: 'Instant', description: 'Verified & live', status: 'completed' },
    ]
    
    const terminalLogs = [
      {
        time: '00:00:00.120',
        type: 'info',
        prefix: '❯ [worker]',
        text: 'Initializing deployment worker in region iad-1 (runner-edge-iad-4)...',
      },
      {
        time: '00:00:00.340',
        type: 'info',
        prefix: '❯ [git]',
        text: 'Cloning repository uipkge/core-registry at commit a3f89b1 (branch: main)...',
      },
      {
        time: '00:00:01.050',
        type: 'info',
        prefix: '❯ [cache]',
        text: 'Restoring pnpm store cache (v9.15.0)... (1,482 packages restored, 98.4% cache hit)',
      },
      { time: '00:00:02.410', type: 'cmd', prefix: '$', text: 'pnpm run build:registry && pnpm run build' },
      { time: '00:00:04.180', type: 'stdout', prefix: '  [vite]', text: 'Transformed 482 modules in 312ms' },
      {
        time: '00:00:15.920',
        type: 'stdout',
        prefix: '  [astro]',
        text: 'Building static entrypoints for production (Vue 3.5 + React 19 islands)...',
      },
      { time: '00:00:32.400', type: 'stdout', prefix: '  [astro]', text: '✓ 42 static pages rendered in 16.48s' },
      {
        time: '00:00:44.210',
        type: 'stdout',
        prefix: '  [registry]',
        text: 'Generated 120 manifests into public/r/vue/*.json and public/r/react/*.json',
      },
      {
        time: '00:00:58.800',
        type: 'info',
        prefix: '❯ [edge]',
        text: 'Optimizing edge assets with Brotli compression (saved 4.8 MB)...',
      },
      {
        time: '00:01:01.320',
        type: 'info',
        prefix: '❯ [edge]',
        text: 'Propagating edge routes to Cloudflare global network across 310 PoPs...',
      },
      {
        time: '00:01:03.200',
        type: 'success',
        prefix: '✓ [ready]',
        text: 'Deployment successfully verified and healthy at https://uipkge.dev',
      },
      {
        time: '00:01:03.210',
        type: 'success',
        prefix: '✓ [exit]',
        text: 'Process completed with exit code 0 (success in 1m 03.2s)',
      },
    ]
    
    const deployments: DeploymentRecord[] = [
      {
        id: 'd-1042',
        commitMessage: 'feat(blocks): add ecommerce suite (#412)',
        commitHash: 'a3f89b1',
        branch: 'main',
        environment: 'production',
        status: 'ready',
        duration: '1m 03s',
        deployer: { name: 'Jordan Diaz', initials: 'JD' },
        trigger: 'push',
        deployedAt: '12m ago',
        url: 'https://uipkge.dev',
      },
      {
        id: 'd-1041',
        commitMessage: 'fix(tabs): resolve indicator jitter in safari (#411)',
        commitHash: 'f79b20e',
        branch: 'preview/safari-tabs',
        environment: 'preview',
        status: 'ready',
        duration: '48s',
        deployer: { name: 'Amara Osei', initials: 'AO' },
        trigger: 'push',
        deployedAt: '2h ago',
        url: 'https://uipkge-pr-411.uipkge.dev',
      },
      {
        id: 'd-1040',
        commitMessage: 'refactor(avatar): extract avatar-group primitive (#410)',
        commitHash: '91bc8d3',
        branch: 'staging',
        environment: 'staging',
        status: 'ready',
        duration: '54s',
        deployer: { name: 'Jonas Weber', initials: 'JW' },
        trigger: 'manual',
        deployedAt: '5h ago',
        url: 'https://staging.uipkge.dev',
      },
      {
        id: 'd-1039',
        commitMessage: 'test(e2e): add playwright visual regression suite (#409)',
        commitHash: '62e08a4',
        branch: 'preview/e2e-tests',
        environment: 'preview',
        status: 'building',
        duration: '1m 12s',
        deployer: { name: 'Priya Nair', initials: 'PN' },
        trigger: 'push',
        deployedAt: 'Just now',
        url: 'https://uipkge-pr-409.uipkge.dev',
      },
      {
        id: 'd-1038',
        commitMessage: 'perf(theme): eliminate css custom property recalculation (#408)',
        commitHash: '3d12fa9',
        branch: 'staging',
        environment: 'staging',
        status: 'failed',
        duration: '24s',
        deployer: { name: 'Marcus Lee', initials: 'ML' },
        trigger: 'push',
        deployedAt: 'Yesterday',
        url: 'https://staging-failed.uipkge.dev',
      },
      {
        id: 'd-1037',
        commitMessage: 'chore(deps): update reka-ui to v1.1.0 (#407)',
        commitHash: '180eac2',
        branch: 'main',
        environment: 'production',
        status: 'rolled-back',
        duration: '1m 20s',
        deployer: { name: 'Jordan Diaz', initials: 'JD' },
        trigger: 'rollback',
        deployedAt: '2 days ago',
        url: 'https://uipkge-v2-rollback.uipkge.dev',
      },
    ]
    
    export function DeploymentPipeline({ initialEnv = 'all', initialLogsOpen = true, className }: DeploymentPipelineProps) {
      const [selectedEnv, setSelectedEnv] = React.useState<DeploymentEnvironment>(initialEnv)
      const [selectedBranch, setSelectedBranch] = React.useState<string>('main')
      const [showLogs, setShowLogs] = React.useState<boolean>(initialLogsOpen)
      const [copiedLogs, setCopiedLogs] = React.useState<boolean>(false)
      const [copiedUrlId, setCopiedUrlId] = React.useState<string | null>(null)
      const [isDeploying, setIsDeploying] = React.useState<boolean>(false)
    
      const filteredDeployments =
        selectedEnv === 'all' ? deployments : deployments.filter((d) => d.environment === selectedEnv)
    
      function triggerDeployment() {
        setIsDeploying(true)
        setTimeout(() => {
          setIsDeploying(false)
        }, 1200)
      }
    
      function copyLogs() {
        const text = terminalLogs.map((l) => `[${l.time}] ${l.prefix} ${l.text}`).join('\n')
        if (typeof navigator !== 'undefined' && navigator.clipboard) {
          navigator.clipboard.writeText(text)
          setCopiedLogs(true)
          setTimeout(() => {
            setCopiedLogs(false)
          }, 2000)
        }
      }
    
      function copyUrl(url: string, id: string) {
        if (typeof navigator !== 'undefined' && navigator.clipboard) {
          navigator.clipboard.writeText(url)
          setCopiedUrlId(id)
          setTimeout(() => {
            setCopiedUrlId(null)
          }, 2000)
        }
      }
    
      return (
        <div data-slot="deployment-pipeline" className={cn('w-full space-y-6', className)}>
          {/* Header Section */}
          <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
            <div className="flex items-center gap-3">
              <div className="bg-card flex size-10 items-center justify-center rounded-lg border shadow-xs">
                <Layers className="text-primary size-5" aria-hidden="true" />
              </div>
              <div>
                <h2 className="text-2xl font-bold tracking-tight">Deployments</h2>
                <div className="text-muted-foreground flex items-center gap-1.5 text-xs">
                  <span className="text-foreground font-mono font-medium">uipkge</span>
                  <span>/</span>
                  <span className="font-mono">core-registry</span>
                </div>
              </div>
            </div>
    
            <div className="flex flex-wrap items-center gap-2">
              <DropdownMenu>
                <DropdownMenuTrigger asChild>
                  <Button variant="outline" size="sm" className="font-mono text-xs">
                    <GitBranch className="text-muted-foreground size-3.5" aria-hidden="true" />
                    <span>{selectedBranch}</span>
                    <ChevronDown className="text-muted-foreground size-3.5 opacity-60" aria-hidden="true" />
                  </Button>
                </DropdownMenuTrigger>
                <DropdownMenuContent align="end" className="w-48">
                  <DropdownMenuLabel>Select branch</DropdownMenuLabel>
                  <DropdownMenuSeparator />
                  {branches.map((branch) => (
                    <DropdownMenuItem
                      key={branch}
                      className={selectedBranch === branch ? 'bg-accent font-semibold' : ''}
                      onClick={() => setSelectedBranch(branch)}
                    >
                      <GitBranch className="mr-2 size-3.5" aria-hidden="true" />
                      <span className="font-mono text-xs">{branch}</span>
                    </DropdownMenuItem>
                  ))}
                </DropdownMenuContent>
              </DropdownMenu>
    
              <Button size="sm" disabled={isDeploying} onClick={triggerDeployment}>
                {isDeploying ? (
                  <Loader2 className="size-3.5 animate-spin" aria-hidden="true" />
                ) : (
                  <Play className="size-3.5 fill-current" aria-hidden="true" />
                )}
                {isDeploying ? 'Triggering...' : 'Trigger Deployment'}
              </Button>
            </div>
          </div>
    
          {/* Environment Tabs Navigation */}
          <Tabs
            value={selectedEnv}
            onValueChange={(val) => setSelectedEnv(val as DeploymentEnvironment)}
            className="w-full"
          >
            <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
              <TabsList variant="segmented" className="w-full sm:w-auto">
                <TabsTrigger value="all">All</TabsTrigger>
                <TabsTrigger value="production">Production</TabsTrigger>
                <TabsTrigger value="preview">Preview</TabsTrigger>
                <TabsTrigger value="staging">Staging</TabsTrigger>
              </TabsList>
              <span className="text-muted-foreground text-xs tabular-nums">
                Showing {filteredDeployments.length} of {deployments.length} deployments
              </span>
            </div>
          </Tabs>
    
          {/* Latest Deployment Hero Card */}
          <Card className="border-border shadow-xs">
            <CardHeader className="space-y-4 pb-4">
              <div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
                <div className="space-y-1.5">
                  <div className="flex flex-wrap items-center gap-2">
                    <CardTitle className="text-base font-semibold">Latest Deployment</CardTitle>
                    <div className="border-success/30 bg-success/10 text-success inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-xs font-medium">
                      <span className="relative flex size-2 shrink-0">
                        <span className="bg-success absolute inline-flex h-full w-full rounded-full opacity-75" />
                        <span className="bg-success relative inline-flex size-2 rounded-full" />
                      </span>
                      Live · Production
                    </div>
                  </div>
                  <CardDescription>
                    <a
                      href="https://uipkge.dev"
                      target="_blank"
                      rel="noopener noreferrer"
                      className="text-foreground hover:text-primary inline-flex min-h-6 items-center gap-1 py-0.5 font-mono text-xs font-medium underline underline-offset-4 transition-colors"
                    >
                      https://uipkge.dev
                      <ExternalLink className="text-muted-foreground size-3" aria-hidden="true" />
                    </a>
                  </CardDescription>
                </div>
    
                <div className="flex flex-wrap items-center gap-2">
                  <Button variant="outline" size="sm" onClick={() => setShowLogs(!showLogs)}>
                    <Terminal className="size-3.5" aria-hidden="true" />
                    <span>{showLogs ? 'Hide Build Logs' : 'View Build Logs'}</span>
                    {showLogs ? (
                      <ChevronUp className="text-muted-foreground size-3.5" aria-hidden="true" />
                    ) : (
                      <ChevronDown className="text-muted-foreground size-3.5" aria-hidden="true" />
                    )}
                  </Button>
                  <Button variant="outline" size="sm">
                    <RotateCw className="size-3.5" aria-hidden="true" />
                    <span>Redeploy</span>
                  </Button>
                  <Button variant="outline" size="sm">
                    <Undo2 className="size-3.5" aria-hidden="true" />
                    <span>Instant Rollback</span>
                  </Button>
                </div>
              </div>
    
              {/* Commit Details Bar */}
              <div className="bg-muted/40 flex flex-wrap items-center gap-x-4 gap-y-2 rounded-lg border px-3.5 py-2.5 text-xs">
                <div className="flex items-center gap-2">
                  <Avatar className="size-5">
                    <AvatarFallback className="text-xs font-medium">JD</AvatarFallback>
                  </Avatar>
                  <span className="text-foreground font-medium">Jordan Diaz</span>
                </div>
                <Separator orientation="vertical" className="hidden h-3.5 sm:block" />
                <span className="text-foreground font-medium">feat(blocks): add ecommerce suite (#412)</span>
                <Separator orientation="vertical" className="hidden h-3.5 sm:block" />
                <div className="text-muted-foreground flex items-center gap-1 font-mono">
                  <GitCommit className="size-3.5" aria-hidden="true" />
                  <span>a3f89b1</span>
                </div>
                <Separator orientation="vertical" className="hidden h-3.5 sm:block" />
                <div className="text-muted-foreground flex items-center gap-1 font-mono">
                  <GitBranch className="size-3.5" aria-hidden="true" />
                  <span>main</span>
                </div>
                <Separator orientation="vertical" className="hidden h-3.5 sm:block" />
                <div className="text-muted-foreground flex items-center gap-1">
                  <Clock className="size-3.5" aria-hidden="true" />
                  <span>Deployed 12m ago</span>
                </div>
              </div>
            </CardHeader>
    
            <CardContent className="space-y-4 pt-0">
              {/* Execution Stepper */}
              <div className="space-y-2.5">
                <div className="flex items-center justify-between">
                  <span className="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
                    Pipeline Execution
                  </span>
                  <span className="text-muted-foreground font-mono text-xs">5 of 5 steps completed · 1m 03s</span>
                </div>
    
                <div className="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-5">
                  {pipelineSteps.map((step, index) => (
                    <div
                      key={step.id}
                      className="bg-card/60 hover:bg-muted/40 flex flex-col justify-between rounded-lg border p-3 shadow-2xs transition-colors"
                    >
                      <div className="flex items-center justify-between">
                        <span className="bg-success/15 text-success flex size-5 items-center justify-center rounded-full">
                          <Check className="size-3 stroke-[3]" aria-hidden="true" />
                        </span>
                        <span className="text-muted-foreground font-mono text-xs">{step.duration}</span>
                      </div>
                      <div className="mt-2">
                        <p className="text-foreground text-xs font-semibold">
                          {index + 1}. {step.name}
                        </p>
                        <p className="text-muted-foreground truncate text-xs">{step.description}</p>
                      </div>
                    </div>
                  ))}
                </div>
              </div>
    
              {/* Collapsible Dark Terminal / Build Log Viewer */}
              {showLogs && (
                <div className="overflow-hidden rounded-lg border border-zinc-800 bg-zinc-950 font-mono text-xs shadow-inner">
                  <div className="flex items-center justify-between border-b border-zinc-800 bg-zinc-900/90 px-3.5 py-2">
                    <div className="flex items-center gap-2">
                      <div className="flex items-center gap-1.5">
                        <span className="bg-destructive/80 size-2.5 rounded-full" />
                        <span className="bg-warning/80 size-2.5 rounded-full" />
                        <span className="bg-success/80 size-2.5 rounded-full" />
                      </div>
                      <span className="text-muted-foreground text-xs">
                        build-stdout — node v22.12.0 — runner-edge-iad-4
                      </span>
                    </div>
                    <div className="flex items-center gap-2">
                      <span className="text-muted-foreground text-muted-foreground hidden text-xs sm:inline">
                        Total: 1m 03.2s
                      </span>
                      <Button
                        variant="ghost"
                        size="xs"
                        className="h-6 gap-1 px-2 text-zinc-300 hover:bg-zinc-800 hover:text-zinc-100"
                        onClick={copyLogs}
                      >
                        {copiedLogs ? (
                          <Check className="text-success size-3" aria-hidden="true" />
                        ) : (
                          <Copy className="size-3" aria-hidden="true" />
                        )}
                        <span>{copiedLogs ? 'Copied' : 'Copy logs'}</span>
                      </Button>
                    </div>
                  </div>
    
                  <div className="max-h-72 space-y-1 overflow-y-auto p-4 select-text">
                    {terminalLogs.map((line, idx) => (
                      <div key={idx} className="flex items-start gap-2 leading-relaxed">
                        <span className="text-muted-foreground select-none">[{line.time}]</span>
                        <span
                          className={cn(
                            'font-medium select-none',
                            line.type === 'success' && 'text-success',
                            line.type === 'info' && 'text-info',
                            line.type === 'cmd' && 'text-warning',
                            line.type === 'stdout' && 'text-muted-foreground',
                          )}
                        >
                          {line.prefix}
                        </span>
                        <span
                          className={cn(
                            line.type === 'success' && 'text-success font-medium',
                            line.type === 'cmd' && 'font-semibold text-zinc-100',
                            line.type === 'info' && 'text-zinc-300',
                            line.type === 'stdout' && 'text-zinc-300',
                          )}
                        >
                          {line.text}
                        </span>
                      </div>
                    ))}
                  </div>
    
                  <div className="text-muted-foreground flex items-center justify-between border-t border-zinc-800 bg-zinc-900/60 px-3.5 py-1.5 text-xs">
                    <div className="flex items-center gap-2">
                      <span className="bg-success/10 text-success inline-flex items-center gap-1 rounded px-1.5 py-0.5 font-medium">
                        <Check className="size-3" aria-hidden="true" /> Exit code: 0
                      </span>
                      <span>Clean build</span>
                    </div>
                    <span className="text-muted-foreground">Memory: 412 MB / 2048 MB</span>
                  </div>
                </div>
              )}
            </CardContent>
          </Card>
    
          {/* Deployment History Table */}
          <Card className="border-border shadow-xs">
            <CardHeader className="pb-3">
              <CardTitle className="text-base font-semibold">Deployment History</CardTitle>
              <CardDescription className="text-xs">
                Past deployments across production, preview, and staging environments.
              </CardDescription>
            </CardHeader>
            <CardContent className="p-0">
              <div className="overflow-x-auto">
                <Table>
                  <TableHeader>
                    <TableRow>
                      <TableHead className="w-10">
                        <span className="sr-only">Status</span>
                      </TableHead>
                      <TableHead>Commit & Message</TableHead>
                      <TableHead>Environment</TableHead>
                      <TableHead>Duration</TableHead>
                      <TableHead>Deployer</TableHead>
                      <TableHead>Trigger</TableHead>
                      <TableHead className="text-right">Deployed</TableHead>
                      <TableHead className="w-10">
                        <span className="sr-only">Actions</span>
                      </TableHead>
                    </TableRow>
                  </TableHeader>
                  <TableBody>
                    {filteredDeployments.map((deployment) => (
                      <TableRow key={deployment.id} className="hover:bg-muted/50">
                        <TableCell className="py-3">
                          {deployment.status === 'ready' && (
                            <span title="Ready">
                              <CheckCircle2 className="text-success size-4" aria-hidden="true" />
                            </span>
                          )}
                          {deployment.status === 'building' && (
                            <span title="Building">
                              <Loader2 className="text-info size-4 animate-spin" aria-hidden="true" />
                            </span>
                          )}
                          {deployment.status === 'failed' && (
                            <span title="Failed">
                              <XCircle className="text-destructive size-4" aria-hidden="true" />
                            </span>
                          )}
                          {deployment.status === 'rolled-back' && (
                            <span title="Rolled back">
                              <Undo2 className="text-warning size-4" aria-hidden="true" />
                            </span>
                          )}
                        </TableCell>
                        <TableCell>
                          <div className="min-w-[220px]">
                            <p className="text-foreground max-w-sm truncate text-sm font-medium">
                              {deployment.commitMessage}
                            </p>
                            <div className="text-muted-foreground mt-0.5 flex items-center gap-2 font-mono text-xs">
                              <span className="flex items-center gap-1">
                                <GitBranch className="size-3" aria-hidden="true" />
                                {deployment.branch}
                              </span>
                              <span>·</span>
                              <span className="flex items-center gap-1">
                                <GitCommit className="size-3" aria-hidden="true" />
                                {deployment.commitHash}
                              </span>
                            </div>
                          </div>
                        </TableCell>
                        <TableCell>
                          <Badge
                            variant={
                              deployment.environment === 'production'
                                ? 'default'
                                : deployment.environment === 'preview'
                                  ? 'secondary'
                                  : 'outline'
                            }
                            className="text-xs capitalize"
                          >
                            {deployment.environment}
                          </Badge>
                        </TableCell>
                        <TableCell className="text-muted-foreground font-mono text-xs tabular-nums">
                          {deployment.duration}
                        </TableCell>
                        <TableCell>
                          <div className="flex items-center gap-2">
                            <Avatar className="size-6">
                              <AvatarFallback className="text-xs font-medium">
                                {deployment.deployer.initials}
                              </AvatarFallback>
                            </Avatar>
                            <span className="text-foreground text-xs">{deployment.deployer.name}</span>
                          </div>
                        </TableCell>
                        <TableCell>
                          <div className="text-muted-foreground flex items-center gap-1.5 text-xs">
                            {deployment.trigger === 'push' && <GitCommit className="size-3.5" aria-hidden="true" />}
                            {deployment.trigger === 'manual' && <Play className="size-3.5" aria-hidden="true" />}
                            {deployment.trigger === 'rollback' && <RotateCcw className="size-3.5" aria-hidden="true" />}
                            <span className="capitalize">{deployment.trigger}</span>
                          </div>
                        </TableCell>
                        <TableCell className="text-muted-foreground text-right text-xs whitespace-nowrap tabular-nums">
                          {deployment.deployedAt}
                        </TableCell>
                        <TableCell>
                          <DropdownMenu>
                            <DropdownMenuTrigger asChild>
                              <Button variant="ghost" size="icon-sm" className="text-muted-foreground">
                                <MoreHorizontal className="size-4" aria-hidden="true" />
                                <span className="sr-only">Open actions for {deployment.id}</span>
                              </Button>
                            </DropdownMenuTrigger>
                            <DropdownMenuContent align="end">
                              <DropdownMenuLabel>Actions</DropdownMenuLabel>
                              <DropdownMenuSeparator />
                              <DropdownMenuItem>
                                <Undo2 className="mr-2 size-4" aria-hidden="true" />
                                Rollback to this deploy
                              </DropdownMenuItem>
                              <DropdownMenuItem onClick={() => copyUrl(deployment.url, deployment.id)}>
                                {copiedUrlId === deployment.id ? (
                                  <Check className="text-success mr-2 size-4" aria-hidden="true" />
                                ) : (
                                  <Copy className="mr-2 size-4" aria-hidden="true" />
                                )}
                                {copiedUrlId === deployment.id ? 'Copied URL' : 'Copy deployment URL'}
                              </DropdownMenuItem>
                              <DropdownMenuItem onClick={() => setShowLogs(true)}>
                                <Terminal className="mr-2 size-4" aria-hidden="true" />
                                Inspect build logs
                              </DropdownMenuItem>
                              <DropdownMenuItem asChild>
                                <a
                                  href={deployment.url}
                                  target="_blank"
                                  rel="noopener noreferrer"
                                  className="flex items-center"
                                >
                                  <ExternalLink className="mr-2 size-4" aria-hidden="true" />
                                  View live preview
                                </a>
                              </DropdownMenuItem>
                            </DropdownMenuContent>
                          </DropdownMenu>
                        </TableCell>
                      </TableRow>
                    ))}
                    {filteredDeployments.length === 0 && (
                      <TableRow>
                        <TableCell colSpan={8} className="text-muted-foreground h-24 text-center text-sm">
                          No deployments match the selected environment filter.
                        </TableCell>
                      </TableRow>
                    )}
                  </TableBody>
                </Table>
              </div>
            </CardContent>
          </Card>
        </div>
      )
    }
    

Raw manifest:https://uipkge.dev/r/react/deployment-pipeline.json