Database Migration Schema Diff
blockdevopsPrisma, Flyway, and Liquibase database migration runner with pending DDL visual diffs, rollback scripts, schema integrity telemetry, and execution history.
Installation
$pnpm dlx uipkge-ng@latest add database-migration-schema-diff$npx uipkge-ng@latest add database-migration-schema-diff$yarn dlx uipkge-ng@latest add database-migration-schema-diff$bunx uipkge-ng@latest add database-migration-schema-diffManifest:
https://uipkge.dev/r/angular/database-migration-schema-diff.jsonInstalls to:src/app/components/blocks/database-migration-schema-diff/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.
DiffLineinterface DiffLine {
num: number
type: 'addition' | 'modification' | 'deletion' | 'neutral'
prefix: string
text: string
}HistoricalMigrationinterface HistoricalMigration {
id: string
filename: string
version: string
description: string
appliedAt: string
author: string
executionTime: string
tablesAffected: string
status: 'applied' | 'pending' | 'rolled_back'
checksum: string
}Files installed (2)
src/app/components/blocks/database-migration-schema-diff/database-migration-schema-diff.component.ts34.1 kB
import { ChangeDetectionStrategy, Component, DestroyRef, Input, inject, signal } from '@angular/core' import { Check, CheckCircle2, Clock, Code2, Copy, Database, FileCode2, HardDrive, Layers, Loader2, LucideAngularModule, Play, RotateCcw, ShieldCheck, Terminal, Timer, Undo2, Zap, } from 'lucide-angular' import { cn } from '@/lib/utils' 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 { UiSeparatorComponent } from '@/ui/separator/separator.component' import { UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, } from '@/ui/table/table.component' import { UiTabsComponent, UiTabsContentComponent, UiTabsListComponent, UiTabsTriggerComponent, } from '@/ui/tabs/tabs.component' export interface DiffLine { num: number type: 'addition' | 'modification' | 'deletion' | 'neutral' prefix: string text: string } export interface HistoricalMigration { id: string filename: string version: string description: string appliedAt: string author: string executionTime: string tablesAffected: string status: 'applied' | 'pending' | 'rolled_back' checksum: string } export type DatabaseMigrationSchemaDiffTab = 'up' | 'rollback' | 'impact' | 'cli' const upSqlRaw = `-- Up Migration -- Migration: 20260821142850_add_org_billing_seats_and_quotas.sql -- Target: public.organizations ALTER TABLE public.organizations ADD COLUMN billing_seats_allocated INTEGER NOT NULL DEFAULT 5, ADD COLUMN max_seat_limit INTEGER NOT NULL DEFAULT 20, ADD COLUMN last_seat_audit_at TIMESTAMPTZ DEFAULT NOW(); CREATE INDEX idx_orgs_seat_audit ON public.organizations(last_seat_audit_at);` const downSqlRaw = `-- Down Migration (Rollback) -- Reverts: 20260821142850_add_org_billing_seats_and_quotas.sql -- Target: public.organizations DROP INDEX IF EXISTS idx_orgs_seat_audit; ALTER TABLE public.organizations DROP COLUMN IF EXISTS billing_seats_allocated, DROP COLUMN IF EXISTS max_seat_limit, DROP COLUMN IF EXISTS last_seat_audit_at;` const cliCommand = 'npx prisma migrate deploy --schema=./prisma/schema.prisma' const upDiffLines: DiffLine[] = [ { num: 1, type: 'neutral', prefix: ' ', text: '-- Up Migration' }, { num: 2, type: 'neutral', prefix: ' ', text: '-- Migration: 20260821142850_add_org_billing_seats_and_quotas.sql' }, { num: 3, type: 'neutral', prefix: ' ', text: '-- Target: public.organizations' }, { num: 4, type: 'neutral', prefix: ' ', text: '' }, { num: 5, type: 'modification', prefix: '~', text: 'ALTER TABLE public.organizations ' }, { num: 6, type: 'addition', prefix: '+', text: ' ADD COLUMN billing_seats_allocated INTEGER NOT NULL DEFAULT 5,' }, { num: 7, type: 'addition', prefix: '+', text: ' ADD COLUMN max_seat_limit INTEGER NOT NULL DEFAULT 20,' }, { num: 8, type: 'addition', prefix: '+', text: ' ADD COLUMN last_seat_audit_at TIMESTAMPTZ DEFAULT NOW();' }, { num: 9, type: 'neutral', prefix: ' ', text: '' }, { num: 10, type: 'addition', prefix: '+', text: 'CREATE INDEX idx_orgs_seat_audit ON public.organizations(last_seat_audit_at);', }, ] const downDiffLines: DiffLine[] = [ { num: 1, type: 'neutral', prefix: ' ', text: '-- Down Migration (Rollback)' }, { num: 2, type: 'neutral', prefix: ' ', text: '-- Reverts: 20260821142850_add_org_billing_seats_and_quotas.sql' }, { num: 3, type: 'neutral', prefix: ' ', text: '-- Target: public.organizations' }, { num: 4, type: 'neutral', prefix: ' ', text: '' }, { num: 5, type: 'deletion', prefix: '-', text: 'DROP INDEX IF EXISTS idx_orgs_seat_audit;' }, { num: 6, type: 'neutral', prefix: ' ', text: '' }, { num: 7, type: 'modification', prefix: '~', text: 'ALTER TABLE public.organizations ' }, { num: 8, type: 'deletion', prefix: '-', text: ' DROP COLUMN IF EXISTS billing_seats_allocated,' }, { num: 9, type: 'deletion', prefix: '-', text: ' DROP COLUMN IF EXISTS max_seat_limit,' }, { num: 10, type: 'deletion', prefix: '-', text: ' DROP COLUMN IF EXISTS last_seat_audit_at;' }, ] const historicalMigrations: HistoricalMigration[] = [ { id: 'mig_48', filename: '20260814091522_add_audit_logs_partitioning.sql', version: 'v2026.08.14.02', description: 'Partition audit_logs table by month range and attach default tablespace', appliedAt: 'Aug 14, 2026 · 09:15 UTC', author: 'alex.chen@corp', executionTime: '1.8s', tablesAffected: 'audit_logs, audit_logs_2026_08', status: 'applied', checksum: 'sha256:4e8b39c0', }, { id: 'mig_47', filename: '20260810143011_create_api_rate_limits_table.sql', version: 'v2026.08.10.01', description: 'Create rate_limits table & redis fallback synchronization schema', appliedAt: 'Aug 10, 2026 · 14:30 UTC', author: 'sarah.m@corp', executionTime: '0.9s', tablesAffected: 'api_rate_limits', status: 'applied', checksum: 'sha256:d12c88f1', }, { id: 'mig_46', filename: '20260802110540_add_saml_sso_workspaces.sql', version: 'v2026.08.02.03', description: 'Add SAML SSO domain verification and enterprise IdP metadata records', appliedAt: 'Aug 02, 2026 · 11:05 UTC', author: 'devops-bot', executionTime: '2.1s', tablesAffected: 'workspaces, idp_configs', status: 'applied', checksum: 'sha256:88a3b114', }, { id: 'mig_45', filename: '20260725184219_optimize_query_cache_indexes.sql', version: 'v2026.07.25.01', description: 'Add CONCURRENT B-Tree composite index on team_id + created_at timestamp', appliedAt: 'Jul 25, 2026 · 18:42 UTC', author: 'alex.chen@corp', executionTime: '4.2s', tablesAffected: 'query_logs, cache_entries', status: 'applied', checksum: 'sha256:fa2300b9', }, { id: 'mig_44', filename: '20260718082005_init_multi_tenant_memberships.sql', version: 'v2026.07.18.01', description: 'Initialize organization_memberships, team_roles, and permission cascades', appliedAt: 'Jul 18, 2026 · 08:20 UTC', author: 'elena.r@corp', executionTime: '1.2s', tablesAffected: 'org_members, permissions', status: 'applied', checksum: 'sha256:56df9201', }, ] /** * DatabaseMigrationSchemaDiff -- the React block, 1:1: target cluster header with the pending / * in-sync badge, "Preview Rollback Script" and "Apply Migration" actions, four telemetry cards, * the migration inspector (Up diff, Down rollback, schema impact and Prisma CLI tabs, with copy * buttons) and the recent applied migrations table. Edit the data consts above. */ @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-database-migration-schema-diff, [ui-database-migration-schema-diff]', standalone: true, // React renders the root <div> itself: keep this wrapper out of the layout. host: { class: 'contents' }, imports: [ LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardHeaderComponent, UiCardTitleComponent, UiCardDescriptionComponent, UiCardContentComponent, UiSeparatorComponent, UiTableComponent, UiTableHeaderComponent, UiTableBodyComponent, UiTableRowComponent, UiTableHeadComponent, UiTableCellComponent, UiTabsComponent, UiTabsListComponent, UiTabsTriggerComponent, UiTabsContentComponent, ], template: ` <div data-slot="database-migration-schema-diff" [class]="rootClass"> <!-- Header: Target Database & Version Runner Status --> <ui-card class="border-border bg-card shadow-xs"> <ui-card-header class="pb-4"> <div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between"> <div class="space-y-1.5"> <div class="flex flex-wrap items-center gap-2"> <div class="text-foreground flex items-center gap-2 text-base font-semibold sm:text-lg"> <lucide-icon [img]="Database" class="text-primary size-5 shrink-0" /> <span>PostgreSQL 16.2 · production_primary_cluster</span> </div> <div class="bg-success/10 text-success flex items-center gap-1.5 rounded-full px-2 py-0.5 text-xs font-medium" > <span class="bg-success size-1.5 animate-pulse rounded-full"></span> <span>Connected</span> </div> </div> <div class="text-muted-foreground flex flex-wrap items-center gap-2 text-xs sm:text-sm"> <span class="text-foreground font-mono font-medium">v2026.08.21.01_add_org_billing_seats</span> <ui-separator orientation="vertical" class="h-3.5" /> <span>db.us-east-1.aws.neon.tech:5432</span> <ui-separator orientation="vertical" class="h-3.5" /> <span>public schema</span> </div> </div> <div class="flex flex-wrap items-center gap-2.5"> @if (!isApplied()) { <span ui-badge wrap variant="warning" class="gap-1.5 px-2.5 py-1 text-xs"> <lucide-icon [img]="Clock" class="size-3.5" /> <span>1 Pending Migration Ready to Apply</span> </span> } @else { <span ui-badge wrap variant="success" class="gap-1.5 px-2.5 py-1 text-xs"> <lucide-icon [img]="CheckCircle2" class="size-3.5" /> <span>All Migrations Applied · In Sync</span> </span> } <button ui-button variant="outline" size="sm" class="h-9 gap-1.5 text-xs font-medium shadow-xs active:scale-[0.98]" (click)="handlePreviewRollback()" > <lucide-icon [img]="Undo2" class="text-muted-foreground size-3.5" /> <span>Preview Rollback Script</span> </button> @if (!isApplied()) { <button ui-button variant="default" size="sm" class="h-9 gap-1.5 text-xs font-medium shadow-xs active:scale-[0.98]" [disabled]="isApplying()" (click)="handleApplyMigration()" > @if (isApplying()) { <lucide-icon [img]="Loader2" class="size-3.5 animate-spin" /> } @else { <lucide-icon [img]="Play" class="size-3.5 fill-current" /> } <span>{{ isApplying() ? 'Applying Migration...' : 'Apply Migration (prisma migrate deploy)' }}</span> </button> } @else { <button ui-button variant="secondary" size="sm" class="h-9 gap-1.5 text-xs font-medium active:scale-[0.98]" (click)="handleResetState()" > <lucide-icon [img]="RotateCcw" class="size-3.5" /> <span>Reset State</span> </button> } </div> </div> </ui-card-header> </ui-card> <!-- 4 Migration Telemetry Cards --> <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"> <!-- Total Applied Migrations --> <ui-card class="border-border bg-card shadow-xs"> <ui-card-content class="p-4 sm:p-5"> <div class="flex items-center justify-between"> <span class="text-muted-foreground text-xs font-medium">Total Applied Migrations</span> <div class="bg-muted text-muted-foreground flex size-8 items-center justify-center rounded-md"> <lucide-icon [img]="Layers" class="size-4" /> </div> </div> <div class="mt-3"> <div class="text-foreground text-2xl font-bold tracking-tight">{{ isApplied() ? '49' : '48' }}</div> <p class="text-muted-foreground mt-1 text-xs">48 Migrations Applied · Baseline to v2026.08.14</p> </div> <div class="text-success mt-3 flex items-center gap-1.5 text-xs font-medium"> <lucide-icon [img]="Check" class="size-3.5" /> <span>100% successful execution history</span> </div> </ui-card-content> </ui-card> <!-- Pending Migrations --> <ui-card [class]="pendingCardClass()"> <ui-card-content class="p-4 sm:p-5"> <div class="flex items-center justify-between"> <span class="text-muted-foreground text-xs font-medium">Pending Migrations</span> <div [class]="pendingIconClass()"> @if (!isApplied()) { <lucide-icon [img]="Clock" class="size-4" /> } @else { <lucide-icon [img]="CheckCircle2" class="size-4" /> } </div> </div> <div class="mt-3"> <div class="text-foreground text-2xl font-bold tracking-tight">{{ isApplied() ? '0' : '1' }}</div> <p class="text-muted-foreground mt-1 text-xs"> {{ isApplied() ? '0 Pending · Schema up to date' : '1 Pending Deployment' }} </p> </div> <div [class]="pendingFooterClass()"> @if (!isApplied()) { <lucide-icon [img]="ShieldCheck" class="size-3.5" /> } @else { <lucide-icon [img]="Check" class="size-3.5" /> } <span>{{ !isApplied() ? 'Safe DDL · Non-blocking ALTER' : 'Schema verified against shadow DB' }}</span> </div> </ui-card-content> </ui-card> <!-- Last Migration Execution --> <ui-card class="border-border bg-card shadow-xs"> <ui-card-content class="p-4 sm:p-5"> <div class="flex items-center justify-between"> <span class="text-muted-foreground text-xs font-medium">Last Migration Execution</span> <div class="bg-muted text-muted-foreground flex size-8 items-center justify-center rounded-md"> <lucide-icon [img]="Zap" class="size-4" /> </div> </div> <div class="mt-3"> <div class="text-foreground text-2xl font-bold tracking-tight">{{ isApplied() ? '0.12s' : '3.4s' }}</div> <p class="text-muted-foreground mt-1 text-xs">3.4s execution time · 0 lock timeouts</p> </div> <div class="text-muted-foreground mt-3 flex items-center gap-1.5 text-xs"> <lucide-icon [img]="Timer" class="size-3.5" /> <span>Zero query contention or lock waits</span> </div> </ui-card-content> </ui-card> <!-- Target Schema Integrity --> <ui-card class="border-border bg-card shadow-xs"> <ui-card-content class="p-4 sm:p-5"> <div class="flex items-center justify-between"> <span class="text-muted-foreground text-xs font-medium">Target Schema Integrity</span> <div class="bg-success/10 text-success flex size-8 items-center justify-center rounded-md"> <lucide-icon [img]="ShieldCheck" class="size-4" /> </div> </div> <div class="mt-3"> <div class="text-foreground text-2xl font-bold tracking-tight">100%</div> <p class="text-muted-foreground mt-1 text-xs">100% Validated · Zero Drift</p> </div> <div class="text-success mt-3 flex items-center gap-1.5 text-xs font-medium"> <lucide-icon [img]="CheckCircle2" class="size-3.5" /> <span>Prisma schema & DB in sync</span> </div> </ui-card-content> </ui-card> </div> <!-- Active Migration Inspector & DDL Diff Card --> <ui-card class="border-border bg-card shadow-xs"> <ui-card-header class="border-border border-b pb-4"> <div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"> <div class="space-y-1"> <div class="flex flex-wrap items-center gap-2"> <lucide-icon [img]="FileCode2" class="text-primary size-4 shrink-0" /> <h3 ui-card-title class="text-foreground font-mono text-sm font-semibold sm:text-base"> 20260821142850_add_org_billing_seats_and_quotas.sql </h3> @if (!isApplied()) { <span ui-badge wrap variant="warning" class="text-xs">Pending DDL Execution</span> } @else { <span ui-badge wrap variant="success" class="text-xs">Applied in Production</span> } </div> <p ui-card-description class="text-muted-foreground text-xs"> Target Table: <span class="text-foreground font-mono font-medium">public.organizations</span> · Author: <span class="text-foreground">alex.chen@corp</span> · Estimated duration: <span class="text-foreground font-mono font-medium">~120ms</span> </p> </div> <div class="flex items-center gap-2"> <button ui-button variant="outline" size="sm" class="h-8 gap-1.5 text-xs font-medium" (click)="copySql()"> @if (sqlCopied()) { <lucide-icon [img]="Check" class="text-success size-3.5" /> } @else { <lucide-icon [img]="Copy" class="size-3.5" /> } <span>{{ sqlCopied() ? 'Copied SQL' : 'Copy SQL' }}</span> </button> </div> </div> </ui-card-header> <ui-card-content class="p-4 sm:p-6"> <ui-tabs [value]="activeTab()" (valueChange)="setActiveTab($event)" class="w-full space-y-4"> <div ui-tabs-list class="grid w-full grid-cols-2 sm:inline-flex sm:w-auto sm:grid-cols-none"> <button ui-tabs-trigger value="up" class="gap-1.5 text-xs"> <lucide-icon [img]="Code2" class="size-3.5" /> <span>DDL Diff (Up)</span> </button> <button ui-tabs-trigger value="rollback" class="gap-1.5 text-xs"> <lucide-icon [img]="Undo2" class="size-3.5" /> <span>Rollback Script (Down)</span> </button> <button ui-tabs-trigger value="impact" class="gap-1.5 text-xs"> <lucide-icon [img]="HardDrive" class="size-3.5" /> <span>Schema Impact</span> </button> <button ui-tabs-trigger value="cli" class="gap-1.5 text-xs"> <lucide-icon [img]="Terminal" class="size-3.5" /> <span>Prisma / CLI Runner</span> </button> </div> <!-- Tab 1: DDL Diff (Up) --> <ui-tabs-content value="up" class="space-y-3 focus-visible:outline-none"> <div class="border-border bg-muted/50 flex items-center justify-between rounded-t-md border-x border-t px-3 py-2 text-xs" > <div class="text-muted-foreground flex items-center gap-2"> <span class="text-foreground font-medium">Forward Migration (Up)</span> <span>·</span> <span class="text-success font-medium">+3 columns added</span> <span>·</span> <span class="text-info font-medium">+1 index created</span> </div> <span class="text-muted-foreground font-mono">SQL (PostgreSQL DDL)</span> </div> <div class="border-border bg-muted/20 overflow-x-auto rounded-b-md border font-mono text-xs leading-relaxed" > <div class="max-w-[540px] min-w-full py-2"> @for (line of upDiffLines; track line.num) { <div [class]="lineClass(line)"> <span class="text-muted-foreground/50 w-7 shrink-0 pr-3 text-right select-none">{{ line.num }}</span> <span [class]="prefixClass(line)">{{ line.prefix }}</span> <span class="pl-1 whitespace-pre">{{ line.text }}</span> </div> } </div> </div> </ui-tabs-content> <!-- Tab 2: Rollback Script (Down) --> <ui-tabs-content value="rollback" class="space-y-3 focus-visible:outline-none"> <div class="border-border bg-muted/50 flex items-center justify-between rounded-t-md border-x border-t px-3 py-2 text-xs" > <div class="text-muted-foreground flex items-center gap-2"> <span class="text-foreground font-medium">Revert Script (Down)</span> <span>·</span> <span class="text-destructive font-medium">-3 columns dropped</span> <span>·</span> <span class="text-destructive font-medium">-1 index dropped</span> </div> <span class="text-muted-foreground font-mono">SQL (PostgreSQL DDL)</span> </div> <div class="border-border bg-muted/20 overflow-x-auto rounded-b-md border font-mono text-xs leading-relaxed" > <div class="max-w-[540px] min-w-full py-2"> @for (line of downDiffLines; track line.num) { <div [class]="lineClass(line)"> <span class="text-muted-foreground/50 w-7 shrink-0 pr-3 text-right select-none">{{ line.num }}</span> <span [class]="prefixClass(line)">{{ line.prefix }}</span> <span class="pl-1 whitespace-pre">{{ line.text }}</span> </div> } </div> </div> </ui-tabs-content> <!-- Tab 3: Schema Impact & Locks --> <ui-tabs-content value="impact" class="space-y-4 focus-visible:outline-none"> <div class="grid grid-cols-1 gap-4 md:grid-cols-2"> <div class="border-border bg-muted/30 space-y-3 rounded-lg border p-4"> <div class="text-foreground flex items-center gap-2 text-sm font-medium"> <lucide-icon [img]="Database" class="text-primary size-4 shrink-0" /> <span>Target Table & Column Additions</span> </div> <div class="space-y-2 text-xs"> <div class="border-border/60 bg-card flex items-center justify-between rounded-md border p-2"> <span class="text-foreground font-mono font-medium">billing_seats_allocated</span> <span ui-badge wrap variant="secondary" class="font-mono text-xs" >INTEGER NOT NULL DEFAULT 5</span > </div> <div class="border-border/60 bg-card flex items-center justify-between rounded-md border p-2"> <span class="text-foreground font-mono font-medium">max_seat_limit</span> <span ui-badge wrap variant="secondary" class="font-mono text-xs" >INTEGER NOT NULL DEFAULT 20</span > </div> <div class="border-border/60 bg-card flex items-center justify-between rounded-md border p-2"> <span class="text-foreground font-mono font-medium">last_seat_audit_at</span> <span ui-badge wrap variant="secondary" class="font-mono text-xs">TIMESTAMPTZ DEFAULT NOW()</span> </div> </div> </div> <div class="border-border bg-muted/30 space-y-3 rounded-lg border p-4"> <div class="text-foreground flex items-center gap-2 text-sm font-medium"> <lucide-icon [img]="ShieldCheck" class="text-success size-4 shrink-0" /> <span>Lock Safety & Concurrency Evaluation</span> </div> <div class="text-muted-foreground space-y-2 text-xs"> <div class="border-border/60 bg-card flex items-center justify-between rounded-md border p-2"> <span>Lock Level Required</span> <span class="text-foreground font-mono font-medium">ACCESS EXCLUSIVE (Catalog only)</span> </div> <div class="border-border/60 bg-card flex items-center justify-between rounded-md border p-2"> <span>Table Rewrite Required</span> <span ui-badge wrap variant="success" class="text-xs">No (PG11+ Constant Default)</span> </div> <div class="border-border/60 bg-card flex items-center justify-between rounded-md border p-2"> <span>Estimated Lock Duration</span> <span class="text-success font-mono font-medium">< 2ms</span> </div> </div> </div> </div> </ui-tabs-content> <!-- Tab 4: CLI Runner --> <ui-tabs-content value="cli" class="space-y-3 focus-visible:outline-none"> <div class="border-border bg-card space-y-3 rounded-lg border p-4"> <div class="flex items-center justify-between"> <div class="text-foreground flex items-center gap-2 text-xs font-medium"> <lucide-icon [img]="Terminal" class="text-primary size-4 shrink-0" /> <span>Execute via Prisma CLI</span> </div> <button ui-button variant="outline" size="xs" class="h-7 gap-1 text-xs" (click)="copyCli()"> @if (copiedCli()) { <lucide-icon [img]="Check" class="text-success size-3" /> } @else { <lucide-icon [img]="Copy" class="size-3" /> } <span>{{ copiedCli() ? 'Copied' : 'Copy' }}</span> </button> </div> <div class="bg-muted/60 text-foreground rounded-md p-3 font-mono text-xs select-all"> {{ cliCommand }} </div> <p class="text-muted-foreground text-xs"> Applies all pending migrations to the configured datasource cluster without prompting for interactive confirmation. </p> </div> </ui-tabs-content> </ui-tabs> </ui-card-content> </ui-card> <!-- Historical Migrations Table Card --> <ui-card class="border-border bg-card shadow-xs"> <ui-card-header class="pb-3"> <div class="flex items-center justify-between"> <div> <h3 ui-card-title class="text-foreground text-base font-semibold">Recent Applied Migrations</h3> <p ui-card-description class="text-muted-foreground text-xs"> Historical DDL execution logs, author signatures, and validation checksums </p> </div> <span ui-badge wrap variant="outline" class="font-mono text-xs">5 Most Recent</span> </div> </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 class="hover:bg-transparent"> <th ui-table-head class="text-xs font-medium">Migration & Version</th> <th ui-table-head class="text-xs font-medium">Target Scope</th> <th ui-table-head class="text-xs font-medium">Executed At</th> <th ui-table-head class="text-xs font-medium">Author</th> <th ui-table-head class="text-xs font-medium">Duration</th> <th ui-table-head class="text-right text-xs font-medium">Status</th> </tr> </thead> <tbody ui-table-body> @for (migration of historicalMigrations; track migration.id) { <tr ui-table-row class="hover:bg-muted/50 text-xs transition-colors"> <td ui-table-cell class="py-3"> <div class="space-y-0.5"> <div class="text-foreground font-mono font-medium">{{ migration.filename }}</div> <div class="text-muted-foreground flex items-center gap-1.5"> <span class="text-primary font-mono font-medium">{{ migration.version }}</span> <span>·</span> <span>{{ migration.description }}</span> </div> </div> </td> <td ui-table-cell class="text-muted-foreground font-mono whitespace-nowrap"> {{ migration.tablesAffected }} </td> <td ui-table-cell class="text-muted-foreground whitespace-nowrap">{{ migration.appliedAt }}</td> <td ui-table-cell class="text-muted-foreground whitespace-nowrap">{{ migration.author }}</td> <td ui-table-cell class="text-foreground font-mono font-medium whitespace-nowrap"> {{ migration.executionTime }} </td> <td ui-table-cell class="text-right whitespace-nowrap"> <span ui-badge wrap variant="success" class="gap-1 px-2 py-0.5 text-xs font-medium"> <lucide-icon [img]="CheckCircle2" class="size-3" /> <span>Applied</span> </span> </td> </tr> } </tbody> </div> </div> </ui-card-content> </ui-card> </div> `, }) export class UiDatabaseMigrationSchemaDiffComponent { @Input('class') className?: string protected readonly upDiffLines = upDiffLines protected readonly downDiffLines = downDiffLines protected readonly historicalMigrations = historicalMigrations protected readonly cliCommand = cliCommand readonly activeTab = signal<DatabaseMigrationSchemaDiffTab>('up') readonly isApplying = signal(false) readonly isApplied = signal(false) readonly copiedUpSql = signal(false) readonly copiedDownSql = signal(false) readonly copiedCli = signal(false) protected readonly Check = Check protected readonly CheckCircle2 = CheckCircle2 protected readonly Clock = Clock protected readonly Code2 = Code2 protected readonly Copy = Copy protected readonly Database = Database protected readonly FileCode2 = FileCode2 protected readonly HardDrive = HardDrive protected readonly Layers = Layers protected readonly Loader2 = Loader2 protected readonly Play = Play protected readonly RotateCcw = RotateCcw protected readonly ShieldCheck = ShieldCheck protected readonly Terminal = Terminal protected readonly Timer = Timer protected readonly Undo2 = Undo2 protected readonly Zap = Zap private readonly timers = new Set<ReturnType<typeof setTimeout>>() constructor() { inject(DestroyRef).onDestroy(() => this.timers.forEach((t) => clearTimeout(t))) } protected get rootClass(): string { return cn('w-full space-y-6', this.className) } protected pendingCardClass(): string { return cn('border-border bg-card shadow-xs transition-colors', !this.isApplied() && 'border-warning/40') } protected pendingIconClass(): string { return cn( 'flex size-8 items-center justify-center rounded-md', !this.isApplied() ? 'bg-warning/10 text-warning' : 'bg-success/10 text-success', ) } protected pendingFooterClass(): string { return cn('mt-3 flex items-center gap-1.5 text-xs font-medium', !this.isApplied() ? 'text-warning' : 'text-success') } protected sqlCopied(): boolean { return this.activeTab() === 'rollback' ? this.copiedDownSql() : this.copiedUpSql() } protected lineClass(line: DiffLine): string { return cn( 'flex items-center px-3 py-0.5 transition-colors', line.type === 'addition' && 'border-success bg-success/10 text-success border-l-2 font-medium', line.type === 'deletion' && 'border-destructive bg-destructive/10 text-destructive border-l-2 font-medium', line.type === 'modification' && 'border-info bg-info/10 text-info border-l-2 font-medium', line.type === 'neutral' && 'text-muted-foreground border-l-2 border-transparent', ) } protected prefixClass(line: DiffLine): string { return cn( 'w-4 shrink-0 text-center font-bold select-none', line.type === 'addition' && 'text-success', line.type === 'deletion' && 'text-destructive', line.type === 'modification' && 'text-info', line.type === 'neutral' && 'text-transparent', ) } setActiveTab(value: string): void { if (value === 'up' || value === 'rollback' || value === 'impact' || value === 'cli') this.activeTab.set(value) } handleApplyMigration(): void { if (this.isApplying() || this.isApplied()) return this.isApplying.set(true) this.later(900, () => { this.isApplying.set(false) this.isApplied.set(true) }) } handlePreviewRollback(): void { this.activeTab.set('rollback') } handleResetState(): void { this.isApplied.set(false) this.isApplying.set(false) } copySql(): void { const rollback = this.activeTab() === 'rollback' this.copyToClipboard(rollback ? downSqlRaw : upSqlRaw, rollback ? 'down' : 'up') } copyCli(): void { this.copyToClipboard(cliCommand, 'cli') } copyToClipboard(text: string, type: 'up' | 'down' | 'cli'): void { if (typeof navigator !== 'undefined' && navigator.clipboard) navigator.clipboard.writeText(text).catch(() => {}) const flag = type === 'up' ? this.copiedUpSql : type === 'down' ? this.copiedDownSql : this.copiedCli flag.set(true) this.later(2000, () => flag.set(false)) } private later(ms: number, fn: () => void): void { const t = setTimeout(() => { this.timers.delete(t) fn() }, ms) this.timers.add(t) } }src/app/components/blocks/database-migration-schema-diff/index.ts0.2 kB
export { UiDatabaseMigrationSchemaDiffComponent, type DiffLine, type HistoricalMigration, type DatabaseMigrationSchemaDiffTab, } from './database-migration-schema-diff.component'
Raw manifest:https://uipkge.dev/r/angular/database-migration-schema-diff.json