Menu

Change language

GitHub repo

Deployment Pipeline

blockdevops

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

Installation

$npx uipkge-ng@latest add deployment-pipeline
Manifest:https://uipkge.dev/r/angular/deployment-pipeline.jsonInstalls to:src/app/components/blocks/deployment-pipeline/

Variants

Loading interactive previews…

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'
}
TerminalLogLine
interface TerminalLogLine {
  time: string
  type: TerminalLogType
  prefix: string
  text: string
}

Files installed (2)

  • src/app/components/blocks/deployment-pipeline/deployment-pipeline.component.ts32.2 kB
    import { ChangeDetectionStrategy, Component, DestroyRef, Input, OnInit, computed, inject, signal } from '@angular/core'
    import {
      Check,
      CheckCircle2,
      ChevronDown,
      ChevronUp,
      Clock,
      Copy,
      ExternalLink,
      GitBranch,
      GitCommit,
      Layers,
      Loader2,
      LucideAngularModule,
      MoreHorizontal,
      Play,
      RotateCcw,
      RotateCw,
      Terminal,
      Undo2,
      XCircle,
    } from 'lucide-angular'
    import { cn } from '@/lib/utils'
    import { UiAvatarComponent, UiAvatarFallbackComponent } from '@/ui/avatar/avatar.component'
    import { UiBadgeComponent } from '@/ui/badge/badge.component'
    import { UiButtonComponent } from '@/ui/button/button.component'
    import {
      UiCardComponent,
      UiCardContentComponent,
      UiCardDescriptionComponent,
      UiCardHeaderComponent,
      UiCardTitleComponent,
    } from '@/ui/card/card.component'
    import {
      UiDropdownMenuComponent,
      UiDropdownMenuContentComponent,
      UiDropdownMenuItemComponent,
      UiDropdownMenuLabelComponent,
      UiDropdownMenuSeparatorComponent,
      UiDropdownMenuTriggerComponent,
    } from '@/ui/dropdown-menu/dropdown-menu.component'
    import { UiSeparatorComponent } from '@/ui/separator/separator.component'
    import {
      UiTableBodyComponent,
      UiTableCellComponent,
      UiTableComponent,
      UiTableHeadComponent,
      UiTableHeaderComponent,
      UiTableRowComponent,
    } from '@/ui/table/table.component'
    import { UiTabsComponent, UiTabsListComponent, UiTabsTriggerComponent } from '@/ui/tabs/tabs.component'
    
    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 type TerminalLogType = 'info' | 'cmd' | 'stdout' | 'success'
    
    export interface TerminalLogLine {
      time: string
      type: TerminalLogType
      prefix: string
      text: 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: TerminalLogLine[] = [
      {
        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',
      },
    ]
    
    /**
     * CI/CD deployments dashboard: environment tabs, latest-deployment hero with the
     * execution stepper and a collapsible build-log terminal, and a filterable history
     * table with per-row actions. Edit the data consts above to wire it to real data.
     */
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-deployment-pipeline, [ui-deployment-pipeline]',
      standalone: true,
      // React renders the root <div> itself: keep this wrapper out of the layout.
      host: { class: 'contents' },
      imports: [
        LucideAngularModule,
        UiAvatarComponent,
        UiAvatarFallbackComponent,
        UiBadgeComponent,
        UiButtonComponent,
        UiCardComponent,
        UiCardContentComponent,
        UiCardDescriptionComponent,
        UiCardHeaderComponent,
        UiCardTitleComponent,
        UiDropdownMenuComponent,
        UiDropdownMenuContentComponent,
        UiDropdownMenuItemComponent,
        UiDropdownMenuLabelComponent,
        UiDropdownMenuSeparatorComponent,
        UiDropdownMenuTriggerComponent,
        UiSeparatorComponent,
        UiTableComponent,
        UiTableHeaderComponent,
        UiTableBodyComponent,
        UiTableRowComponent,
        UiTableHeadComponent,
        UiTableCellComponent,
        UiTabsComponent,
        UiTabsListComponent,
        UiTabsTriggerComponent,
      ],
      template: `
        <div data-slot="deployment-pipeline" [class]="rootClass">
          <!-- Header Section -->
          <div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
            <div class="flex items-center gap-3">
              <div class="bg-card flex size-10 items-center justify-center rounded-lg border shadow-xs">
                <lucide-icon [img]="Layers" class="text-primary size-5" aria-hidden="true" />
              </div>
              <div>
                <h2 class="text-2xl font-bold tracking-tight">Deployments</h2>
                <div class="text-muted-foreground flex items-center gap-1.5 text-xs">
                  <span class="text-foreground font-mono font-medium">uipkge</span>
                  <span>/</span>
                  <span class="font-mono">core-registry</span>
                </div>
              </div>
            </div>
    
            <div class="flex flex-wrap items-center gap-2">
              <ui-dropdown-menu>
                <button ui-button ui-dropdown-menu-trigger variant="outline" size="sm" class="font-mono text-xs">
                  <lucide-icon [img]="GitBranch" class="text-muted-foreground size-3.5" aria-hidden="true" />
                  <span>{{ selectedBranch() }}</span>
                  <lucide-icon [img]="ChevronDown" class="text-muted-foreground size-3.5 opacity-60" aria-hidden="true" />
                </button>
                <ui-dropdown-menu-content align="end" class="w-48">
                  <ui-dropdown-menu-label>Select branch</ui-dropdown-menu-label>
                  <ui-dropdown-menu-separator />
                  @for (branch of branches; track branch) {
                    <ui-dropdown-menu-item
                      [class]="selectedBranch() === branch ? 'bg-accent font-semibold' : ''"
                      (click)="selectedBranch.set(branch)"
                    >
                      <lucide-icon [img]="GitBranch" class="mr-2 size-3.5" aria-hidden="true" />
                      <span class="font-mono text-xs">{{ branch }}</span>
                    </ui-dropdown-menu-item>
                  }
                </ui-dropdown-menu-content>
              </ui-dropdown-menu>
    
              <button ui-button size="sm" [disabled]="isDeploying()" (click)="triggerDeployment()">
                @if (isDeploying()) {
                  <lucide-icon [img]="Loader2" class="size-3.5 animate-spin" aria-hidden="true" />
                } @else {
                  <lucide-icon [img]="Play" class="size-3.5 fill-current" aria-hidden="true" />
                }
                {{ isDeploying() ? 'Triggering...' : 'Trigger Deployment' }}
              </button>
            </div>
          </div>
    
          <!-- Environment Tabs Navigation -->
          <ui-tabs [value]="selectedEnv()" (valueChange)="setEnv($event)" class="w-full">
            <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
              <ui-tabs-list variant="segmented" class="w-full sm:w-auto">
                <ui-tabs-trigger value="all">All</ui-tabs-trigger>
                <ui-tabs-trigger value="production">Production</ui-tabs-trigger>
                <ui-tabs-trigger value="preview">Preview</ui-tabs-trigger>
                <ui-tabs-trigger value="staging">Staging</ui-tabs-trigger>
              </ui-tabs-list>
              <span class="text-muted-foreground text-xs tabular-nums">
                Showing {{ filteredDeployments().length }} of {{ deployments.length }} deployments
              </span>
            </div>
          </ui-tabs>
    
          <!-- Latest Deployment Hero Card -->
          <ui-card class="border-border shadow-xs">
            <ui-card-header class="space-y-4 pb-4">
              <div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
                <div class="space-y-1.5">
                  <div class="flex flex-wrap items-center gap-2">
                    <ui-card-title class="text-base font-semibold">Latest Deployment</ui-card-title>
                    <div
                      class="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 class="relative flex size-2 shrink-0">
                        <span class="bg-success absolute inline-flex h-full w-full rounded-full opacity-75"></span>
                        <span class="bg-success relative inline-flex size-2 rounded-full"></span>
                      </span>
                      Live · Production
                    </div>
                  </div>
                  <ui-card-description>
                    <a
                      href="https://uipkge.dev"
                      target="_blank"
                      rel="noopener noreferrer"
                      class="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
                      <lucide-icon [img]="ExternalLink" class="text-muted-foreground size-3" aria-hidden="true" />
                    </a>
                  </ui-card-description>
                </div>
    
                <div class="flex flex-wrap items-center gap-2">
                  <button ui-button variant="outline" size="sm" (click)="showLogs.set(!showLogs())">
                    <lucide-icon [img]="Terminal" class="size-3.5" aria-hidden="true" />
                    <span>{{ showLogs() ? 'Hide Build Logs' : 'View Build Logs' }}</span>
                    @if (showLogs()) {
                      <lucide-icon [img]="ChevronUp" class="text-muted-foreground size-3.5" aria-hidden="true" />
                    } @else {
                      <lucide-icon [img]="ChevronDown" class="text-muted-foreground size-3.5" aria-hidden="true" />
                    }
                  </button>
                  <button ui-button variant="outline" size="sm">
                    <lucide-icon [img]="RotateCw" class="size-3.5" aria-hidden="true" />
                    <span>Redeploy</span>
                  </button>
                  <button ui-button variant="outline" size="sm">
                    <lucide-icon [img]="Undo2" class="size-3.5" aria-hidden="true" />
                    <span>Instant Rollback</span>
                  </button>
                </div>
              </div>
    
              <!-- Commit Details Bar -->
              <div class="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 class="flex items-center gap-2">
                  <ui-avatar class="size-5">
                    <ui-avatar-fallback class="text-xs font-medium">JD</ui-avatar-fallback>
                  </ui-avatar>
                  <span class="text-foreground font-medium">Jordan Diaz</span>
                </div>
                <ui-separator orientation="vertical" class="hidden h-3.5 sm:block" />
                <span class="text-foreground font-medium">feat(blocks): add ecommerce suite (#412)</span>
                <ui-separator orientation="vertical" class="hidden h-3.5 sm:block" />
                <div class="text-muted-foreground flex items-center gap-1 font-mono">
                  <lucide-icon [img]="GitCommit" class="size-3.5" aria-hidden="true" />
                  <span>a3f89b1</span>
                </div>
                <ui-separator orientation="vertical" class="hidden h-3.5 sm:block" />
                <div class="text-muted-foreground flex items-center gap-1 font-mono">
                  <lucide-icon [img]="GitBranch" class="size-3.5" aria-hidden="true" />
                  <span>main</span>
                </div>
                <ui-separator orientation="vertical" class="hidden h-3.5 sm:block" />
                <div class="text-muted-foreground flex items-center gap-1">
                  <lucide-icon [img]="Clock" class="size-3.5" aria-hidden="true" />
                  <span>Deployed 12m ago</span>
                </div>
              </div>
            </ui-card-header>
    
            <ui-card-content class="space-y-4 pt-0">
              <!-- Execution Stepper -->
              <div class="space-y-2.5">
                <div class="flex items-center justify-between">
                  <span class="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
                    Pipeline Execution
                  </span>
                  <span class="text-muted-foreground font-mono text-xs">5 of 5 steps completed · 1m 03s</span>
                </div>
    
                <div class="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-5">
                  @for (step of pipelineSteps; track step.id; let index = $index) {
                    <div
                      class="bg-card/60 hover:bg-muted/40 flex flex-col justify-between rounded-lg border p-3 shadow-2xs transition-colors"
                    >
                      <div class="flex items-center justify-between">
                        <span class="bg-success/15 text-success flex size-5 items-center justify-center rounded-full">
                          <lucide-icon [img]="Check" class="size-3 stroke-[3]" aria-hidden="true" />
                        </span>
                        <span class="text-muted-foreground font-mono text-xs">{{ step.duration }}</span>
                      </div>
                      <div class="mt-2">
                        <p class="text-foreground text-xs font-semibold">{{ index + 1 }}. {{ step.name }}</p>
                        <p class="text-muted-foreground truncate text-xs">{{ step.description }}</p>
                      </div>
                    </div>
                  }
                </div>
              </div>
    
              <!-- Collapsible Dark Terminal / Build Log Viewer -->
              @if (showLogs()) {
                <div class="overflow-hidden rounded-lg border border-zinc-800 bg-zinc-950 font-mono text-xs shadow-inner">
                  <div class="flex items-center justify-between border-b border-zinc-800 bg-zinc-900/90 px-3.5 py-2">
                    <div class="flex items-center gap-2">
                      <div class="flex items-center gap-1.5">
                        <span class="bg-destructive/80 size-2.5 rounded-full"></span>
                        <span class="bg-warning/80 size-2.5 rounded-full"></span>
                        <span class="bg-success/80 size-2.5 rounded-full"></span>
                      </div>
                      <span class="text-muted-foreground text-xs"> build-stdout — node v22.12.0 — runner-edge-iad-4 </span>
                    </div>
                    <div class="flex items-center gap-2">
                      <span class="text-muted-foreground hidden text-xs sm:inline">Total: 1m 03.2s</span>
                      <button
                        ui-button
                        variant="ghost"
                        size="xs"
                        class="h-6 gap-1 px-2 text-zinc-300 hover:bg-zinc-800 hover:text-zinc-100"
                        (click)="copyLogs()"
                      >
                        @if (copiedLogs()) {
                          <lucide-icon [img]="Check" class="text-success size-3" aria-hidden="true" />
                        } @else {
                          <lucide-icon [img]="Copy" class="size-3" aria-hidden="true" />
                        }
                        <span>{{ copiedLogs() ? 'Copied' : 'Copy logs' }}</span>
                      </button>
                    </div>
                  </div>
    
                  <div class="max-h-72 space-y-1 overflow-y-auto p-4 select-text">
                    @for (line of terminalLogs; track $index) {
                      <div class="flex items-start gap-2 leading-relaxed">
                        <span class="text-muted-foreground select-none">[{{ line.time }}]</span>
                        <span [class]="prefixClass(line.type)">{{ line.prefix }}</span>
                        <span [class]="textClass(line.type)">{{ line.text }}</span>
                      </div>
                    }
                  </div>
    
                  <div
                    class="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 class="flex items-center gap-2">
                      <span
                        class="bg-success/10 text-success inline-flex items-center gap-1 rounded px-1.5 py-0.5 font-medium"
                      >
                        <lucide-icon [img]="Check" class="size-3" aria-hidden="true" /> Exit code: 0
                      </span>
                      <span>Clean build</span>
                    </div>
                    <span class="text-muted-foreground">Memory: 412 MB / 2048 MB</span>
                  </div>
                </div>
              }
            </ui-card-content>
          </ui-card>
    
          <!-- Deployment History Table -->
          <ui-card class="border-border shadow-xs">
            <ui-card-header class="pb-3">
              <ui-card-title class="text-base font-semibold">Deployment History</ui-card-title>
              <ui-card-description class="text-xs">
                Past deployments across production, preview, and staging environments.
              </ui-card-description>
            </ui-card-header>
            <ui-card-content class="p-0">
              <div class="overflow-x-auto">
                <div ui-table>
                  <thead ui-table-header>
                    <tr ui-table-row>
                      <th ui-table-head class="w-10">
                        <span class="sr-only">Status</span>
                      </th>
                      <th ui-table-head>Commit &amp; Message</th>
                      <th ui-table-head>Environment</th>
                      <th ui-table-head>Duration</th>
                      <th ui-table-head>Deployer</th>
                      <th ui-table-head>Trigger</th>
                      <th ui-table-head class="text-right">Deployed</th>
                      <th ui-table-head class="w-10">
                        <span class="sr-only">Actions</span>
                      </th>
                    </tr>
                  </thead>
                  <tbody ui-table-body>
                    @for (deployment of filteredDeployments(); track deployment.id) {
                      <tr ui-table-row class="hover:bg-muted/50">
                        <td ui-table-cell class="py-3">
                          @switch (deployment.status) {
                            @case ('ready') {
                              <span title="Ready">
                                <lucide-icon [img]="CheckCircle2" class="text-success size-4" aria-hidden="true" />
                              </span>
                            }
                            @case ('building') {
                              <span title="Building">
                                <lucide-icon [img]="Loader2" class="text-info size-4 animate-spin" aria-hidden="true" />
                              </span>
                            }
                            @case ('failed') {
                              <span title="Failed">
                                <lucide-icon [img]="XCircle" class="text-destructive size-4" aria-hidden="true" />
                              </span>
                            }
                            @case ('rolled-back') {
                              <span title="Rolled back">
                                <lucide-icon [img]="Undo2" class="text-warning size-4" aria-hidden="true" />
                              </span>
                            }
                          }
                        </td>
                        <td ui-table-cell>
                          <div class="min-w-[220px]">
                            <p class="text-foreground max-w-sm truncate text-sm font-medium">
                              {{ deployment.commitMessage }}
                            </p>
                            <div class="text-muted-foreground mt-0.5 flex items-center gap-2 font-mono text-xs">
                              <span class="flex items-center gap-1">
                                <lucide-icon [img]="GitBranch" class="size-3" aria-hidden="true" />
                                {{ deployment.branch }}
                              </span>
                              <span>·</span>
                              <span class="flex items-center gap-1">
                                <lucide-icon [img]="GitCommit" class="size-3" aria-hidden="true" />
                                {{ deployment.commitHash }}
                              </span>
                            </div>
                          </div>
                        </td>
                        <td ui-table-cell>
                          <span ui-badge [variant]="envBadgeVariant(deployment)" class="text-xs capitalize">
                            {{ deployment.environment }}
                          </span>
                        </td>
                        <td ui-table-cell class="text-muted-foreground font-mono text-xs tabular-nums">
                          {{ deployment.duration }}
                        </td>
                        <td ui-table-cell>
                          <div class="flex items-center gap-2">
                            <ui-avatar class="size-6">
                              <ui-avatar-fallback class="text-xs font-medium">
                                {{ deployment.deployer.initials }}
                              </ui-avatar-fallback>
                            </ui-avatar>
                            <span class="text-foreground text-xs">{{ deployment.deployer.name }}</span>
                          </div>
                        </td>
                        <td ui-table-cell>
                          <div class="text-muted-foreground flex items-center gap-1.5 text-xs">
                            @switch (deployment.trigger) {
                              @case ('push') {
                                <lucide-icon [img]="GitCommit" class="size-3.5" aria-hidden="true" />
                              }
                              @case ('manual') {
                                <lucide-icon [img]="Play" class="size-3.5" aria-hidden="true" />
                              }
                              @case ('rollback') {
                                <lucide-icon [img]="RotateCcw" class="size-3.5" aria-hidden="true" />
                              }
                            }
                            <span class="capitalize">{{ deployment.trigger }}</span>
                          </div>
                        </td>
                        <td ui-table-cell class="text-muted-foreground text-right text-xs whitespace-nowrap tabular-nums">
                          {{ deployment.deployedAt }}
                        </td>
                        <td ui-table-cell>
                          <ui-dropdown-menu>
                            <button
                              ui-button
                              ui-dropdown-menu-trigger
                              variant="ghost"
                              size="icon-sm"
                              class="text-muted-foreground"
                            >
                              <lucide-icon [img]="MoreHorizontal" class="size-4" aria-hidden="true" />
                              <span class="sr-only">Open actions for {{ deployment.id }}</span>
                            </button>
                            <ui-dropdown-menu-content align="end">
                              <ui-dropdown-menu-label>Actions</ui-dropdown-menu-label>
                              <ui-dropdown-menu-separator />
                              <ui-dropdown-menu-item>
                                <lucide-icon [img]="Undo2" class="mr-2 size-4" aria-hidden="true" />
                                Rollback to this deploy
                              </ui-dropdown-menu-item>
                              <ui-dropdown-menu-item (click)="copyUrl(deployment.url, deployment.id)">
                                @if (copiedUrlId() === deployment.id) {
                                  <lucide-icon [img]="Check" class="text-success mr-2 size-4" aria-hidden="true" />
                                } @else {
                                  <lucide-icon [img]="Copy" class="mr-2 size-4" aria-hidden="true" />
                                }
                                {{ copiedUrlId() === deployment.id ? 'Copied URL' : 'Copy deployment URL' }}
                              </ui-dropdown-menu-item>
                              <ui-dropdown-menu-item (click)="showLogs.set(true)">
                                <lucide-icon [img]="Terminal" class="mr-2 size-4" aria-hidden="true" />
                                Inspect build logs
                              </ui-dropdown-menu-item>
                              <a
                                ui-dropdown-menu-item
                                [href]="deployment.url"
                                target="_blank"
                                rel="noopener noreferrer"
                                class="flex items-center"
                              >
                                <lucide-icon [img]="ExternalLink" class="mr-2 size-4" aria-hidden="true" />
                                View live preview
                              </a>
                            </ui-dropdown-menu-content>
                          </ui-dropdown-menu>
                        </td>
                      </tr>
                    }
                    @if (filteredDeployments().length === 0) {
                      <tr ui-table-row>
                        <td ui-table-cell colspan="8" class="text-muted-foreground h-24 text-center text-sm">
                          No deployments match the selected environment filter.
                        </td>
                      </tr>
                    }
                  </tbody>
                </div>
              </div>
            </ui-card-content>
          </ui-card>
        </div>
      `,
    })
    export class UiDeploymentPipelineComponent implements OnInit {
      @Input() initialEnv: DeploymentEnvironment = 'all'
      @Input() initialLogsOpen = true
      @Input('class') className?: string
    
      protected readonly Check = Check
      protected readonly CheckCircle2 = CheckCircle2
      protected readonly ChevronDown = ChevronDown
      protected readonly ChevronUp = ChevronUp
      protected readonly Clock = Clock
      protected readonly Copy = Copy
      protected readonly ExternalLink = ExternalLink
      protected readonly GitBranch = GitBranch
      protected readonly GitCommit = GitCommit
      protected readonly Layers = Layers
      protected readonly Loader2 = Loader2
      protected readonly MoreHorizontal = MoreHorizontal
      protected readonly Play = Play
      protected readonly RotateCcw = RotateCcw
      protected readonly RotateCw = RotateCw
      protected readonly Terminal = Terminal
      protected readonly Undo2 = Undo2
      protected readonly XCircle = XCircle
    
      protected readonly branches = branches
      protected readonly pipelineSteps = pipelineSteps
      protected readonly terminalLogs = terminalLogs
      protected readonly deployments = deployments
    
      readonly selectedEnv = signal<DeploymentEnvironment>('all')
      readonly selectedBranch = signal('main')
      readonly showLogs = signal(true)
      readonly copiedLogs = signal(false)
      readonly copiedUrlId = signal<string | null>(null)
      readonly isDeploying = signal(false)
    
      readonly filteredDeployments = computed(() => {
        const env = this.selectedEnv()
        return env === 'all' ? deployments : deployments.filter((d) => d.environment === env)
      })
    
      private readonly timers = new Set<ReturnType<typeof setTimeout>>()
    
      constructor() {
        inject(DestroyRef).onDestroy(() => this.timers.forEach((t) => clearTimeout(t)))
      }
    
      ngOnInit(): void {
        this.selectedEnv.set(this.initialEnv)
        this.showLogs.set(this.initialLogsOpen)
      }
    
      get rootClass(): string {
        return cn('w-full space-y-6', this.className)
      }
    
      setEnv(value: string): void {
        this.selectedEnv.set(value as DeploymentEnvironment)
      }
    
      envBadgeVariant(deployment: DeploymentRecord): 'default' | 'secondary' | 'outline' {
        return deployment.environment === 'production'
          ? 'default'
          : deployment.environment === 'preview'
            ? 'secondary'
            : 'outline'
      }
    
      prefixClass(type: TerminalLogType): string {
        return cn(
          'font-medium select-none',
          type === 'success' && 'text-success',
          type === 'info' && 'text-info',
          type === 'cmd' && 'text-warning',
          type === 'stdout' && 'text-muted-foreground',
        )
      }
    
      textClass(type: TerminalLogType): string {
        return cn(
          type === 'success' && 'text-success font-medium',
          type === 'cmd' && 'font-semibold text-zinc-100',
          type === 'info' && 'text-zinc-300',
          type === 'stdout' && 'text-zinc-300',
        )
      }
    
      triggerDeployment(): void {
        this.isDeploying.set(true)
        this.later(() => this.isDeploying.set(false), 1200)
      }
    
      copyLogs(): void {
        const text = terminalLogs.map((l) => `[${l.time}] ${l.prefix} ${l.text}`).join('\n')
        if (typeof navigator !== 'undefined' && navigator.clipboard) {
          void navigator.clipboard.writeText(text)
          this.copiedLogs.set(true)
          this.later(() => this.copiedLogs.set(false), 2000)
        }
      }
    
      copyUrl(url: string, id: string): void {
        if (typeof navigator !== 'undefined' && navigator.clipboard) {
          void navigator.clipboard.writeText(url)
          this.copiedUrlId.set(id)
          this.later(() => this.copiedUrlId.set(null), 2000)
        }
      }
    
      private later(fn: () => void, ms: number): void {
        const t = setTimeout(() => {
          this.timers.delete(t)
          fn()
        }, ms)
        this.timers.add(t)
      }
    }
    
  • src/app/components/blocks/deployment-pipeline/index.ts0.3 kB

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