Menu

Change language

GitHub repo

Database Migration Schema Diff

blockdevops

Prisma, Flyway, and Liquibase database migration runner with pending DDL visual diffs, rollback scripts, schema integrity telemetry, and execution history.

Installation

$npx uipkge-ng@latest add database-migration-schema-diff
Manifest:https://uipkge.dev/r/angular/database-migration-schema-diff.jsonInstalls to:src/app/components/blocks/database-migration-schema-diff/

Variants

Loading interactive previews…

Schema

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

DiffLine
interface DiffLine {
  num: number
  type: 'addition' | 'modification' | 'deletion' | 'neutral'
  prefix: string
  text: string
}
HistoricalMigration
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
}

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 &amp; 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&#64;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 &amp; 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 &amp; 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">&lt; 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 &amp; 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

Raw manifest:https://uipkge.dev/r/angular/database-migration-schema-diff.json