Data Retention Policy Manager
blockdevopsSnowflake 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
$pnpm dlx uipkge-ng@latest add data-retention-policy-manager$npx uipkge-ng@latest add data-retention-policy-manager$yarn dlx uipkge-ng@latest add data-retention-policy-manager$bunx uipkge-ng@latest add data-retention-policy-managerManifest:
https://uipkge.dev/r/angular/data-retention-policy-manager.jsonInstalls to:src/app/components/blocks/data-retention-policy-manager/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.
RetentionPolicyinterface 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
}StorageRetentionStatsinterface StorageRetentionStats {
totalManagedData: string
totalManagedSubtitle?: string
coldArchivedData: string
archivedPercentage: number
archivedSubtitle?: string
projectedMonthlySavings: string
savingsSubtitle?: string
purged30d: string
purgedSubtitle?: string
}BannerMessageinterface 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 & 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 & 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 & Database</th> <th ui-table-head class="min-w-[150px]">Retention Period</th> <th ui-table-head class="min-w-[280px]">Lifecycle Action & 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
export { UiDataRetentionPolicyManagerComponent, defaultPolicies, defaultStats, type ArchiveTierType, type EngineType, type RetentionPolicy, type StorageRetentionStats, } from './data-retention-policy-manager.component'
Raw manifest:https://uipkge.dev/r/angular/data-retention-policy-manager.json