Menu

Change language

GitHub repo

Data Retention Policy Manager

blockdevops

Snowflake and BigQuery style automated data retention policy manager, partition TTL scheduler, and cold storage archive tiering with cost impact metrics, lifecycle action execution, and compliance tracking.

Installation

$npx uipkge-ng@latest add data-retention-policy-manager
Manifest:https://uipkge.dev/r/angular/data-retention-policy-manager.jsonInstalls to:src/app/components/blocks/data-retention-policy-manager/

Variants

Loading interactive previews…

Schema

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

RetentionPolicy
interface RetentionPolicy {
  id: string
  tableName: string
  database: string
  engine: EngineType
  partitionColumn: string
  retentionPeriod: string
  lifecycleAction: string
  archiveTier: ArchiveTierType
  complianceReason: string
  totalSize: string
  coldSize: string
  estimatedMonthlySavings: string
  enabled: boolean
  lastRun: string
  nextRun: string
  rowCount?: string
}
StorageRetentionStats
interface StorageRetentionStats {
  totalManagedData: string
  totalManagedSubtitle?: string
  coldArchivedData: string
  archivedPercentage: number
  archivedSubtitle?: string
  projectedMonthlySavings: string
  savingsSubtitle?: string
  purged30d: string
  purgedSubtitle?: string
}
BannerMessage
interface BannerMessage {
  type: 'success' | 'info'
  text: string
}

Files installed (2)

  • src/app/components/blocks/data-retention-policy-manager/data-retention-policy-manager.component.ts43.8 kB
    // Automated data retention policy manager: storage/cost impact cards, a filterable policy table
    // with per-row enable switches and an actions menu, an add/edit policy dialog, and a simulated
    // 1.2s "Run Archive Now" partition sweep dialog. Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, DestroyRef, Input, computed, inject, signal } from '@angular/core'
    import {
      Archive,
      Check,
      CheckCircle2,
      Clock,
      Copy,
      Database,
      DollarSign,
      FileText,
      HardDriveDownload,
      Info,
      LucideAngularModule,
      MoreHorizontal,
      Pencil,
      Play,
      Plus,
      RefreshCw,
      Search,
      ShieldCheck,
      Trash2,
      X,
    } from 'lucide-angular'
    import { cn } from '@/lib/utils'
    import { UiBadgeComponent, type BadgeVariant } from '@/ui/badge/badge.component'
    import { UiButtonComponent } from '@/ui/button/button.component'
    import {
      UiCardComponent,
      UiCardContentComponent,
      UiCardDescriptionComponent,
      UiCardHeaderComponent,
      UiCardTitleComponent,
    } from '@/ui/card/card.component'
    import {
      UiDialogComponent,
      UiDialogContentComponent,
      UiDialogDescriptionComponent,
      UiDialogFooterComponent,
      UiDialogHeaderComponent,
      UiDialogTitleComponent,
    } from '@/ui/dialog/dialog.component'
    import {
      UiDropdownMenuComponent,
      UiDropdownMenuContentComponent,
      UiDropdownMenuItemComponent,
      UiDropdownMenuLabelComponent,
      UiDropdownMenuSeparatorComponent,
      UiDropdownMenuTriggerComponent,
    } from '@/ui/dropdown-menu/dropdown-menu.component'
    import { UiInputComponent } from '@/ui/input/input.component'
    import { UiProgressComponent } from '@/ui/progress/progress.component'
    import {
      UiSelectComponent,
      UiSelectContentComponent,
      UiSelectItemComponent,
      UiSelectTriggerComponent,
      UiSelectValueComponent,
    } from '@/ui/select/select.component'
    import { UiSwitchComponent } from '@/ui/switch/switch.component'
    import {
      UiTableBodyComponent,
      UiTableCellComponent,
      UiTableComponent,
      UiTableHeadComponent,
      UiTableHeaderComponent,
      UiTableRowComponent,
    } from '@/ui/table/table.component'
    
    export type EngineType = 'Snowflake' | 'BigQuery' | 'Databricks' | 'Postgres'
    export type ArchiveTierType = 'Glacier Deep' | 'Coldline' | 'Standard Cold' | 'Purge Immediately'
    
    export interface RetentionPolicy {
      id: string
      tableName: string
      database: string
      engine: EngineType
      partitionColumn: string
      retentionPeriod: string
      lifecycleAction: string
      archiveTier: ArchiveTierType
      complianceReason: string
      totalSize: string
      coldSize: string
      estimatedMonthlySavings: string
      enabled: boolean
      lastRun: string
      nextRun: string
      rowCount?: string
    }
    
    export interface StorageRetentionStats {
      totalManagedData: string
      totalManagedSubtitle?: string
      coldArchivedData: string
      archivedPercentage: number
      archivedSubtitle?: string
      projectedMonthlySavings: string
      savingsSubtitle?: string
      purged30d: string
      purgedSubtitle?: string
    }
    
    export const defaultPolicies: RetentionPolicy[] = [
      {
        id: 'pol-1',
        tableName: 'audit_logs.events',
        database: 'SNOWFLAKE_PROD_DWH',
        engine: 'Snowflake',
        partitionColumn: 'event_timestamp (DAY)',
        retentionPeriod: '90 Days Active',
        lifecycleAction: 'Transition to Cold Glacier at 90d -> Hard Purge at 365d',
        archiveTier: 'Glacier Deep',
        complianceReason: 'SOX Compliance',
        totalSize: '4.2 TB',
        coldSize: '3.1 TB',
        estimatedMonthlySavings: '$420.00 / mo',
        enabled: true,
        lastRun: '1 hour ago',
        nextRun: 'Today at 02:00 UTC',
        rowCount: '1.4B rows',
      },
      {
        id: 'pol-2',
        tableName: 'telemetry.raw_pings',
        database: 'BIGQUERY_IOT_STREAM',
        engine: 'BigQuery',
        partitionColumn: 'received_at (HOUR)',
        retentionPeriod: '30 Days TTL',
        lifecycleAction: 'Direct Partition Drop at 30d TTL -> Zero Archive',
        archiveTier: 'Purge Immediately',
        complianceReason: 'Storage Optimization',
        totalSize: '5.6 TB',
        coldSize: '0.0 TB',
        estimatedMonthlySavings: '$560.00 / mo',
        enabled: true,
        lastRun: '10 min ago',
        nextRun: 'Continuous partition sweep',
        rowCount: '8.9B rows',
      },
      {
        id: 'pol-3',
        tableName: 'billing.invoices_archive',
        database: 'SNOWFLAKE_FINANCE',
        engine: 'Snowflake',
        partitionColumn: 'invoice_date (MONTH)',
        retentionPeriod: '7 Years Compliance',
        lifecycleAction: 'Hot 1 Year -> Glacier Vault Lock Tier 7 Years',
        archiveTier: 'Glacier Deep',
        complianceReason: 'SOX Compliance',
        totalSize: '2.4 TB',
        coldSize: '2.1 TB',
        estimatedMonthlySavings: '$210.00 / mo',
        enabled: true,
        lastRun: 'Yesterday',
        nextRun: 'Sep 01 at 00:00 UTC',
        rowCount: '180M rows',
      },
      {
        id: 'pol-4',
        tableName: 'analytics.staging_events',
        database: 'BIGQUERY_INGEST',
        engine: 'BigQuery',
        partitionColumn: 'ingest_time (DAY)',
        retentionPeriod: '14 Days TTL',
        lifecycleAction: 'Truncate partition > 14d -> Permanent Delete',
        archiveTier: 'Purge Immediately',
        complianceReason: 'Storage Optimization',
        totalSize: '1.5 TB',
        coldSize: '0.0 TB',
        estimatedMonthlySavings: '$150.00 / mo',
        enabled: false,
        lastRun: '3 days ago',
        nextRun: 'Paused',
        rowCount: '420M rows',
      },
      {
        id: 'pol-5',
        tableName: 'customer_pii.user_sessions',
        database: 'SNOWFLAKE_CUSTOMER360',
        engine: 'Snowflake',
        partitionColumn: 'session_start (DAY)',
        retentionPeriod: '180 Days Active',
        lifecycleAction: 'Anonymize at 90d -> Cold Glacier -> Hard Purge at 180d',
        archiveTier: 'Standard Cold',
        complianceReason: 'GDPR Art. 17',
        totalSize: '1.1 TB',
        coldSize: '0.9 TB',
        estimatedMonthlySavings: '$80.00 / mo',
        enabled: true,
        lastRun: '6 hours ago',
        nextRun: 'Tomorrow at 04:00 UTC',
        rowCount: '290M rows',
      },
    ]
    
    export const defaultStats: StorageRetentionStats = {
      totalManagedData: '14.8 TB',
      totalManagedSubtitle: 'Across 48 partitioned tables',
      coldArchivedData: '9.2 TB',
      archivedPercentage: 62,
      archivedSubtitle: '62% archived to Glacier / Cold Tier',
      projectedMonthlySavings: '$1,420.00 / mo saved',
      savingsSubtitle: '68% cost drop vs active NVMe',
      purged30d: '1.8 TB',
      purgedSubtitle: 'Expired TTL partitions purged',
    }
    
    interface BannerMessage {
      type: 'success' | 'info'
      text: string
    }
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-data-retention-policy-manager, [ui-data-retention-policy-manager]',
      standalone: true,
      // React renders the root <div> itself: the host stays out of layout and `class` goes to the root.
      host: { '[attr.class]': '"contents"' },
      imports: [
        LucideAngularModule,
        UiBadgeComponent,
        UiButtonComponent,
        UiCardComponent,
        UiCardContentComponent,
        UiCardDescriptionComponent,
        UiCardHeaderComponent,
        UiCardTitleComponent,
        UiDialogComponent,
        UiDialogContentComponent,
        UiDialogDescriptionComponent,
        UiDialogFooterComponent,
        UiDialogHeaderComponent,
        UiDialogTitleComponent,
        UiDropdownMenuComponent,
        UiDropdownMenuContentComponent,
        UiDropdownMenuItemComponent,
        UiDropdownMenuLabelComponent,
        UiDropdownMenuSeparatorComponent,
        UiDropdownMenuTriggerComponent,
        UiInputComponent,
        UiProgressComponent,
        UiSelectComponent,
        UiSelectContentComponent,
        UiSelectItemComponent,
        UiSelectTriggerComponent,
        UiSelectValueComponent,
        UiSwitchComponent,
        UiTableComponent,
        UiTableBodyComponent,
        UiTableCellComponent,
        UiTableHeadComponent,
        UiTableHeaderComponent,
        UiTableRowComponent,
      ],
      template: `
        <div data-slot="data-retention-policy-manager" [class]="rootClass">
          <!-- Header Section -->
          <div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
            <div class="space-y-1">
              <div class="flex items-center gap-2">
                <div class="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-lg">
                  <lucide-icon [img]="Archive" class="size-4.5" />
                </div>
                <h1 class="text-foreground text-2xl font-bold tracking-tight">
                  Data Retention &amp; Cold Storage Lifecycle
                </h1>
              </div>
              <p class="text-muted-foreground text-sm">
                Configure automated table partition expiration, cold storage tiering, and compliance retention periods.
              </p>
            </div>
    
            <div class="flex items-center gap-2">
              <button ui-button class="gap-1.5" (click)="openAddPolicyModal()">
                <lucide-icon [img]="Plus" class="size-4" />
                Add Retention Policy
              </button>
            </div>
          </div>
    
          <!-- Notification / Action banner -->
          @if (bannerMessage(); as banner) {
            <div class="border-border bg-card flex items-center justify-between gap-3 rounded-lg border p-3 shadow-xs">
              <div class="flex items-center gap-2.5 text-sm">
                @if (banner.type === 'success') {
                  <lucide-icon [img]="CheckCircle2" class="text-success size-4 shrink-0" />
                } @else {
                  <lucide-icon [img]="Info" class="text-primary size-4 shrink-0" />
                }
                <span class="text-foreground text-xs font-medium sm:text-sm">{{ banner.text }}</span>
              </div>
              <button ui-button variant="ghost" size="icon" class="size-7" (click)="bannerMessage.set(null)">
                <lucide-icon [img]="X" class="size-3.5" />
                <span class="sr-only">Dismiss</span>
              </button>
            </div>
          }
    
          <!-- 4 Storage & Cost Impact Cards -->
          <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
            <!-- Total Managed Data -->
            <div ui-card class="shadow-xs">
              <div ui-card-header class="flex flex-row items-center justify-between pb-2">
                <h3 ui-card-title class="text-muted-foreground text-sm font-medium">Total Managed Data</h3>
                <div class="bg-primary/10 text-primary rounded-lg p-2">
                  <lucide-icon [img]="Database" class="size-4" />
                </div>
              </div>
              <div ui-card-content class="space-y-1">
                <div class="text-foreground text-2xl font-bold tracking-tight">{{ stats.totalManagedData }}</div>
                <p class="text-muted-foreground text-xs">{{ stats.totalManagedSubtitle }}</p>
              </div>
            </div>
    
            <!-- Auto-Archived to Glacier / Cold Tier -->
            <div ui-card class="shadow-xs">
              <div ui-card-header class="flex flex-row items-center justify-between pb-2">
                <h3 ui-card-title class="text-muted-foreground text-sm font-medium">
                  Auto-Archived to Glacier / Cold Tier
                </h3>
                <div class="bg-success/10 text-success rounded-lg p-2">
                  <lucide-icon [img]="HardDriveDownload" class="size-4" />
                </div>
              </div>
              <div ui-card-content class="space-y-2">
                <div class="flex items-center justify-between gap-x-2">
                  <span class="text-foreground text-2xl font-bold tracking-tight">{{ stats.coldArchivedData }}</span>
                  <span ui-badge variant="info" class="text-xs">{{ stats.archivedPercentage }}% archived</span>
                </div>
                <div ui-progress [value]="stats.archivedPercentage" class="h-1.5"></div>
                <p class="text-muted-foreground text-xs">{{ stats.archivedSubtitle }}</p>
              </div>
            </div>
    
            <!-- Projected Monthly Storage Savings -->
            <div ui-card class="shadow-xs">
              <div ui-card-header class="flex flex-row items-center justify-between pb-2">
                <h3 ui-card-title class="text-muted-foreground text-sm font-medium">Projected Monthly Savings</h3>
                <div class="bg-success/10 text-success rounded-lg p-2">
                  <lucide-icon [img]="DollarSign" class="size-4" />
                </div>
              </div>
              <div ui-card-content class="space-y-1">
                <div class="flex items-center gap-2">
                  <span class="text-foreground text-2xl font-bold tracking-tight">{{ stats.projectedMonthlySavings }}</span>
                  <span ui-badge variant="success" class="text-xs">Active</span>
                </div>
                <p class="text-muted-foreground text-xs">{{ stats.savingsSubtitle }}</p>
              </div>
            </div>
    
            <!-- Expired Data Purged in 30d -->
            <div ui-card class="shadow-xs">
              <div ui-card-header class="flex flex-row items-center justify-between pb-2">
                <h3 ui-card-title class="text-muted-foreground text-sm font-medium">Expired Data Purged (30d)</h3>
                <div class="bg-warning/10 text-warning rounded-lg p-2">
                  <lucide-icon [img]="Trash2" class="size-4" />
                </div>
              </div>
              <div ui-card-content class="space-y-1">
                <div class="flex items-center gap-2">
                  <span class="text-foreground text-2xl font-bold tracking-tight">{{ stats.purged30d }}</span>
                  <span ui-badge variant="outline" class="text-xs">TTL Drops</span>
                </div>
                <p class="text-muted-foreground text-xs">{{ stats.purgedSubtitle }}</p>
              </div>
            </div>
          </div>
    
          <!-- Retention Policies Table Card -->
          <div ui-card class="shadow-xs">
            <div ui-card-header class="pb-3">
              <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
                <div>
                  <h3 ui-card-title class="text-lg font-semibold">Table Retention Policies &amp; Partition TTL</h3>
                  <p ui-card-description class="text-xs">
                    Automated lifecycle transition rules, cold tiering, and compliance audit locks across databases.
                  </p>
                </div>
    
                <!-- Filter Toolbar -->
                <div class="flex flex-wrap items-center gap-2">
                  <div class="relative w-full sm:w-64">
                    <lucide-icon
                      [img]="Search"
                      class="text-muted-foreground pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2"
                    />
                    <ui-input
                      [value]="searchQuery()"
                      (valueChange)="searchQuery.set($event)"
                      placeholder="Search table, database, or compliance..."
                      class="h-8 pl-9 text-xs"
                    />
                  </div>
    
                  <ui-select [value]="statusFilter()" (valueChange)="statusFilter.set($event)">
                    <button ui-select-trigger class="h-8 w-32 text-xs">
                      <ui-select-value placeholder="All Status" />
                    </button>
                    <ui-select-content>
                      <ui-select-item value="all">All Status</ui-select-item>
                      <ui-select-item value="enabled">Active</ui-select-item>
                      <ui-select-item value="disabled">Disabled</ui-select-item>
                    </ui-select-content>
                  </ui-select>
    
                  <ui-select [value]="complianceFilter()" (valueChange)="complianceFilter.set($event)">
                    <button ui-select-trigger class="h-8 w-38 text-xs">
                      <ui-select-value placeholder="All Compliance" />
                    </button>
                    <ui-select-content>
                      <ui-select-item value="all">All Compliance</ui-select-item>
                      <ui-select-item value="SOX Compliance">SOX Compliance</ui-select-item>
                      <ui-select-item value="GDPR Art. 17">GDPR Art. 17</ui-select-item>
                      <ui-select-item value="Storage Optimization">Storage Optimization</ui-select-item>
                    </ui-select-content>
                  </ui-select>
                </div>
              </div>
            </div>
    
            <div 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="min-w-[240px]">Table Name &amp; Database</th>
                      <th ui-table-head class="min-w-[150px]">Retention Period</th>
                      <th ui-table-head class="min-w-[280px]">Lifecycle Action &amp; Tiering</th>
                      <th ui-table-head class="min-w-[150px]">Compliance Reason</th>
                      <th ui-table-head class="min-w-[120px]">Status</th>
                      <th ui-table-head class="w-16 text-right">Actions</th>
                    </tr>
                  </thead>
                  <tbody ui-table-body>
                    @for (policy of filteredPolicies(); track policy.id) {
                      <tr ui-table-row [class]="rowClass(policy)">
                        <!-- Table Name & Database -->
                        <td ui-table-cell class="py-3.5 align-top">
                          <div class="space-y-1">
                            <div class="flex items-center gap-1.5">
                              <span class="text-foreground font-mono text-xs font-semibold">{{ policy.tableName }}</span>
                              <button
                                type="button"
                                class="text-muted-foreground hover:text-foreground inline-flex size-4 shrink-0 items-center justify-center transition-colors"
                                [title]="copiedTable() === policy.tableName ? 'Copied!' : 'Copy table name'"
                                (click)="copyTableName(policy.tableName)"
                              >
                                @if (copiedTable() === policy.tableName) {
                                  <lucide-icon [img]="Check" class="text-success size-3" />
                                } @else {
                                  <lucide-icon [img]="Copy" class="size-3" />
                                }
                              </button>
                            </div>
                            <div class="flex flex-wrap items-center gap-1.5 text-xs">
                              <span ui-badge variant="outline" class="font-mono text-xs">{{ policy.engine }}</span>
                              <span class="text-muted-foreground font-mono text-xs">{{ policy.database }}</span>
                            </div>
                            @if (policy.rowCount) {
                              <div class="text-muted-foreground text-xs">
                                {{ policy.rowCount }} · {{ policy.totalSize }}
                              </div>
                            }
                          </div>
                        </td>
    
                        <!-- Retention Period Badge -->
                        <td ui-table-cell class="py-3.5 align-top">
                          <div class="space-y-1">
                            <span ui-badge [variant]="retentionBadgeVariant(policy.retentionPeriod)" class="gap-1 text-xs">
                              <lucide-icon [img]="Clock" class="size-3" />
                              {{ policy.retentionPeriod }}
                            </span>
                            <p class="text-muted-foreground font-mono text-xs">Col: {{ policy.partitionColumn }}</p>
                          </div>
                        </td>
    
                        <!-- Lifecycle Action & Tiering -->
                        <td ui-table-cell class="py-3.5 align-top">
                          <div class="space-y-1">
                            <div class="text-foreground flex items-center gap-1.5 text-xs font-medium">
                              <lucide-icon [img]="Archive" class="text-muted-foreground size-3.5 shrink-0" />
                              <span>{{ policy.lifecycleAction }}</span>
                            </div>
                            <div class="text-muted-foreground flex flex-wrap items-center gap-2 text-xs">
                              <span>
                                Tier: <strong class="text-foreground font-normal">{{ policy.archiveTier }}</strong>
                              </span>
                              <span>·</span>
                              <span>
                                Savings:
                                <span class="text-success font-semibold">{{ policy.estimatedMonthlySavings }}</span>
                              </span>
                            </div>
                          </div>
                        </td>
    
                        <!-- Compliance Reason -->
                        <td ui-table-cell class="py-3.5 align-top">
                          <div class="space-y-1">
                            <span
                              ui-badge
                              [variant]="complianceBadgeVariant(policy.complianceReason)"
                              class="gap-1 text-xs"
                            >
                              @if (isShieldCompliance(policy.complianceReason)) {
                                <lucide-icon [img]="ShieldCheck" class="size-3" />
                              } @else {
                                <lucide-icon [img]="FileText" class="size-3" />
                              }
                              {{ policy.complianceReason }}
                            </span>
                            <p class="text-muted-foreground text-xs">Next: {{ policy.nextRun }}</p>
                          </div>
                        </td>
    
                        <!-- Status Switch -->
                        <td ui-table-cell class="py-3.5 align-middle">
                          <div class="flex items-center gap-2">
                            <button
                              ui-switch
                              [checked]="policy.enabled"
                              [attr.aria-label]="'Toggle retention policy for ' + policy.tableName"
                              (checkedChange)="togglePolicyStatus(policy.id)"
                            ></button>
                            <span [class]="statusTextClass(policy)">{{ policy.enabled ? 'Enabled' : 'Disabled' }}</span>
                          </div>
                        </td>
    
                        <!-- Action Menu -->
                        <td ui-table-cell class="py-3.5 text-right align-middle">
                          <ui-dropdown-menu>
                            <button ui-button ui-dropdown-menu-trigger variant="ghost" size="icon" class="size-8">
                              <lucide-icon [img]="MoreHorizontal" class="size-4" />
                              <span class="sr-only">Open actions for {{ policy.tableName }}</span>
                            </button>
                            <ui-dropdown-menu-content align="end" class="w-52">
                              <ui-dropdown-menu-label class="text-xs">Policy Controls</ui-dropdown-menu-label>
                              <ui-dropdown-menu-separator />
                              <ui-dropdown-menu-item class="cursor-pointer" (select)="openRunArchiveModal(policy)">
                                <lucide-icon [img]="Play" class="text-success mr-2 size-4" />
                                <span>Run Archive Now</span>
                              </ui-dropdown-menu-item>
                              <ui-dropdown-menu-item class="cursor-pointer" (select)="openEditPolicyModal(policy)">
                                <lucide-icon [img]="Pencil" class="mr-2 size-4" />
                                <span>Edit Policy</span>
                              </ui-dropdown-menu-item>
                              <ui-dropdown-menu-item class="cursor-pointer" (select)="copyTableName(policy.tableName)">
                                <lucide-icon [img]="Copy" class="mr-2 size-4" />
                                <span>Copy Table Name</span>
                              </ui-dropdown-menu-item>
                              <ui-dropdown-menu-separator />
                              <ui-dropdown-menu-item
                                class="text-destructive focus:text-destructive cursor-pointer"
                                (select)="deletePolicy(policy.id)"
                              >
                                <lucide-icon [img]="Trash2" class="mr-2 size-4" />
                                <span>Delete Policy</span>
                              </ui-dropdown-menu-item>
                            </ui-dropdown-menu-content>
                          </ui-dropdown-menu>
                        </td>
                      </tr>
                    }
    
                    @if (filteredPolicies().length === 0) {
                      <tr ui-table-row>
                        <td ui-table-cell colspan="6" class="text-muted-foreground h-32 text-center text-sm">
                          <div class="flex flex-col items-center justify-center space-y-1">
                            <lucide-icon [img]="Search" class="text-muted-foreground/40 size-6" />
                            <p class="text-foreground text-sm font-medium">No retention policies found</p>
                            <p class="text-muted-foreground text-xs">Try adjusting your search query or filter settings.</p>
                          </div>
                        </td>
                      </tr>
                    }
                  </tbody>
                </div>
              </div>
            </div>
          </div>
    
          <!-- Add / Edit Policy Dialog -->
          <ui-dialog [open]="isPolicyModalOpen()" (openChange)="isPolicyModalOpen.set($event)">
            <ui-dialog-content class="sm:max-w-lg">
              <div ui-dialog-header>
                <h2 ui-dialog-title>{{ editingPolicy() ? 'Edit Retention Policy' : 'Add Retention Policy' }}</h2>
                <p ui-dialog-description class="text-xs">
                  Configure partition expiration time-to-live, cold tier transitions, and legal retention mandates.
                </p>
              </div>
    
              <div class="space-y-3.5 py-2">
                <div class="space-y-1">
                  <label for="policy-table-name" class="text-foreground text-xs font-medium"
                    >Target Table (schema.table)</label
                  >
                  <ui-input
                    id="policy-table-name"
                    [value]="formTableName()"
                    (valueChange)="formTableName.set($event)"
                    placeholder="e.g. telemetry.clickstream_raw"
                    class="font-mono text-xs"
                  />
                </div>
    
                <div class="grid grid-cols-2 gap-3">
                  <div class="space-y-1">
                    <label for="policy-engine" class="text-foreground text-xs font-medium">Engine</label>
                    <ui-select [value]="formEngine()" (valueChange)="setFormEngine($event)">
                      <button ui-select-trigger id="policy-engine" class="text-xs">
                        <ui-select-value placeholder="Engine" />
                      </button>
                      <ui-select-content>
                        <ui-select-item value="Snowflake">Snowflake</ui-select-item>
                        <ui-select-item value="BigQuery">BigQuery</ui-select-item>
                        <ui-select-item value="Databricks">Databricks</ui-select-item>
                        <ui-select-item value="Postgres">Postgres</ui-select-item>
                      </ui-select-content>
                    </ui-select>
                  </div>
    
                  <div class="space-y-1">
                    <label for="policy-database" class="text-foreground text-xs font-medium">Database / Cluster</label>
                    <ui-input
                      id="policy-database"
                      [value]="formDatabase()"
                      (valueChange)="formDatabase.set($event)"
                      placeholder="e.g. SNOWFLAKE_PROD_DWH"
                      class="font-mono text-xs"
                    />
                  </div>
                </div>
    
                <div class="grid grid-cols-2 gap-3">
                  <div class="space-y-1">
                    <label for="policy-partition-col" class="text-foreground text-xs font-medium">Partition Column</label>
                    <ui-input
                      id="policy-partition-col"
                      [value]="formPartitionCol()"
                      (valueChange)="formPartitionCol.set($event)"
                      placeholder="e.g. event_timestamp (DAY)"
                      class="font-mono text-xs"
                    />
                  </div>
    
                  <div class="space-y-1">
                    <label for="policy-retention-period" class="text-foreground text-xs font-medium"
                      >Retention Period</label
                    >
                    <ui-select [value]="formRetentionPeriod()" (valueChange)="formRetentionPeriod.set($event)">
                      <button ui-select-trigger id="policy-retention-period" class="text-xs">
                        <ui-select-value placeholder="Period" />
                      </button>
                      <ui-select-content>
                        <ui-select-item value="14 Days TTL">14 Days TTL</ui-select-item>
                        <ui-select-item value="30 Days TTL">30 Days TTL</ui-select-item>
                        <ui-select-item value="90 Days Active">90 Days Active</ui-select-item>
                        <ui-select-item value="180 Days Active">180 Days Active</ui-select-item>
                        <ui-select-item value="365 Days Active">365 Days Active</ui-select-item>
                        <ui-select-item value="7 Years Compliance">7 Years Compliance</ui-select-item>
                      </ui-select-content>
                    </ui-select>
                  </div>
                </div>
    
                <div class="space-y-1">
                  <label for="policy-lifecycle-action" class="text-foreground text-xs font-medium">
                    Lifecycle Transition Action
                  </label>
                  <ui-input
                    id="policy-lifecycle-action"
                    [value]="formLifecycleAction()"
                    (valueChange)="formLifecycleAction.set($event)"
                    placeholder="e.g. Transition to Cold Glacier at 90d -> Hard Purge at 365d"
                    class="text-xs"
                  />
                </div>
    
                <div class="grid grid-cols-2 gap-3">
                  <div class="space-y-1">
                    <label for="policy-archive-tier" class="text-foreground text-xs font-medium">Cold Storage Tier</label>
                    <ui-select [value]="formArchiveTier()" (valueChange)="setFormArchiveTier($event)">
                      <button ui-select-trigger id="policy-archive-tier" class="text-xs">
                        <ui-select-value placeholder="Tier" />
                      </button>
                      <ui-select-content>
                        <ui-select-item value="Glacier Deep">Glacier Deep Archive</ui-select-item>
                        <ui-select-item value="Coldline">BigQuery Coldline</ui-select-item>
                        <ui-select-item value="Standard Cold">Standard Cold</ui-select-item>
                        <ui-select-item value="Purge Immediately">Purge Immediately</ui-select-item>
                      </ui-select-content>
                    </ui-select>
                  </div>
    
                  <div class="space-y-1">
                    <label for="policy-compliance" class="text-foreground text-xs font-medium">Compliance Framework</label>
                    <ui-select [value]="formComplianceReason()" (valueChange)="formComplianceReason.set($event)">
                      <button ui-select-trigger id="policy-compliance" class="text-xs">
                        <ui-select-value placeholder="Compliance" />
                      </button>
                      <ui-select-content>
                        <ui-select-item value="SOX Compliance">SOX Compliance</ui-select-item>
                        <ui-select-item value="GDPR Art. 17">GDPR Art. 17</ui-select-item>
                        <ui-select-item value="Storage Optimization">Storage Optimization</ui-select-item>
                        <ui-select-item value="PCI-DSS v4.0">PCI-DSS v4.0</ui-select-item>
                      </ui-select-content>
                    </ui-select>
                  </div>
                </div>
              </div>
    
              <div ui-dialog-footer>
                <button ui-button variant="outline" (click)="isPolicyModalOpen.set(false)">Cancel</button>
                <button ui-button [disabled]="!formTableName().trim()" (click)="savePolicy()">
                  {{ editingPolicy() ? 'Save Policy Changes' : 'Create Retention Policy' }}
                </button>
              </div>
            </ui-dialog-content>
          </ui-dialog>
    
          <!-- Run Archive Dry-Run & Execution Modal -->
          <ui-dialog [open]="isArchiveModalOpen()" (openChange)="isArchiveModalOpen.set($event)">
            <ui-dialog-content class="sm:max-w-md">
              <div ui-dialog-header>
                <div class="flex items-center gap-2">
                  <div class="bg-primary/10 text-primary rounded-lg p-2">
                    <lucide-icon [img]="HardDriveDownload" class="size-5" />
                  </div>
                  <div>
                    <h2 ui-dialog-title>Run Archive Partition Sweep</h2>
                    <p ui-dialog-description class="text-xs">
                      Trigger manual partition evaluation and transition expired partitions to cold storage.
                    </p>
                  </div>
                </div>
              </div>
    
              <div class="space-y-3 py-2 text-xs">
                <div class="border-border bg-muted/40 space-y-2 rounded-lg border p-3">
                  <div class="flex items-center justify-between gap-x-2">
                    <span class="text-muted-foreground">Target Table:</span>
                    <span class="text-foreground font-mono font-semibold">{{ selectedPolicyForArchive()?.tableName }}</span>
                  </div>
                  <div class="flex items-center justify-between gap-x-2">
                    <span class="text-muted-foreground">Database Engine:</span>
                    <span ui-badge variant="outline" class="font-mono text-xs">{{
                      selectedPolicyForArchive()?.engine
                    }}</span>
                  </div>
                  <div class="flex items-center justify-between gap-x-2">
                    <span class="text-muted-foreground">Lifecycle Action:</span>
                    <span class="text-foreground font-medium">{{ selectedPolicyForArchive()?.lifecycleAction }}</span>
                  </div>
                  <div class="flex items-center justify-between gap-x-2">
                    <span class="text-muted-foreground">Est. Savings:</span>
                    <span class="text-success font-semibold">{{
                      selectedPolicyForArchive()?.estimatedMonthlySavings
                    }}</span>
                  </div>
                </div>
    
                <div class="border-border bg-card text-muted-foreground space-y-1 rounded-lg border p-3">
                  <div class="text-foreground flex items-center gap-1.5 font-medium">
                    <lucide-icon [img]="ShieldCheck" class="text-success size-4" />
                    <span>Dry-Run Check Passed</span>
                  </div>
                  <p class="text-xs">
                    Partitions older than configured threshold will be compressed, archived into cold tier, and purged from
                    active storage without table locks.
                  </p>
                </div>
              </div>
    
              <div ui-dialog-footer>
                <button ui-button variant="outline" [disabled]="isArchiving()" (click)="isArchiveModalOpen.set(false)">
                  Cancel
                </button>
                <button ui-button [disabled]="isArchiving()" class="gap-1.5" (click)="executeArchiveRun()">
                  @if (isArchiving()) {
                    <lucide-icon [img]="RefreshCw" class="size-4 animate-spin" />
                    Archiving Partitions...
                  } @else {
                    <lucide-icon [img]="Play" class="size-4" />
                    Run Archive Sweep Now
                  }
                </button>
              </div>
            </ui-dialog-content>
          </ui-dialog>
        </div>
      `,
    })
    export class UiDataRetentionPolicyManagerComponent {
      protected readonly Archive = Archive
      protected readonly Check = Check
      protected readonly CheckCircle2 = CheckCircle2
      protected readonly Clock = Clock
      protected readonly Copy = Copy
      protected readonly Database = Database
      protected readonly DollarSign = DollarSign
      protected readonly FileText = FileText
      protected readonly HardDriveDownload = HardDriveDownload
      protected readonly Info = Info
      protected readonly MoreHorizontal = MoreHorizontal
      protected readonly Pencil = Pencil
      protected readonly Play = Play
      protected readonly Plus = Plus
      protected readonly RefreshCw = RefreshCw
      protected readonly Search = Search
      protected readonly ShieldCheck = ShieldCheck
      protected readonly Trash2 = Trash2
      protected readonly X = X
    
      private readonly destroyRef = inject(DestroyRef)
      private readonly timers = new Set<ReturnType<typeof setTimeout>>()
    
      /** Seed rows; later edits live in the component's own state, as in React's useState initialiser. */
      @Input() set initialPolicies(value: RetentionPolicy[] | undefined) {
        this.policies.set(value ?? defaultPolicies)
      }
      @Input() initialStats?: StorageRetentionStats
      @Input('class') className?: string
    
      readonly policies = signal<RetentionPolicy[]>(defaultPolicies)
      readonly searchQuery = signal('')
      readonly statusFilter = signal('all')
      readonly complianceFilter = signal('all')
      readonly bannerMessage = signal<BannerMessage | null>(null)
      readonly copiedTable = signal<string | null>(null)
    
      // Policy dialog state
      readonly isPolicyModalOpen = signal(false)
      readonly editingPolicy = signal<RetentionPolicy | null>(null)
      readonly formTableName = signal('')
      readonly formDatabase = signal('')
      readonly formEngine = signal<EngineType>('Snowflake')
      readonly formPartitionCol = signal('')
      readonly formRetentionPeriod = signal('90 Days Active')
      readonly formLifecycleAction = signal('Transition to Cold Glacier at 90d -> Hard Purge at 365d')
      readonly formArchiveTier = signal<ArchiveTierType>('Glacier Deep')
      readonly formComplianceReason = signal('Storage Optimization')
    
      // Instant archive dialog state
      readonly isArchiveModalOpen = signal(false)
      readonly selectedPolicyForArchive = signal<RetentionPolicy | null>(null)
      readonly isArchiving = signal(false)
    
      constructor() {
        this.destroyRef.onDestroy(() => {
          for (const t of this.timers) clearTimeout(t)
          this.timers.clear()
        })
      }
    
      get stats(): StorageRetentionStats {
        return this.initialStats ?? defaultStats
      }
    
      get rootClass(): string {
        return cn('w-full space-y-6', this.className)
      }
    
      readonly filteredPolicies = computed(() => {
        const q = this.searchQuery().trim().toLowerCase()
        const status = this.statusFilter()
        const compliance = this.complianceFilter()
        return this.policies().filter((policy) => {
          if (status === 'enabled' && !policy.enabled) return false
          if (status === 'disabled' && policy.enabled) return false
          if (compliance !== 'all' && policy.complianceReason !== compliance) return false
          if (!q) return true
          return (
            policy.tableName.toLowerCase().includes(q) ||
            policy.database.toLowerCase().includes(q) ||
            policy.complianceReason.toLowerCase().includes(q) ||
            policy.lifecycleAction.toLowerCase().includes(q) ||
            policy.engine.toLowerCase().includes(q)
          )
        })
      })
    
      rowClass(policy: RetentionPolicy): string {
        return cn('group transition-colors', !policy.enabled && 'opacity-70 dark:opacity-65')
      }
    
      statusTextClass(policy: RetentionPolicy): string {
        return cn('text-xs font-medium', policy.enabled ? 'text-success' : 'text-muted-foreground')
      }
    
      isShieldCompliance(reason: string): boolean {
        return reason.includes('SOX') || reason.includes('GDPR')
      }
    
      togglePolicyStatus(id: string): void {
        this.policies.update((prev) =>
          prev.map((policy) => {
            if (policy.id !== id) return policy
            const nextState = !policy.enabled
            this.bannerMessage.set({
              type: 'info',
              text: `Retention policy for table "${policy.tableName}" has been ${nextState ? 'activated' : 'paused'}.`,
            })
            return { ...policy, enabled: nextState }
          }),
        )
      }
    
      deletePolicy(id: string): void {
        const target = this.policies().find((p) => p.id === id)
        this.policies.update((prev) => prev.filter((p) => p.id !== id))
        if (target) {
          this.bannerMessage.set({ type: 'info', text: `Deleted retention policy for "${target.tableName}".` })
        }
      }
    
      copyTableName(name: string): void {
        if (typeof navigator !== 'undefined' && navigator.clipboard) {
          void navigator.clipboard.writeText(name)
          this.copiedTable.set(name)
          this.later(() => this.copiedTable.update((curr) => (curr === name ? null : curr)), 2000)
        }
      }
    
      openAddPolicyModal(): void {
        this.editingPolicy.set(null)
        this.formTableName.set('')
        this.formDatabase.set('SNOWFLAKE_PROD_DWH')
        this.formEngine.set('Snowflake')
        this.formPartitionCol.set('created_at (DAY)')
        this.formRetentionPeriod.set('90 Days Active')
        this.formLifecycleAction.set('Transition to Cold Glacier at 90d -> Hard Purge at 365d')
        this.formArchiveTier.set('Glacier Deep')
        this.formComplianceReason.set('Storage Optimization')
        this.isPolicyModalOpen.set(true)
      }
    
      openEditPolicyModal(policy: RetentionPolicy): void {
        this.editingPolicy.set(policy)
        this.formTableName.set(policy.tableName)
        this.formDatabase.set(policy.database)
        this.formEngine.set(policy.engine)
        this.formPartitionCol.set(policy.partitionColumn)
        this.formRetentionPeriod.set(policy.retentionPeriod)
        this.formLifecycleAction.set(policy.lifecycleAction)
        this.formArchiveTier.set(policy.archiveTier)
        this.formComplianceReason.set(policy.complianceReason)
        this.isPolicyModalOpen.set(true)
      }
    
      setFormEngine(value: string): void {
        this.formEngine.set(value as EngineType)
      }
    
      setFormArchiveTier(value: string): void {
        this.formArchiveTier.set(value as ArchiveTierType)
      }
    
      savePolicy(): void {
        const tableName = this.formTableName().trim()
        if (!tableName) return
        const editing = this.editingPolicy()
    
        if (editing) {
          this.policies.update((prev) =>
            prev.map((p) =>
              p.id === editing.id
                ? {
                    ...p,
                    tableName,
                    database: this.formDatabase().trim() || 'SNOWFLAKE_PROD_DWH',
                    engine: this.formEngine(),
                    partitionColumn: this.formPartitionCol().trim() || 'event_timestamp',
                    retentionPeriod: this.formRetentionPeriod(),
                    lifecycleAction: this.formLifecycleAction(),
                    archiveTier: this.formArchiveTier(),
                    complianceReason: this.formComplianceReason(),
                  }
                : p,
            ),
          )
          this.bannerMessage.set({ type: 'success', text: `Updated retention policy for "${this.formTableName()}".` })
        } else {
          const newPolicy: RetentionPolicy = {
            id: `pol-${Date.now()}`,
            tableName,
            database: this.formDatabase().trim() || 'SNOWFLAKE_PROD_DWH',
            engine: this.formEngine(),
            partitionColumn: this.formPartitionCol().trim() || 'created_at (DAY)',
            retentionPeriod: this.formRetentionPeriod(),
            lifecycleAction: this.formLifecycleAction(),
            archiveTier: this.formArchiveTier(),
            complianceReason: this.formComplianceReason(),
            totalSize: '1.2 TB',
            coldSize: '0.8 TB',
            estimatedMonthlySavings: '$110.00 / mo',
            enabled: true,
            lastRun: 'Pending First Run',
            nextRun: 'Tonight at 00:00 UTC',
            rowCount: '150M rows',
          }
          this.policies.update((prev) => [newPolicy, ...prev])
          this.bannerMessage.set({
            type: 'success',
            text: `Created new automated retention policy for "${newPolicy.tableName}".`,
          })
        }
        this.isPolicyModalOpen.set(false)
      }
    
      openRunArchiveModal(policy: RetentionPolicy): void {
        this.selectedPolicyForArchive.set(policy)
        this.isArchiveModalOpen.set(true)
      }
    
      executeArchiveRun(): void {
        const target = this.selectedPolicyForArchive()
        if (!target) return
        this.isArchiving.set(true)
        this.later(() => {
          this.isArchiving.set(false)
          this.isArchiveModalOpen.set(false)
          this.policies.update((prev) => prev.map((p) => (p.id === target.id ? { ...p, lastRun: 'Just now' } : p)))
          this.bannerMessage.set({
            type: 'success',
            text: `Archive partition sweep successfully completed for "${target.tableName}". Cold tier storage synchronized.`,
          })
        }, 1200)
      }
    
      retentionBadgeVariant(period: string): BadgeVariant {
        if (period.includes('7 Years') || period.includes('Compliance')) return 'success'
        if (period.includes('TTL') || period.includes('14 Days') || period.includes('30 Days')) return 'warning'
        if (period.includes('90 Days') || period.includes('180 Days')) return 'info'
        return 'secondary'
      }
    
      complianceBadgeVariant(reason: string): BadgeVariant {
        if (reason.includes('GDPR') || reason.includes('CCPA')) return 'info'
        if (reason.includes('SOX') || reason.includes('HIPAA')) return 'success'
        if (reason.includes('Storage') || reason.includes('Optimization')) return 'secondary'
        if (reason.includes('PCI')) return 'warning'
        return 'outline'
      }
    
      private later(fn: () => void, ms: number): void {
        const t = setTimeout(() => {
          this.timers.delete(t)
          fn()
        }, ms)
        this.timers.add(t)
      }
    }
    
  • src/app/components/blocks/data-retention-policy-manager/index.ts0.2 kB

Raw manifest:https://uipkge.dev/r/angular/data-retention-policy-manager.json