Menu

Change language

GitHub repo

Reverse Etl Sync Manager

blockdevops

Census and Hightouch style Reverse ETL sync manager and data activation pipeline for synchronizing Snowflake data warehouse marts to SaaS destinations (Salesforce CRM and HubSpot) with live telemetry cards, schema field mapping matrix, and sync run history log.

Installation

$npx uipkge-ng@latest add reverse-etl-sync-manager
Manifest:https://uipkge.dev/r/angular/reverse-etl-sync-manager.jsonInstalls to:src/app/components/blocks/reverse-etl-sync-manager/

Variants

Loading interactive previews…

Schema

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

FieldMapping
interface FieldMapping {
  id: string
  sourceColumn: string
  sourceType: string
  sourceTable?: string
  destinationField: string
  destinationObject: string
  destinationType: string
  isMatchKey?: boolean
  syncMode: SyncMode
  transform: string
  sampleValue: string
  active: boolean
}
SyncRunRecord
interface SyncRunRecord {
  id: string
  runNumber: string
  startedAt: string
  duration: string
  durationSec: number
  status: 'succeeded' | 'running' | 'failed' | 'warning'
  statusLabel: string
  totalRecords: number
  insertedRecords: number
  updatedRecords: number
  deletedRecords: number
  failedRecords: number
  warehouse: string
  queryId: string
  batchId: string
  syncTrigger: 'Scheduled Cron' | 'Manual Trigger' | 'dbt Cloud Webhook' | 'API Dispatch'
  errorSummary?: string
}
ReverseEtlMetrics
interface ReverseEtlMetrics {
  recordsSynced: number
  successRate: number
  duration: string
  warehouseName: string
  insertions: number
  updates: number
  deletions: number
  scheduleInterval: string
  nextRunIn: string
}

Files installed (2)

  • src/app/components/blocks/reverse-etl-sync-manager/reverse-etl-sync-manager.component.ts44.1 kB
    // Census / Hightouch style Reverse ETL sync manager: Snowflake warehouse marts to SaaS
    // destinations (Salesforce CRM + HubSpot) with live telemetry cards, a schema field mapping
    // matrix, and a sync run history log. Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, DestroyRef, Input, computed, inject, signal } from '@angular/core'
    import {
      Activity,
      ArrowRight,
      ArrowRightLeft,
      CalendarClock,
      Check,
      CheckCircle2,
      ChevronRight,
      Clock,
      Copy,
      Database,
      Download,
      Filter,
      Key,
      Layers,
      LucideAngularModule,
      Pencil,
      RefreshCw,
      Search,
      Server,
      ShieldCheck,
      Timer,
    } 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 { UiInputComponent } from '@/ui/input/input.component'
    import { UiSeparatorComponent } from '@/ui/separator/separator.component'
    import {
      UiTableBodyComponent,
      UiTableCellComponent,
      UiTableComponent,
      UiTableHeadComponent,
      UiTableHeaderComponent,
      UiTableRowComponent,
    } from '@/ui/table/table.component'
    
    export type SyncMode =
      'Upsert on Match Key' | 'Update if Newer' | 'Update only (Null overwrite off)' | 'Always Overwrite'
    
    export interface FieldMapping {
      id: string
      sourceColumn: string
      sourceType: string
      sourceTable?: string
      destinationField: string
      destinationObject: string
      destinationType: string
      isMatchKey?: boolean
      syncMode: SyncMode
      transform: string
      sampleValue: string
      active: boolean
    }
    
    export interface SyncRunRecord {
      id: string
      runNumber: string
      startedAt: string
      duration: string
      durationSec: number
      status: 'succeeded' | 'running' | 'failed' | 'warning'
      statusLabel: string
      totalRecords: number
      insertedRecords: number
      updatedRecords: number
      deletedRecords: number
      failedRecords: number
      warehouse: string
      queryId: string
      batchId: string
      syncTrigger: 'Scheduled Cron' | 'Manual Trigger' | 'dbt Cloud Webhook' | 'API Dispatch'
      errorSummary?: string
    }
    
    export interface ReverseEtlMetrics {
      recordsSynced: number
      successRate: number
      duration: string
      warehouseName: string
      insertions: number
      updates: number
      deletions: number
      scheduleInterval: string
      nextRunIn: string
    }
    
    const defaultMappings: FieldMapping[] = [
      {
        id: 'map-1',
        sourceColumn: 'customer_id',
        sourceType: 'VARCHAR(64) [PK]',
        sourceTable: 'analytics.dim_high_value_customers',
        destinationField: 'External_ID__c',
        destinationObject: 'Account',
        destinationType: 'Text(64) [Unique Match Key]',
        isMatchKey: true,
        syncMode: 'Upsert on Match Key',
        transform: 'Direct Map (Primary Key)',
        sampleValue: 'CUST_9918203',
        active: true,
      },
      {
        id: 'map-2',
        sourceColumn: 'calculated_arr',
        sourceType: 'NUMBER(14,2)',
        sourceTable: 'analytics.dim_high_value_customers',
        destinationField: 'AnnualRevenue',
        destinationObject: 'Account',
        destinationType: 'Currency(16,2)',
        isMatchKey: false,
        syncMode: 'Update if Newer',
        transform: 'Currency Cast (USD)',
        sampleValue: '$14,250.00',
        active: true,
      },
      {
        id: 'map-3',
        sourceColumn: 'last_login_at',
        sourceType: 'TIMESTAMP_TZ',
        sourceTable: 'analytics.dim_high_value_customers',
        destinationField: 'Last_Active_Date__c',
        destinationObject: 'Contact',
        destinationType: 'DateTime',
        isMatchKey: false,
        syncMode: 'Update only (Null overwrite off)',
        transform: 'ISO8601 Date Parse',
        sampleValue: '2026-08-21 14:30:00',
        active: true,
      },
      {
        id: 'map-4',
        sourceColumn: 'health_score',
        sourceType: 'INTEGER',
        sourceTable: 'analytics.dim_high_value_customers',
        destinationField: 'Health_Score__c',
        destinationObject: 'Account',
        destinationType: 'Number(3,0)',
        isMatchKey: false,
        syncMode: 'Update if Newer',
        transform: 'Score Normalizer (0-100)',
        sampleValue: '94.8 / 100',
        active: true,
      },
      {
        id: 'map-5',
        sourceColumn: 'plan_tier',
        sourceType: 'VARCHAR(50)',
        sourceTable: 'analytics.dim_high_value_customers',
        destinationField: 'Plan_Tier__c',
        destinationObject: 'Account',
        destinationType: 'Picklist',
        isMatchKey: false,
        syncMode: 'Upsert on Match Key',
        transform: 'Enum Mapping (3 Tiers)',
        sampleValue: 'Tier 1 Enterprise',
        active: true,
      },
    ]
    
    const defaultRuns: SyncRunRecord[] = [
      {
        id: 'run-8821',
        runNumber: '#SYNC-8821',
        startedAt: '2026-08-21T14:00:00Z',
        duration: '01m:12s',
        durationSec: 72,
        status: 'succeeded',
        statusLabel: 'Succeeded',
        totalRecords: 14290,
        insertedRecords: 428,
        updatedRecords: 13862,
        deletedRecords: 0,
        failedRecords: 0,
        warehouse: 'SNOWFLAKE_WH_XS',
        queryId: '01b64e9a-0001-2a81-0000-00049281a8b1',
        batchId: 'sf-bulk-batch-7718902',
        syncTrigger: 'Scheduled Cron',
      },
      {
        id: 'run-8820',
        runNumber: '#SYNC-8820',
        startedAt: '2026-08-21T13:00:00Z',
        duration: '00m:58s',
        durationSec: 58,
        status: 'succeeded',
        statusLabel: 'Succeeded',
        totalRecords: 3410,
        insertedRecords: 85,
        updatedRecords: 3325,
        deletedRecords: 0,
        failedRecords: 0,
        warehouse: 'SNOWFLAKE_WH_XS',
        queryId: '01b64e9a-0001-2a80-0000-00049279b904',
        batchId: 'sf-bulk-batch-7718819',
        syncTrigger: 'Scheduled Cron',
      },
      {
        id: 'run-8819',
        runNumber: '#SYNC-8819',
        startedAt: '2026-08-21T12:00:00Z',
        duration: '01m:45s',
        durationSec: 105,
        status: 'succeeded',
        statusLabel: 'Succeeded',
        totalRecords: 12850,
        insertedRecords: 310,
        updatedRecords: 12540,
        deletedRecords: 0,
        failedRecords: 0,
        warehouse: 'SNOWFLAKE_WH_XS',
        queryId: '01b64e9a-0001-2a7f-0000-00049265f128',
        batchId: 'sf-bulk-batch-7718744',
        syncTrigger: 'dbt Cloud Webhook',
      },
      {
        id: 'run-8818',
        runNumber: '#SYNC-8818',
        startedAt: '2026-08-21T11:00:00Z',
        duration: '00m:42s',
        durationSec: 42,
        status: 'succeeded',
        statusLabel: 'Succeeded',
        totalRecords: 980,
        insertedRecords: 12,
        updatedRecords: 968,
        deletedRecords: 0,
        failedRecords: 0,
        warehouse: 'SNOWFLAKE_WH_XS',
        queryId: '01b64e9a-0001-2a7e-0000-00049251cc90',
        batchId: 'sf-bulk-batch-7718690',
        syncTrigger: 'Scheduled Cron',
      },
    ]
    
    const defaultMetrics: ReverseEtlMetrics = {
      recordsSynced: 14290,
      successRate: 100,
      duration: '01m:12s',
      warehouseName: 'Snowflake SQL Warehouse',
      insertions: 428,
      updates: 13862,
      deletions: 0,
      scheduleInterval: 'Every 1 hour',
      nextRunIn: '48m',
    }
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-reverse-etl-sync-manager, [ui-reverse-etl-sync-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,
        UiInputComponent,
        UiSeparatorComponent,
        UiTableBodyComponent,
        UiTableCellComponent,
        UiTableComponent,
        UiTableHeadComponent,
        UiTableHeaderComponent,
        UiTableRowComponent,
      ],
      template: `
        <div data-slot="reverse-etl-sync-manager" [class]="rootClass">
          <!-- Top Header: Sync Pipeline Name, Source, Destination & Global Triggers -->
          <div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
            <div class="flex items-start gap-3 sm:items-center">
              <div class="bg-card flex size-10 shrink-0 items-center justify-center rounded-lg border shadow-xs">
                <lucide-icon [img]="ArrowRightLeft" class="text-primary size-5" />
              </div>
              <div class="space-y-1">
                <div class="flex flex-wrap items-center gap-2">
                  <h2 class="text-foreground text-xl font-bold tracking-tight sm:text-2xl">
                    Snowflake Gold Marts → Salesforce Accounts & Hubspot
                  </h2>
    
                  @if (isSyncing()) {
                    <div
                      class="border-info/30 bg-info/10 text-info inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-xs font-medium"
                    >
                      <lucide-icon [img]="RefreshCw" class="size-3 animate-spin" />
                      <span>Syncing Warehouse Deltas...</span>
                    </div>
                  } @else {
                    <div
                      class="border-success/30 bg-success/10 text-success inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-xs font-medium"
                    >
                      <span class="relative flex size-2 shrink-0">
                        <span class="bg-success absolute inline-flex h-full w-full rounded-full opacity-75"></span>
                        <span class="bg-success relative inline-flex size-2 rounded-full"></span>
                      </span>
                      <span>Sync Succeeded · {{ formatNumber(metrics().recordsSynced) }} Records Updated</span>
                    </div>
                  }
                </div>
    
                <!-- Source to Destination Breadcrumb -->
                <div class="flex flex-wrap items-center gap-1.5 text-xs">
                  <span class="text-foreground inline-flex items-center gap-1 font-semibold">
                    <lucide-icon [img]="Database" class="text-info size-3.5" />
                    Snowflake:
                    <code class="text-foreground/90 font-mono font-medium">analytics.dim_high_value_customers</code>
                  </span>
                  <lucide-icon [img]="ArrowRight" class="text-muted-foreground size-3.5" />
                  <span class="text-primary inline-flex items-center gap-1 font-semibold">
                    <lucide-icon [img]="Layers" class="text-primary size-3.5" />
                    Salesforce CRM & HubSpot Marketing
                  </span>
                  <span class="text-muted-foreground hidden font-mono sm:inline">Reverse ETL · Hightouch/Census Model</span>
                </div>
              </div>
            </div>
    
            <!-- Header Action Controls -->
            <div class="flex flex-wrap items-center gap-2">
              <button ui-button variant="outline" size="sm" [class]="editMappingsClass" (click)="toggleEditMappings()">
                <lucide-icon [img]="Pencil" class="size-3.5" />
                <span>{{ isEditingMappings() ? 'Exit Mapping Mode' : 'Edit Field Mappings' }}</span>
              </button>
    
              <button
                ui-button
                aria-label="Download attachment"
                variant="outline"
                size="sm"
                class="gap-1.5"
                (click)="exportSyncManifest()"
              >
                @if (copiedManifest()) {
                  <lucide-icon [img]="Check" class="text-success size-3.5" />
                } @else {
                  <lucide-icon [img]="Download" class="size-3.5" />
                }
                <span>{{ copiedManifest() ? 'Exported' : 'Export Audit JSON' }}</span>
              </button>
    
              <button
                ui-button
                variant="default"
                size="sm"
                [disabled]="isSyncing()"
                class="gap-1.5 shadow-xs"
                (click)="triggerManualSync()"
              >
                <lucide-icon [img]="RefreshCw" class="size-3.5" [class.animate-spin]="isSyncing()" />
                <span>{{ isSyncing() ? 'Dispatching Batch...' : 'Trigger Sync Now' }}</span>
              </button>
            </div>
          </div>
    
          <!-- Active Sync Notification Alert Banner -->
          @if (syncNotice()) {
            <div
              class="text-foreground border-info/30 bg-info/10 flex items-center justify-between rounded-lg border p-3 text-xs shadow-xs"
            >
              <div class="flex items-center gap-2.5">
                <lucide-icon [img]="RefreshCw" class="text-info size-4 shrink-0" />
                <div>
                  <span class="text-info font-semibold">Manual Reverse ETL Sync Triggered:</span>
                  <span>
                    Executing incremental delta reconciliation on
                    <code class="font-mono font-medium">analytics.dim_high_value_customers</code>
                    via Snowflake Warehouse
                    <code class="font-mono font-medium">SNOWFLAKE_WH_XS</code>.
                  </span>
                </div>
              </div>
              <span ui-badge variant="outline" class="font-mono text-xs">Salesforce Bulk API 2.0</span>
            </div>
          }
    
          <!-- 4 Reverse ETL KPI Cards -->
          <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
            <!-- 1. Records Synced -->
            <div ui-card class="border-border shadow-xs">
              <div ui-card-content class="p-4">
                <div class="flex items-center justify-between">
                  <span class="text-muted-foreground text-xs font-medium">Records Synced</span>
                  <div
                    class="border-success/20 bg-success/10 text-success flex size-8 items-center justify-center rounded-md border"
                  >
                    <lucide-icon [img]="CheckCircle2" class="size-4" />
                  </div>
                </div>
                <div class="mt-3 space-y-1.5">
                  <div class="flex items-baseline gap-1.5">
                    <span class="text-foreground font-mono text-2xl font-bold tracking-tight tabular-nums">
                      {{ formatNumber(metrics().recordsSynced) }}
                    </span>
                    <span class="text-muted-foreground text-xs">Records</span>
                  </div>
                  <div class="flex items-center gap-2">
                    <span ui-badge variant="success" class="font-mono text-xs">{{ metrics().successRate }}% Success</span>
                  </div>
                  <div class="text-muted-foreground flex items-center justify-between pt-1 text-xs">
                    <span>Failed Records</span>
                    <span class="text-success font-mono font-medium">0 Errors</span>
                  </div>
                </div>
              </div>
            </div>
    
            <!-- 2. Sync Duration -->
            <div ui-card class="border-border shadow-xs">
              <div ui-card-content class="p-4">
                <div class="flex items-center justify-between">
                  <span class="text-muted-foreground text-xs font-medium">Sync Duration</span>
                  <div
                    class="border-info/20 bg-info/10 text-info flex size-8 items-center justify-center rounded-md border"
                  >
                    <lucide-icon [img]="Timer" class="size-4" />
                  </div>
                </div>
                <div class="mt-3 space-y-1.5">
                  <div class="flex items-baseline gap-1.5">
                    <span class="text-foreground font-mono text-2xl font-bold tracking-tight tabular-nums">
                      {{ metrics().duration }}
                    </span>
                    <span class="text-muted-foreground text-xs">runtime</span>
                  </div>
                  <div class="flex items-center gap-2">
                    <span ui-badge variant="info" class="font-mono text-xs">{{ metrics().warehouseName }}</span>
                  </div>
                  <div class="text-muted-foreground flex items-center justify-between pt-1 text-xs">
                    <span>Throughput</span>
                    <span class="text-foreground font-mono font-medium">198 rows/sec</span>
                  </div>
                </div>
              </div>
            </div>
    
            <!-- 3. Changes Detected -->
            <div ui-card class="border-border shadow-xs">
              <div ui-card-content class="p-4">
                <div class="flex items-center justify-between">
                  <span class="text-muted-foreground text-xs font-medium">Changes Detected</span>
                  <div
                    class="border-primary/20 bg-primary/10 text-primary flex size-8 items-center justify-center rounded-md border"
                  >
                    <lucide-icon [img]="Activity" class="size-4" />
                  </div>
                </div>
                <div class="mt-3 space-y-1.5">
                  <div class="flex items-baseline gap-1.5">
                    <span class="text-foreground font-mono text-2xl font-bold tracking-tight tabular-nums">
                      {{ formatNumber(metrics().recordsSynced) }}
                    </span>
                    <span class="text-muted-foreground text-xs">Total Deltas</span>
                  </div>
                  <div class="flex flex-wrap items-center gap-1.5 font-mono text-xs">
                    <span class="bg-success/10 text-success rounded px-1.5 py-0.5 font-medium">
                      {{ metrics().insertions }} Insertions
                    </span>
                    <span class="bg-info/10 text-info rounded px-1.5 py-0.5 font-medium">
                      {{ formatNumber(metrics().updates) }} Updates
                    </span>
                    <span class="bg-muted text-muted-foreground rounded px-1.5 py-0.5 font-medium">
                      {{ metrics().deletions }} Deletions
                    </span>
                  </div>
                  <!-- Segmented distribution progress bar -->
                  <div class="bg-muted flex h-1.5 w-full overflow-hidden rounded-full border pt-0">
                    <div class="bg-success transition-colors duration-300" [style.width]="insertionShare"></div>
                    <div class="bg-info transition-colors duration-300" [style.width]="updateShare"></div>
                  </div>
                </div>
              </div>
            </div>
    
            <!-- 4. Next Scheduled Run -->
            <div ui-card class="border-border shadow-xs">
              <div ui-card-content class="p-4">
                <div class="flex items-center justify-between">
                  <span class="text-muted-foreground text-xs font-medium">Next Scheduled Run</span>
                  <div
                    class="border-warning/20 bg-warning/10 text-warning flex size-8 items-center justify-center rounded-md border"
                  >
                    <lucide-icon [img]="CalendarClock" class="size-4" />
                  </div>
                </div>
                <div class="mt-3 space-y-1.5">
                  <div class="flex items-baseline gap-1.5">
                    <span class="text-foreground font-mono text-2xl font-bold tracking-tight tabular-nums">
                      in {{ metrics().nextRunIn }}
                    </span>
                    <span class="text-muted-foreground text-xs">remaining</span>
                  </div>
                  <div class="flex items-center gap-2">
                    <span ui-badge variant="outline" class="border-warning/30 text-warning font-mono text-xs">
                      {{ metrics().scheduleInterval }}
                    </span>
                  </div>
                  <div class="text-muted-foreground flex items-center justify-between pt-1 text-xs">
                    <span>Cron Expression</span>
                    <span class="text-foreground font-mono font-medium">0 * * * *</span>
                  </div>
                </div>
              </div>
            </div>
          </div>
    
          <!-- Field Mapping Configuration Table Card -->
          <div ui-card class="border-border shadow-xs">
            <div ui-card-header class="pb-3">
              <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
                <div>
                  <div class="flex items-center gap-2">
                    <h3 ui-card-title class="text-base font-semibold">
                      Field Mapping Configuration ({{ mappings().length }} Mappings Active)
                    </h3>
                    @if (isEditingMappings()) {
                      <span ui-badge variant="default" class="text-xs">Editing Mode Active</span>
                    }
                  </div>
                  <p ui-card-description class="text-xs">
                    Schema transformation matrix mapping Snowflake warehouse marts to Salesforce CRM object attributes and
                    match keys
                  </p>
                </div>
    
                <div class="flex items-center gap-2">
                  <span ui-badge variant="outline" class="font-mono text-xs">Primary Match: Account: External_ID__c</span>
                </div>
              </div>
            </div>
    
            <!-- Filter / Search Toolbar for Mappings -->
            <div class="border-b px-4 py-3">
              <div class="flex flex-col flex-wrap gap-3 sm:flex-row sm:items-center sm:justify-between">
                <div class="relative w-full sm:min-w-[12rem] sm:flex-1">
                  <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]="mappingSearch()"
                    (valueChange)="mappingSearch.set($event)"
                    placeholder="Search mappings..."
                    class="pl-9 text-xs"
                  />
                </div>
    
                <div class="flex flex-wrap items-center gap-1.5">
                  <span class="text-muted-foreground mr-1 flex items-center gap-1 text-xs font-medium">
                    <lucide-icon [img]="Filter" class="size-3" />
                    Mode:
                  </span>
    
                  <button type="button" [class]="modeFilterClass('all')" (click)="selectedModeFilter.set('all')">
                    All ({{ mappings().length }})
                  </button>
    
                  <button type="button" [class]="modeFilterClass('upsert')" (click)="selectedModeFilter.set('upsert')">
                    Upsert Keys (2)
                  </button>
    
                  <button
                    type="button"
                    [class]="modeFilterClass('update_if_newer')"
                    (click)="selectedModeFilter.set('update_if_newer')"
                  >
                    Update if Newer (2)
                  </button>
    
                  <button
                    type="button"
                    [class]="modeFilterClass('update_only')"
                    (click)="selectedModeFilter.set('update_only')"
                  >
                    Update Only (1)
                  </button>
                </div>
              </div>
            </div>
    
            <div ui-card-content class="p-0">
              <div class="overflow-x-auto">
                <table ui-table density="cozy">
                  <thead ui-table-header>
                    <tr ui-table-row>
                      <th ui-table-head class="text-xs font-semibold">Source Column (Snowflake)</th>
                      <th ui-table-head class="w-12 text-center text-xs font-semibold" aria-label="Mapping Direction"></th>
                      <th ui-table-head class="text-xs font-semibold">Destination Field (Salesforce CRM)</th>
                      <th ui-table-head class="text-xs font-semibold">Data Type & Sync Mode</th>
                      <th ui-table-head class="text-xs font-semibold">Sample Sync Value</th>
                      <th ui-table-head class="text-center text-xs font-semibold">Status</th>
                    </tr>
                  </thead>
                  <tbody ui-table-body>
                    @for (mapping of filteredMappings(); track mapping.id) {
                      <tr ui-table-row class="group">
                        <!-- Source Column (Snowflake) -->
                        <td ui-table-cell class="py-3">
                          <div class="space-y-1">
                            <div class="flex items-center gap-1.5">
                              <span class="text-foreground font-mono text-xs font-semibold">
                                {{ mapping.sourceColumn }}
                              </span>
                              @if (mapping.isMatchKey) {
                                <span ui-badge variant="info" class="gap-1 font-mono text-xs">
                                  <lucide-icon [img]="Key" class="size-3" />
                                  Match Key
                                </span>
                              }
                            </div>
                            <div class="text-muted-foreground flex items-center gap-1.5 text-xs">
                              <span class="font-mono text-xs">{{ mapping.sourceType }}</span>
                              <span>·</span>
                              <span class="text-muted-foreground/80 truncate font-mono text-xs">
                                {{ mapping.transform }}
                              </span>
                            </div>
                          </div>
                        </td>
    
                        <!-- Mapping Arrow (→) -->
                        <td ui-table-cell class="text-muted-foreground group-hover:text-primary w-12 py-3 text-center">
                          <div class="flex items-center justify-center">
                            <lucide-icon
                              [img]="ArrowRight"
                              class="size-4 transition-transform group-hover:translate-x-0.5"
                            />
                          </div>
                        </td>
    
                        <!-- Destination Field (Salesforce CRM) -->
                        <td ui-table-cell class="py-3">
                          <div class="space-y-1">
                            <div class="flex items-center gap-1.5">
                              <span ui-badge variant="outline" class="font-mono text-xs">
                                {{ mapping.destinationObject }}
                              </span>
                              <span class="text-foreground font-mono text-xs font-semibold">
                                {{ mapping.destinationField }}
                              </span>
                            </div>
                            <p class="text-muted-foreground font-mono text-xs">
                              Target Type: {{ mapping.destinationType }}
                            </p>
                          </div>
                        </td>
    
                        <!-- Data Type & Sync Mode -->
                        <td ui-table-cell class="py-3">
                          <div class="space-y-1">
                            <span
                              ui-badge
                              [variant]="mapping.syncMode.includes('Upsert') ? 'default' : 'outline'"
                              class="font-mono text-xs"
                            >
                              {{ mapping.syncMode }}
                            </span>
                            <p class="text-muted-foreground text-xs">
                              {{ mapping.isMatchKey ? 'Primary deduplication index' : 'Safe incremental sync' }}
                            </p>
                          </div>
                        </td>
    
                        <!-- Sample Sync Value -->
                        <td ui-table-cell class="py-3">
                          <div class="space-y-0.5">
                            <div
                              class="bg-muted/50 text-foreground inline-flex items-center gap-1.5 rounded-md border px-2 py-1 font-mono text-xs font-medium"
                            >
                              <span class="text-primary font-semibold">›</span>
                              <span>{{ mapping.sampleValue }}</span>
                            </div>
                            <p class="text-muted-foreground text-xs">Live sample preview</p>
                          </div>
                        </td>
    
                        <!-- Status Badge -->
                        <td ui-table-cell class="py-3 text-center">
                          <span ui-badge variant="success" class="gap-1 font-mono text-xs">
                            <span class="bg-success size-1.5 rounded-full"></span>
                            Active
                          </span>
                        </td>
                      </tr>
                    }
                  </tbody>
                </table>
              </div>
            </div>
          </div>
    
          <!-- Sync Run History & Error Queue Table Card -->
          <div ui-card class="border-border shadow-xs">
            <div ui-card-header class="pb-3">
              <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
                <div>
                  <h3 ui-card-title class="text-base font-semibold">
                    Sync Run History & Execution Log (Recent {{ runs().length }} Runs)
                  </h3>
                  <p ui-card-description class="text-xs">
                    Audit trail of automated and manual reverse ETL execution runs, Snowflake query IDs, and Salesforce Bulk
                    API ingestion
                  </p>
                </div>
    
                <div class="flex items-center gap-2">
                  <div
                    class="bg-success/10 text-success flex items-center gap-1.5 rounded-md border px-2.5 py-1 text-xs font-medium"
                  >
                    <lucide-icon [img]="ShieldCheck" class="size-3.5" />
                    <span>Error DLQ: 0 Failed Records</span>
                  </div>
                </div>
              </div>
            </div>
    
            <div ui-card-content class="p-0">
              <div class="divide-border/60 divide-y">
                @for (run of runs(); track run.id) {
                  <div [class]="runRowClass(run.id)">
                    <!-- Run Summary Header Row -->
                    <div
                      role="button"
                      tabindex="0"
                      [attr.aria-expanded]="!!expandedRunIds()[run.id]"
                      class="focus-visible:ring-ring flex cursor-pointer flex-col gap-3 p-3.5 focus-visible:ring-2 focus-visible:outline-none focus-visible:ring-inset sm:flex-row sm:items-center sm:justify-between"
                      (click)="toggleRunExpand(run.id)"
                      (keydown.enter)="toggleRunExpand(run.id)"
                      (keydown.space)="$event.preventDefault(); toggleRunExpand(run.id)"
                    >
                      <!-- Left Rail: Chevron, Run ID, Status, Timestamp & Trigger -->
                      <div class="flex min-w-0 items-start gap-3 sm:items-center">
                        <button
                          type="button"
                          aria-label="Toggle sync run details"
                          class="text-muted-foreground group-hover:text-foreground mt-0.5 shrink-0 transition-transform sm:mt-0"
                          [class.text-foreground]="!!expandedRunIds()[run.id]"
                          [class.rotate-90]="!!expandedRunIds()[run.id]"
                        >
                          <lucide-icon [img]="ChevronRight" class="size-4" />
                        </button>
    
                        <div class="space-y-1 sm:space-y-0.5">
                          <div class="flex flex-wrap items-center gap-2">
                            <span class="text-foreground font-mono text-xs font-bold">
                              {{ run.runNumber }}
                            </span>
    
                            <span ui-badge variant="success" class="gap-1 font-mono text-xs">
                              <span class="bg-success size-1.5 rounded-full"></span>
                              {{ run.statusLabel }}
                            </span>
    
                            <span class="text-muted-foreground font-mono text-xs whitespace-nowrap">
                              {{ formatTimestamp(run.startedAt) }}
                            </span>
    
                            <span ui-badge variant="outline" class="font-mono text-xs">
                              {{ run.syncTrigger }}
                            </span>
                          </div>
    
                          <p class="text-muted-foreground text-xs">
                            Processed {{ formatNumber(run.totalRecords) }} records ({{
                              formatNumber(run.insertedRecords)
                            }}
                            added, {{ formatNumber(run.updatedRecords) }} updated) · 0 failed
                          </p>
                        </div>
                      </div>
    
                      <!-- Right Rail: Duration, Rows Affected, Warehouse -->
                      <div class="flex shrink-0 flex-wrap items-center gap-3 sm:justify-end">
                        <div class="bg-muted/60 flex items-center gap-1.5 rounded-md border px-2 py-1 font-mono text-xs">
                          <lucide-icon [img]="Clock" class="text-muted-foreground size-3" />
                          <span class="text-muted-foreground">Duration:</span>
                          <span class="text-foreground font-medium tabular-nums">{{ run.duration }}</span>
                        </div>
    
                        <div class="text-muted-foreground flex items-center gap-1.5 font-mono text-xs">
                          <span class="text-foreground/90 font-medium">{{ run.warehouse }}</span>
                        </div>
                      </div>
                    </div>
    
                    <!-- Expanded Run Diagnostics Details -->
                    @if (expandedRunIds()[run.id]) {
                      <div class="bg-muted/15 border-t px-4 py-3.5">
                        <div class="space-y-3">
                          <div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
                            <!-- Snowflake Query Identifier -->
                            <div class="bg-card space-y-1 rounded-md border p-3">
                              <div class="flex items-center justify-between">
                                <span class="text-muted-foreground text-xs font-medium">Snowflake Query ID</span>
                                <button
                                  ui-button
                                  variant="ghost"
                                  size="xs"
                                  class="h-6 gap-1 px-2 text-xs"
                                  (click)="copyQueryId(run.id, run.queryId)"
                                >
                                  @if (copiedQueryId() === run.id) {
                                    <lucide-icon [img]="Check" class="text-success size-3" />
                                  } @else {
                                    <lucide-icon [img]="Copy" class="size-3" />
                                  }
                                  <span>{{ copiedQueryId() === run.id ? 'Copied' : 'Copy' }}</span>
                                </button>
                              </div>
                              <p class="text-foreground font-mono text-xs font-semibold">
                                {{ run.queryId }}
                              </p>
                              <p class="text-muted-foreground text-xs">SQL compilation: 180ms · Warehousing tier: XS</p>
                            </div>
    
                            <!-- Salesforce Bulk API Batch ID -->
                            <div class="bg-card space-y-1 rounded-md border p-3">
                              <div class="flex items-center justify-between">
                                <span class="text-muted-foreground text-xs font-medium">Salesforce Bulk Batch ID</span>
                                <span ui-badge variant="outline" class="font-mono text-xs">Bulk 2.0</span>
                              </div>
                              <p class="text-foreground font-mono text-xs font-semibold">
                                {{ run.batchId }}
                              </p>
                              <p class="text-muted-foreground text-xs">Chunk size: 10,000 records/batch · HTTP 200 OK</p>
                            </div>
    
                            <!-- Ingestion Breakdown -->
                            <div class="bg-card space-y-1 rounded-md border p-3 sm:col-span-2 lg:col-span-1">
                              <div class="flex items-center justify-between">
                                <span class="text-muted-foreground text-xs font-medium">Ingestion Breakdown</span>
                                <span class="text-success font-mono text-xs">100% Ingested</span>
                              </div>
                              <div class="flex items-center gap-2 pt-1 font-mono text-xs">
                                <span class="text-success font-medium">+{{ formatNumber(run.insertedRecords) }} Ins</span>
                                <span class="text-info font-medium">~{{ formatNumber(run.updatedRecords) }} Upd</span>
                                <span class="text-muted-foreground">{{ run.deletedRecords }} Del</span>
                              </div>
                              <p class="text-muted-foreground text-xs">Zero dead-letter quarantine items</p>
                            </div>
                          </div>
                        </div>
                      </div>
                    }
                  </div>
                }
              </div>
            </div>
    
            <!-- Bottom Engine & Sync Ingestion Protocol Telemetry Bar -->
            <div
              class="bg-muted/40 flex flex-col gap-2.5 border-t px-4 py-3 text-xs sm:flex-row sm:items-center sm:justify-between"
            >
              <div class="text-muted-foreground flex flex-wrap items-center gap-4">
                <div class="text-foreground flex items-center gap-1.5 font-medium">
                  <lucide-icon [img]="Activity" class="text-success size-3.5" />
                  <span>Reverse ETL Engine:</span>
                  <span class="font-mono">Census / Hightouch Protocol v4.2</span>
                </div>
    
                <ui-separator orientation="vertical" class="hidden h-3.5 sm:block" />
    
                <div class="flex items-center gap-1.5">
                  <lucide-icon [img]="Database" class="text-info size-3.5" />
                  <span>Source DW:</span>
                  <span class="text-foreground font-mono">Snowflake (AWS us-east-1)</span>
                </div>
    
                <ui-separator orientation="vertical" class="hidden h-3.5 sm:block" />
    
                <div class="flex items-center gap-1.5">
                  <lucide-icon [img]="Server" class="text-primary size-3.5" />
                  <span>Target SaaS:</span>
                  <span class="text-foreground font-mono">Salesforce Bulk API 2.0 + HubSpot v3</span>
                </div>
              </div>
    
              <div class="text-muted-foreground flex items-center gap-2 font-mono">
                <span class="bg-success size-1.5 rounded-full"></span>
                <span>Daily API Quota: 94,200 / 100,000 remaining</span>
              </div>
            </div>
          </div>
        </div>
      `,
    })
    export class UiReverseEtlSyncManagerComponent {
      protected readonly Activity = Activity
      protected readonly ArrowRight = ArrowRight
      protected readonly ArrowRightLeft = ArrowRightLeft
      protected readonly CalendarClock = CalendarClock
      protected readonly Check = Check
      protected readonly CheckCircle2 = CheckCircle2
      protected readonly ChevronRight = ChevronRight
      protected readonly Clock = Clock
      protected readonly Copy = Copy
      protected readonly Database = Database
      protected readonly Download = Download
      protected readonly Filter = Filter
      protected readonly Key = Key
      protected readonly Layers = Layers
      protected readonly Pencil = Pencil
      protected readonly RefreshCw = RefreshCw
      protected readonly Search = Search
      protected readonly Server = Server
      protected readonly ShieldCheck = ShieldCheck
      protected readonly Timer = Timer
    
      @Input() set initialMappings(v: FieldMapping[] | undefined) {
        this.mappings.set(v ? [...v] : [...defaultMappings])
      }
      @Input() set initialRuns(v: SyncRunRecord[] | undefined) {
        this.runs.set(v ? [...v] : [...defaultRuns])
      }
      @Input() set initialMetrics(v: ReverseEtlMetrics | undefined) {
        this.metrics.set(v ? { ...v } : { ...defaultMetrics })
      }
      @Input('class') className?: string
    
      readonly mappings = signal<FieldMapping[]>([...defaultMappings])
      readonly runs = signal<SyncRunRecord[]>([...defaultRuns])
      readonly metrics = signal<ReverseEtlMetrics>({ ...defaultMetrics })
    
      readonly isSyncing = signal(false)
      readonly syncNotice = signal(false)
      readonly isEditingMappings = signal(false)
      readonly mappingSearch = signal('')
      readonly selectedModeFilter = signal('all')
      readonly expandedRunIds = signal<Record<string, boolean>>({ 'run-8821': true })
      readonly copiedQueryId = signal<string | null>(null)
      readonly copiedManifest = signal(false)
    
      private copyTimer: ReturnType<typeof setTimeout> | undefined
      private manifestTimer: ReturnType<typeof setTimeout> | undefined
      private syncTimer: ReturnType<typeof setTimeout> | undefined
      private noticeTimer: ReturnType<typeof setTimeout> | undefined
    
      constructor() {
        inject(DestroyRef).onDestroy(() => {
          clearTimeout(this.copyTimer)
          clearTimeout(this.manifestTimer)
          clearTimeout(this.syncTimer)
          clearTimeout(this.noticeTimer)
        })
      }
    
      readonly filteredMappings = computed(() => {
        const q = this.mappingSearch().toLowerCase()
        const mode = this.selectedModeFilter()
        return this.mappings().filter((m) => {
          const matchesSearch =
            q.trim() === '' ||
            m.sourceColumn.toLowerCase().includes(q) ||
            m.destinationField.toLowerCase().includes(q) ||
            m.destinationObject.toLowerCase().includes(q) ||
            m.syncMode.toLowerCase().includes(q) ||
            m.sampleValue.toLowerCase().includes(q)
          const matchesMode =
            mode === 'all' ||
            (mode === 'upsert' && m.syncMode.includes('Upsert')) ||
            (mode === 'update_if_newer' && m.syncMode === 'Update if Newer') ||
            (mode === 'update_only' && m.syncMode.includes('Update only'))
          return matchesSearch && matchesMode
        })
      })
    
      get rootClass(): string {
        return cn('w-full space-y-4', this.className)
      }
    
      get editMappingsClass(): string {
        return cn('gap-1.5', this.isEditingMappings() && 'border-primary bg-primary/10 text-primary')
      }
    
      get insertionShare(): string {
        const m = this.metrics()
        return `${(m.insertions / m.recordsSynced) * 100}%`
      }
    
      get updateShare(): string {
        const m = this.metrics()
        return `${(m.updates / m.recordsSynced) * 100}%`
      }
    
      modeFilterClass(mode: string): string {
        return cn(
          'focus-visible:ring-ring inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none',
          this.selectedModeFilter() === mode
            ? 'border-primary bg-primary text-primary-foreground shadow-xs'
            : 'border-border bg-card text-muted-foreground hover:bg-accent hover:text-foreground',
        )
      }
    
      runRowClass(id: string): string {
        return cn('group transition-colors', this.expandedRunIds()[id] ? 'bg-muted/30' : 'hover:bg-muted/20')
      }
    
      triggerManualSync(): void {
        if (this.isSyncing()) return
        this.isSyncing.set(true)
        this.syncNotice.set(true)
    
        this.syncTimer = setTimeout(() => {
          this.isSyncing.set(false)
          const newRunId = `run-${Date.now()}`
          this.runs.update((prev) => [
            {
              id: newRunId,
              runNumber: `#SYNC-${8822 + (prev.length - 4)}`,
              startedAt: new Date().toISOString(),
              duration: '01m:08s',
              durationSec: 68,
              status: 'succeeded',
              statusLabel: 'Succeeded',
              totalRecords: 14290,
              insertedRecords: 428,
              updatedRecords: 13862,
              deletedRecords: 0,
              failedRecords: 0,
              warehouse: 'SNOWFLAKE_WH_XS',
              queryId: `01b64e9a-0001-${Math.floor(1000 + Math.random() * 9000)}-0000-${Math.floor(100000000000 + Math.random() * 900000000000).toString(16)}`,
              batchId: `sf-bulk-batch-${Math.floor(7718900 + Math.random() * 1000)}`,
              syncTrigger: 'Manual Trigger',
            },
            ...prev,
          ])
          this.expandedRunIds.update((prev) => ({ ...prev, [newRunId]: true }))
        }, 2200)
    
        this.noticeTimer = setTimeout(() => {
          this.syncNotice.set(false)
        }, 6500)
      }
    
      toggleEditMappings(): void {
        this.isEditingMappings.update((v) => !v)
      }
    
      toggleRunExpand(id: string): void {
        this.expandedRunIds.update((prev) => ({ ...prev, [id]: !prev[id] }))
      }
    
      copyQueryId(id: string, text: string): void {
        if (typeof navigator !== 'undefined' && navigator.clipboard) {
          navigator.clipboard.writeText(text)
          this.copiedQueryId.set(id)
          clearTimeout(this.copyTimer)
          this.copyTimer = setTimeout(() => {
            if (this.copiedQueryId() === id) this.copiedQueryId.set(null)
          }, 2000)
        }
      }
    
      exportSyncManifest(): void {
        const manifestData = {
          syncName: 'Snowflake Gold Marts → Salesforce Accounts & Hubspot',
          source: 'Snowflake: analytics.dim_high_value_customers',
          destination: 'Salesforce CRM & HubSpot Marketing',
          exportTimestamp: new Date().toISOString(),
          metrics: this.metrics(),
          fieldMappings: this.mappings(),
          recentRuns: this.runs(),
        }
    
        if (typeof document !== 'undefined') {
          const dataStr = 'data:text/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(manifestData, null, 2))
          const downloadAnchor = document.createElement('a')
          downloadAnchor.setAttribute('href', dataStr)
          downloadAnchor.setAttribute(
            'download',
            `reverse-etl-sync-manifest-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.json`,
          )
          document.body.appendChild(downloadAnchor)
          downloadAnchor.click()
          downloadAnchor.remove()
          this.copiedManifest.set(true)
          clearTimeout(this.manifestTimer)
          this.manifestTimer = setTimeout(() => {
            this.copiedManifest.set(false)
          }, 2000)
        }
      }
    
      formatNumber(num: number): string {
        return new Intl.NumberFormat('en-US').format(num)
      }
    
      formatTimestamp(iso: string): string {
        try {
          const d = new Date(iso)
          return d.toISOString().replace('T', ' ').replace('.000Z', ' UTC').replace('Z', ' UTC')
        } catch {
          return iso
        }
      }
    }
    
  • src/app/components/blocks/reverse-etl-sync-manager/index.ts0.2 kB

Raw manifest:https://uipkge.dev/r/angular/reverse-etl-sync-manager.json