Deployment Pipeline
blockdevopsCI/CD deployment history, active environment status, execution stepper, collapsible build log terminal with stdout timestamps, and filterable deployments table.
Installation
$pnpm dlx uipkge-ng@latest add deployment-pipeline$npx uipkge-ng@latest add deployment-pipeline$yarn dlx uipkge-ng@latest add deployment-pipeline$bunx uipkge-ng@latest add deployment-pipelineManifest:
https://uipkge.dev/r/angular/deployment-pipeline.jsonInstalls to:src/app/components/blocks/deployment-pipeline/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
DeploymentRecordinterface 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
}PipelineStepinterface PipelineStep {
id: string
name: string
duration: string
description: string
status: 'completed' | 'in-progress' | 'pending'
}TerminalLogLineinterface TerminalLogLine {
time: string
type: TerminalLogType
prefix: string
text: string
}npm dependencies
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 & 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
export { UiDeploymentPipelineComponent, type DeploymentEnvironment, type DeploymentStatus, type DeploymentTrigger, type DeploymentRecord, type PipelineStep, type TerminalLogType, type TerminalLogLine, } from './deployment-pipeline.component'
Raw manifest:https://uipkge.dev/r/angular/deployment-pipeline.json