Menu

Change language

GitHub repo

Data Catalog Lineage

blockdevops

Alation and Monte Carlo style enterprise data catalog lineage map and table documentation explorer with interactive upstream source DAGs, dbt transform models, downstream BI consumer traces, schema column definitions with types and constraints, automated data quality assertions, and SQL console query runner.

Installation

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

Variants

Loading interactive previews…

Schema

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

ColumnDef
interface ColumnDef {
  name: string
  type: string
  nullable: boolean
  isPrimaryKey?: boolean
  description: string
  sampleValue: string
  distinctPct: string
  constraints?: string
}
QualityAssertion
interface QualityAssertion {
  id: string
  name: string
  type: string
  targetColumn: string
  description: string
  executionTime: string
  lastRun: string
  status: 'passed' | 'warning' | 'failed'
  ruleDefinition: string
}
LineageSourceNode
interface LineageSourceNode {
  id: string
  name: string
  schema: string
  connector: string
  type: string
  rowCount: string
  latency: string
  status: 'fresh' | 'syncing' | 'delayed'
  iconName: 'CreditCard' | 'Users' | 'Landmark'
  description: string
}
LineageTransformNode
interface LineageTransformNode {
  id: string
  name: string
  modelType: string
  engine: string
  runtime: string
  lastRun: string
  testsPassed: string
  status: 'passed' | 'running' | 'failed'
  iconName: 'Layers' | 'Coins'
  description: string
}
LineageConsumerNode
interface LineageConsumerNode {
  id: string
  name: string
  category: string
  tool: string
  frequency: string
  audience: string
  status: 'active' | 'syncing'
  iconName: 'BarChart3' | 'ArrowRightLeft' | 'LineChart'
  description: string
}

Files installed (2)

  • src/app/components/blocks/data-catalog-lineage/data-catalog-lineage.component.ts55.8 kB
    // Alation and Monte Carlo style enterprise data catalog lineage map and table documentation
    // explorer with interactive upstream source DAGs, dbt transform models, downstream BI consumer
    // traces, schema column definitions with types and constraints, automated data quality
    // assertions, and SQL console query runner. Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core'
    import {
      Activity,
      ArrowRightLeft,
      BarChart3,
      Check,
      CheckCircle2,
      Clock,
      Coins,
      Copy,
      CreditCard,
      Database,
      FileCode2,
      GitFork,
      Key,
      Landmark,
      Layers,
      LineChart,
      LucideAngularModule,
      RefreshCw,
      Search,
      ShieldCheck,
      Table2,
      Terminal,
      Users,
      Workflow,
      X,
    } 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 {
      UiTableBodyComponent,
      UiTableCellComponent,
      UiTableComponent,
      UiTableHeadComponent,
      UiTableHeaderComponent,
      UiTableRowComponent,
    } from '@/ui/table/table.component'
    import {
      UiTabsComponent,
      UiTabsContentComponent,
      UiTabsListComponent,
      UiTabsTriggerComponent,
    } from '@/ui/tabs/tabs.component'
    
    export interface ColumnDef {
      name: string
      type: string
      nullable: boolean
      isPrimaryKey?: boolean
      description: string
      sampleValue: string
      distinctPct: string
      constraints?: string
    }
    
    export interface QualityAssertion {
      id: string
      name: string
      type: string
      targetColumn: string
      description: string
      executionTime: string
      lastRun: string
      status: 'passed' | 'warning' | 'failed'
      ruleDefinition: string
    }
    
    export interface LineageSourceNode {
      id: string
      name: string
      schema: string
      connector: string
      type: string
      rowCount: string
      latency: string
      status: 'fresh' | 'syncing' | 'delayed'
      iconName: 'CreditCard' | 'Users' | 'Landmark'
      description: string
    }
    
    export interface LineageTransformNode {
      id: string
      name: string
      modelType: string
      engine: string
      runtime: string
      lastRun: string
      testsPassed: string
      status: 'passed' | 'running' | 'failed'
      iconName: 'Layers' | 'Coins'
      description: string
    }
    
    export interface LineageConsumerNode {
      id: string
      name: string
      category: string
      tool: string
      frequency: string
      audience: string
      status: 'active' | 'syncing'
      iconName: 'BarChart3' | 'ArrowRightLeft' | 'LineChart'
      description: string
    }
    
    const sourceNodes: LineageSourceNode[] = [
      {
        id: 'src-stripe',
        name: 'raw_stripe.charges',
        schema: 'raw_stripe',
        connector: 'Fivetran CDC',
        type: 'PostgreSQL Raw',
        rowCount: '1.42M rows',
        latency: '~5m sync',
        status: 'fresh',
        iconName: 'CreditCard',
        description: 'Direct transactional charge events, currency conversions, refunds, and card processing fees.',
      },
      {
        id: 'src-salesforce',
        name: 'raw_salesforce.accounts',
        schema: 'raw_salesforce',
        connector: 'Airbyte Cloud',
        type: 'CRM Extract',
        rowCount: '84.5k rows',
        latency: '~15m sync',
        status: 'fresh',
        iconName: 'Users',
        description: 'Enterprise accounts, contract values, billing contacts, and subscription tier assignments.',
      },
      {
        id: 'src-bank',
        name: 'raw_bank.transfers',
        schema: 'raw_bank',
        connector: 'Kafka Stream',
        type: 'Banking API',
        rowCount: '620k rows',
        latency: '<1m latency',
        status: 'fresh',
        iconName: 'Landmark',
        description: 'Direct bank wire settlement entries, ACH clearances, and treasury reconciliation logs.',
      },
    ]
    
    const transformNodes: LineageTransformNode[] = [
      {
        id: 'tr-daily-revenue',
        name: 'dbt.int_daily_revenue',
        modelType: 'dbt Incremental',
        engine: 'dbt Core 1.8',
        runtime: '1m 24s',
        lastRun: '22m ago',
        testsPassed: '14/14 Passed',
        status: 'passed',
        iconName: 'Layers',
        description: 'Aggregates daily charge events into standardized GAAP revenue recognized per customer.',
      },
      {
        id: 'tr-fx-rates',
        name: 'dbt.int_fx_normalized_rates',
        modelType: 'dbt View',
        engine: 'dbt Core 1.8',
        runtime: '18s',
        lastRun: '25m ago',
        testsPassed: '6/6 Passed',
        status: 'passed',
        iconName: 'Coins',
        description: 'Normalized exchange rate matrix cross-referencing European Central Bank & Federal Reserve feeds.',
      },
    ]
    
    const consumerNodes: LineageConsumerNode[] = [
      {
        id: 'cons-metabase',
        name: 'Executive Board Metabase Dashboard',
        category: 'BI Dashboard',
        tool: 'Metabase Enterprise',
        frequency: 'Hourly auto-refresh',
        audience: '42 active executives',
        status: 'active',
        iconName: 'BarChart3',
        description: 'C-Suite executive dashboard tracking recurring revenue, net burn rate, and board deck slide metrics.',
      },
      {
        id: 'cons-netsuite',
        name: 'Finance NetSuite Sync',
        category: 'Reverse ETL',
        tool: 'Census Reverse ETL',
        frequency: 'Hourly batch',
        audience: 'Accounting ERP Ledger',
        status: 'active',
        iconName: 'ArrowRightLeft',
        description: 'Automated journal entry creation and revenue recognition schedules into NetSuite ERP.',
      },
      {
        id: 'cons-looker',
        name: 'Looker ARR Report',
        category: 'Looker Model',
        tool: 'Looker Explore',
        frequency: 'Live semantic model',
        audience: '18 connected reports',
        status: 'active',
        iconName: 'LineChart',
        description: 'Self-service exploration model for RevOps, cohort retention, contraction, and expansion ARR.',
      },
    ]
    
    const columns: ColumnDef[] = [
      {
        name: 'month_id',
        type: 'varchar(7)',
        nullable: false,
        isPrimaryKey: true,
        description: 'YYYY-MM billing calendar interval identifier (e.g. 2026-03) representing fiscal calendar close.',
        sampleValue: "'2026-03'",
        distinctPct: '100% Unique',
        constraints: 'PRIMARY KEY, ISO-8601',
      },
      {
        name: 'gross_revenue',
        type: 'decimal(18,2)',
        nullable: false,
        description:
          'Total unadjusted transaction volume processed across all payment gateways before churn and discounts.',
        sampleValue: '$1,842,500.00',
        distinctPct: '99.8% Distinct',
        constraints: 'CHECK (gross_revenue >= 0)',
      },
      {
        name: 'net_arr',
        type: 'decimal(18,2)',
        nullable: false,
        description:
          'Annualized recurring revenue normalized across all active multi-year and monthly enterprise contracts.',
        sampleValue: '$14,680,240.00',
        distinctPct: '98.9% Distinct',
        constraints: 'CHECK (net_arr >= 0)',
      },
      {
        name: 'churned_mrr',
        type: 'decimal(18,2)',
        nullable: true,
        description:
          'Monthly recurring revenue lost in the period due to voluntary cancellations or involuntary card downgrades.',
        sampleValue: '-$12,450.00',
        distinctPct: '42.1% Distinct',
        constraints: 'CHECK (churned_mrr <= 0)',
      },
      {
        name: 'active_subscriptions',
        type: 'integer',
        nullable: false,
        description: 'Total count of distinct active paying enterprise accounts and self-serve team workspaces.',
        sampleValue: '3,842',
        distinctPct: '100% Integer',
        constraints: 'CHECK (active_subscriptions > 0)',
      },
    ]
    
    const qualityAssertions: QualityAssertion[] = [
      {
        id: 'test-1',
        name: 'assert_unique_month_id',
        type: 'Primary Key Uniqueness',
        targetColumn: 'month_id',
        description: 'Verifies that every monthly fiscal partition key is strictly unique with 0 duplicate rows.',
        executionTime: '420ms',
        lastRun: '18m ago',
        status: 'passed',
        ruleDefinition: 'dbt_expectations.expect_compound_columns_to_be_unique(month_id)',
      },
      {
        id: 'test-2',
        name: 'assert_positive_gross_revenue',
        type: 'Value Range Check',
        targetColumn: 'gross_revenue',
        description: 'Ensures that processed monthly gross revenue cannot be negative under GAAP rules.',
        executionTime: '380ms',
        lastRun: '18m ago',
        status: 'passed',
        ruleDefinition: 'dbt_expectations.expect_column_values_to_be_between(min_value=0)',
      },
      {
        id: 'test-3',
        name: 'assert_foreign_key_accounts_integrity',
        type: 'Referential Integrity',
        targetColumn: 'account_id (upstream join)',
        description: 'Validates that 100% of underlying subscription revenue lines match valid accounts in Salesforce CRM.',
        executionTime: '890ms',
        lastRun: '18m ago',
        status: 'passed',
        ruleDefinition: "dbt_utils.relationships(to=ref('raw_salesforce_accounts'), field='id')",
      },
      {
        id: 'test-4',
        name: 'assert_not_null_critical_metrics',
        type: 'Completeness / Non-Null',
        targetColumn: 'month_id, gross_revenue, net_arr',
        description: 'Guarantees zero null occurrences across high-priority C-Suite revenue and ARR metrics.',
        executionTime: '310ms',
        lastRun: '18m ago',
        status: 'passed',
        ruleDefinition: 'dbt.test_not_null_composite(columns=[month_id, gross_revenue, net_arr])',
      },
      {
        id: 'test-5',
        name: 'assert_valid_iso_month_format',
        type: 'Regex Format Validation',
        targetColumn: 'month_id',
        description: 'Asserts all month partition tokens adhere to standard ISO format regex `^\\d{4}-(0[1-9]|1[0-2])$`.',
        executionTime: '240ms',
        lastRun: '18m ago',
        status: 'passed',
        ruleDefinition: 'dbt_expectations.expect_column_values_to_match_regex(pattern="^\\\\d{4}-(0[1-9]|1[0-2])$")',
      },
      {
        id: 'test-6',
        name: 'assert_monotonic_active_subscriptions',
        type: 'Anomaly Detection (3-Sigma)',
        targetColumn: 'active_subscriptions',
        description:
          'Monte Carlo ML rule checking month-over-month subscription volatility is within 3-sigma confidence bounds.',
        executionTime: '610ms',
        lastRun: '18m ago',
        status: 'passed',
        ruleDefinition: 'monte_carlo.metric_anomaly_detector(threshold_sigma=3.0, sensitivity=0.95)',
      },
    ]
    
    const dbtModelSnippet = `{{ config(
        materialized='incremental',
        unique_key='month_id',
        schema='production_dw',
        tags=['finance', 'tier_1_gold', 'executive_kpi']
    ) }}
    
    WITH daily_rev AS (
        SELECT * FROM {{ ref('int_daily_revenue') }}
        {% if is_incremental() %}
        WHERE event_date >= dateadd(day, -3, current_date)
        {% endif %}
    ),
    
    fx_rates AS (
        SELECT * FROM {{ ref('int_fx_normalized_rates') }}
    ),
    
    monthly_aggregates AS (
        SELECT
            TO_CHAR(d.event_date, 'YYYY-MM') AS month_id,
            SUM(d.gross_amount_usd) AS gross_revenue,
            SUM(d.annualized_contract_val) AS net_arr,
            SUM(CASE WHEN d.is_churned THEN d.mrr_impact ELSE 0 END) AS churned_mrr,
            COUNT(DISTINCT d.customer_account_id) AS active_subscriptions
        FROM daily_rev d
        LEFT JOIN fx_rates fx ON d.currency = fx.currency AND d.event_date = fx.rate_date
        GROUP BY 1
    )
    
    SELECT
        month_id,
        ROUND(gross_revenue, 2) AS gross_revenue,
        ROUND(net_arr, 2) AS net_arr,
        ROUND(churned_mrr, 2) AS churned_mrr,
        active_subscriptions
    FROM monthly_aggregates
    ORDER BY month_id DESC;`
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-data-catalog-lineage, [ui-data-catalog-lineage]',
      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,
        UiTableBodyComponent,
        UiTableCellComponent,
        UiTableComponent,
        UiTableHeadComponent,
        UiTableHeaderComponent,
        UiTableRowComponent,
        UiTabsComponent,
        UiTabsContentComponent,
        UiTabsListComponent,
        UiTabsTriggerComponent,
      ],
      template: `
        <div data-slot="data-catalog-lineage" [class]="rootClass">
          <!-- Header Section -->
          <header class="border-border bg-card/60 rounded-xl border p-4 sm:p-5">
            <div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
              <div class="space-y-2.5">
                <div class="flex flex-wrap items-center gap-2 text-xs">
                  <div class="bg-primary/10 text-primary flex size-6 items-center justify-center rounded-md">
                    <lucide-icon [img]="Database" class="size-3.5" />
                  </div>
                  <span class="text-muted-foreground font-mono">snowflake-dw-production.us-east-1</span>
                  <span class="text-muted-foreground">/</span>
                  <span ui-badge variant="outline" class="font-mono text-xs font-normal"> schema: production_dw </span>
                  <span ui-badge variant="secondary" class="font-mono text-xs"> Tier 1 Core Mart </span>
                </div>
    
                <div class="flex flex-wrap items-center gap-3">
                  <h1 class="text-foreground font-mono text-xl font-bold tracking-tight break-all sm:text-2xl">
                    analytics.fct_monthly_financials
                  </h1>
                  <button
                    ui-button
                    variant="ghost"
                    size="sm"
                    class="h-7 gap-1 px-2 text-xs"
                    (click)="copyRef('analytics.fct_monthly_financials')"
                  >
                    @if (copiedRef()) {
                      <lucide-icon [img]="Check" class="text-success size-3" />
                    } @else {
                      <lucide-icon [img]="Copy" class="size-3" />
                    }
                    <span class="font-mono">{{ copiedRef() ? 'Copied Ref!' : 'Copy Name' }}</span>
                  </button>
                </div>
    
                <div class="flex flex-wrap items-center gap-2 pt-0.5 text-xs">
                  <div class="border-border bg-background inline-flex items-center gap-2 rounded-md border px-2.5 py-1">
                    <span class="relative flex size-2">
                      <span class="bg-success absolute inline-flex size-full rounded-full opacity-75"></span>
                      <span class="bg-success relative inline-flex size-2 rounded-full"></span>
                    </span>
                    <span class="text-muted-foreground">Freshness:</span>
                    <span class="text-foreground font-medium">Updated 18m ago</span>
                    <span class="text-success font-medium">SLA: Healthy</span>
                  </div>
    
                  <div class="border-border bg-background inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1">
                    <lucide-icon [img]="Users" class="text-muted-foreground size-3.5" />
                    <span class="text-muted-foreground">Owner:</span>
                    <span class="text-foreground font-medium">Data Platform Squad</span>
                    <span ui-badge variant="secondary" class="h-4.5 px-1 font-mono text-xs">#data-eng-core</span>
                  </div>
    
                  <div class="border-border bg-background inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1">
                    <lucide-icon [img]="Table2" class="text-muted-foreground size-3.5" />
                    <span class="text-muted-foreground">Rows:</span>
                    <span class="text-foreground font-mono font-medium">240,000</span>
                    <span class="text-foreground font-mono font-medium">5 columns</span>
                  </div>
                </div>
              </div>
    
              <div class="flex flex-wrap items-center gap-2.5 pt-1 lg:pt-0">
                <button ui-button variant="outline" size="sm" class="h-9 gap-1.5 text-xs" (click)="resetLineageFocus()">
                  <lucide-icon [img]="GitFork" class="text-info size-3.5" />
                  <span>View Upstream Lineage</span>
                </button>
    
                <button
                  ui-button
                  aria-label="Close query modal"
                  variant="default"
                  size="sm"
                  class="h-9 gap-1.5 text-xs font-medium"
                  (click)="isQueryModalOpen.set(!isQueryModalOpen())"
                >
                  <lucide-icon [img]="Terminal" class="size-3.5" />
                  <span>Query Table</span>
                </button>
              </div>
            </div>
          </header>
    
          <!-- Interactive SQL Query Drawer / Bar -->
          @if (isQueryModalOpen()) {
            <div class="border-border bg-muted/30 overflow-hidden rounded-xl border transition-colors">
              <div class="border-border bg-card flex items-center justify-between border-b px-4 py-2.5">
                <div class="flex items-center gap-2">
                  <lucide-icon [img]="Terminal" class="text-primary size-4" />
                  <span class="text-foreground font-mono text-xs font-semibold"
                    >Snowflake SQL Console · Interactive Query Runner</span
                  >
                  <span ui-badge variant="outline" class="font-mono text-xs">Snowflake XS Warehouse</span>
                </div>
                <div class="flex items-center gap-2">
                  <button ui-button variant="ghost" size="sm" class="h-7 text-xs" (click)="copyCompiledSQL()">
                    @if (copiedQuery()) {
                      <lucide-icon [img]="Check" class="text-success size-3" />
                    } @else {
                      <lucide-icon [img]="Copy" class="size-3" />
                    }
                    <span>{{ copiedQuery() ? 'Copied' : 'Copy SQL' }}</span>
                  </button>
                  <button
                    ui-button
                    variant="ghost"
                    size="sm"
                    class="size-7 p-0"
                    aria-label="Close query modal"
                    (click)="isQueryModalOpen.set(false)"
                  >
                    <lucide-icon [img]="X" class="size-4" />
                  </button>
                </div>
              </div>
              <div class="space-y-3 p-4">
                <div
                  class="overflow-x-auto rounded-lg bg-neutral-950 p-3 font-mono text-xs text-neutral-100 dark:bg-neutral-950"
                >
                  <pre class="leading-relaxed whitespace-pre"><code>SELECT
      month_id,
      gross_revenue,
      net_arr,
      churned_mrr,
      active_subscriptions
    FROM production_dw.analytics.fct_monthly_financials
    ORDER BY month_id DESC
    LIMIT 5;</code></pre>
                </div>
                <div class="border-border bg-card overflow-hidden rounded-lg border">
                  <div class="overflow-x-auto">
                    <table ui-table>
                      <thead ui-table-header>
                        <tr ui-table-row class="bg-muted/50 hover:bg-muted/50">
                          <th ui-table-head class="font-mono text-xs">month_id (varchar)</th>
                          <th ui-table-head class="text-right font-mono text-xs">gross_revenue (decimal)</th>
                          <th ui-table-head class="text-right font-mono text-xs">net_arr (decimal)</th>
                          <th ui-table-head class="text-right font-mono text-xs">churned_mrr (decimal)</th>
                          <th ui-table-head class="text-right font-mono text-xs">active_subscriptions (int)</th>
                        </tr>
                      </thead>
                      <tbody ui-table-body>
                        <tr ui-table-row class="font-mono text-xs">
                          <td ui-table-cell class="text-warning font-semibold">2026-03</td>
                          <td ui-table-cell class="text-right">$1,842,500.00</td>
                          <td ui-table-cell class="text-success text-right font-medium">$14,680,240.00</td>
                          <td ui-table-cell class="text-destructive text-right">-$12,450.00</td>
                          <td ui-table-cell class="text-right">3,842</td>
                        </tr>
                        <tr ui-table-row class="font-mono text-xs">
                          <td ui-table-cell class="text-warning font-semibold">2026-02</td>
                          <td ui-table-cell class="text-right">$1,798,200.00</td>
                          <td ui-table-cell class="text-success text-right font-medium">$14,350,100.00</td>
                          <td ui-table-cell class="text-destructive text-right">-$14,100.00</td>
                          <td ui-table-cell class="text-right">3,780</td>
                        </tr>
                        <tr ui-table-row class="font-mono text-xs">
                          <td ui-table-cell class="text-warning font-semibold">2026-01</td>
                          <td ui-table-cell class="text-right">$1,720,000.00</td>
                          <td ui-table-cell class="text-success text-right font-medium">$13,920,400.00</td>
                          <td ui-table-cell class="text-destructive text-right">-$9,800.00</td>
                          <td ui-table-cell class="text-right">3,712</td>
                        </tr>
                      </tbody>
                    </table>
                  </div>
                </div>
              </div>
            </div>
          }
    
          <!-- 3-Column Lineage Pipeline Map Section -->
          <section class="border-border bg-card/40 space-y-4 rounded-xl border p-4 sm:p-5">
            <div class="border-border/80 flex flex-col gap-2 border-b pb-3 sm:flex-row sm:items-center sm:justify-between">
              <div class="space-y-0.5">
                <div class="flex items-center gap-2">
                  <lucide-icon [img]="Workflow" class="text-primary size-4" />
                  <h2 class="text-foreground text-sm font-semibold tracking-tight">End-to-End Data Lineage Graph</h2>
                  <span ui-badge variant="secondary" class="font-mono text-xs">DAG Depth: 3 Tiers</span>
                </div>
                <p class="text-muted-foreground text-xs">
                  Interactive trace of upstream ingest sources, dbt intermediate transformations, and active downstream BI
                  consumers.
                </p>
              </div>
    
              <div class="flex items-center gap-2">
                <div
                  class="border-border bg-background inline-flex items-center gap-1.5 rounded-md border px-2 py-1 text-xs"
                >
                  <span class="bg-success size-2 rounded-full"></span>
                  <span class="text-muted-foreground">Pipeline State:</span>
                  <span class="text-foreground font-medium">All 3 Tiers Fresh</span>
                </div>
              </div>
            </div>
    
            <div class="grid grid-cols-1 gap-4 lg:grid-cols-3">
              <!-- COLUMN 1: Source Tables -->
              <div class="space-y-3">
                <div class="flex items-center justify-between px-1">
                  <div class="flex items-center gap-1.5">
                    <lucide-icon [img]="Database" class="text-muted-foreground size-3.5" />
                    <span class="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
                      1. Source Tables (3)
                    </span>
                  </div>
                  <span ui-badge variant="outline" class="font-mono text-xs">Upstream Raw</span>
                </div>
    
                <div class="space-y-2.5">
                  @for (source of sources; track source.id) {
                    <button
                      type="button"
                      [class]="nodeCardClass(selectedNodeId() === source.id)"
                      [attr.aria-pressed]="selectedNodeId() === source.id"
                      (click)="selectedNodeId.set(source.id)"
                    >
                      <div class="flex items-start justify-between gap-2">
                        <div class="flex items-center gap-2">
                          <div
                            class="bg-muted text-foreground border-border/60 flex size-7 shrink-0 items-center justify-center rounded-md border"
                          >
                            @if (source.iconName === 'CreditCard') {
                              <lucide-icon [img]="CreditCard" class="text-chart-1 size-3.5" />
                            } @else if (source.iconName === 'Users') {
                              <lucide-icon [img]="Users" class="text-info size-3.5" />
                            } @else {
                              <lucide-icon [img]="Landmark" class="text-warning size-3.5" />
                            }
                          </div>
                          <div class="min-w-0">
                            <span class="text-foreground block truncate font-mono font-semibold">{{ source.name }}</span>
                            <span class="text-muted-foreground text-xs">{{ source.connector }}</span>
                          </div>
                        </div>
                        <span ui-badge class="border-success/20 bg-success/10 text-success shrink-0 font-mono text-xs">
                          {{ source.status }}
                        </span>
                      </div>
    
                      <div
                        class="border-border/50 text-muted-foreground mt-2.5 flex items-center justify-between border-t pt-2"
                      >
                        <span class="font-mono">{{ source.rowCount }}</span>
                        <span class="flex items-center gap-1 font-mono">
                          <lucide-icon [img]="Clock" class="size-3" />
                          {{ source.latency }}
                        </span>
                      </div>
                    </button>
                  }
                </div>
              </div>
    
              <!-- COLUMN 2: Transform Models -->
              <div class="space-y-3">
                <div class="flex items-center justify-between px-1">
                  <div class="flex items-center gap-1.5">
                    <lucide-icon [img]="Layers" class="text-muted-foreground size-3.5" />
                    <span class="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
                      2. Transform Models (2)
                    </span>
                  </div>
                  <span ui-badge variant="outline" class="font-mono text-xs">dbt Models</span>
                </div>
    
                <div class="space-y-2.5">
                  @for (transform of transforms; track transform.id) {
                    <button
                      type="button"
                      [class]="nodeCardClass(selectedNodeId() === transform.id)"
                      [attr.aria-pressed]="selectedNodeId() === transform.id"
                      (click)="selectedNodeId.set(transform.id)"
                    >
                      <div class="flex items-start justify-between gap-2">
                        <div class="flex items-center gap-2">
                          <div
                            class="bg-primary/10 text-primary border-primary/20 flex size-7 shrink-0 items-center justify-center rounded-md border"
                          >
                            @if (transform.iconName === 'Layers') {
                              <lucide-icon [img]="Layers" class="size-3.5" />
                            } @else {
                              <lucide-icon [img]="Coins" class="size-3.5" />
                            }
                          </div>
                          <div class="min-w-0">
                            <span class="text-foreground block truncate font-mono font-semibold">{{ transform.name }}</span>
                            <span class="text-muted-foreground text-xs">{{ transform.modelType }}</span>
                          </div>
                        </div>
                        <span ui-badge variant="secondary" class="shrink-0 font-mono text-xs">
                          {{ transform.runtime }}
                        </span>
                      </div>
    
                      <div
                        class="border-border/50 text-muted-foreground mt-2.5 flex items-center justify-between border-t pt-2"
                      >
                        <span class="text-success flex items-center gap-1 font-medium">
                          <lucide-icon [img]="CheckCircle2" class="size-3" />
                          {{ transform.testsPassed }}
                        </span>
                        <span class="font-mono">{{ transform.lastRun }}</span>
                      </div>
                    </button>
                  }
                </div>
              </div>
    
              <!-- COLUMN 3: Downstream Consumers & Active Target -->
              <div class="space-y-3">
                <div class="flex items-center justify-between px-1">
                  <div class="flex items-center gap-1.5">
                    <lucide-icon [img]="BarChart3" class="text-muted-foreground size-3.5" />
                    <span class="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
                      3. Downstream & Target
                    </span>
                  </div>
                  <span ui-badge variant="outline" class="font-mono text-xs">Marts & Consumers</span>
                </div>
    
                <div class="space-y-2.5">
                  <button
                    type="button"
                    [class]="targetCardClass"
                    [attr.aria-pressed]="selectedNodeId() === 'target-table'"
                    (click)="selectedNodeId.set('target-table')"
                  >
                    <div class="flex items-start justify-between gap-2">
                      <div class="flex items-center gap-2">
                        <div
                          class="bg-primary text-primary-foreground flex size-7 shrink-0 items-center justify-center rounded-md"
                        >
                          <lucide-icon [img]="Table2" class="size-3.5" />
                        </div>
                        <div class="min-w-0">
                          <div class="flex items-center gap-1.5">
                            <span class="text-foreground truncate font-mono font-bold">fct_monthly_financials</span>
                          </div>
                          <span class="text-primary text-xs font-medium">Active Catalog Target</span>
                        </div>
                      </div>
                      <span ui-badge class="bg-primary text-primary-foreground shrink-0 text-xs font-medium">
                        Current
                      </span>
                    </div>
                  </button>
    
                  @for (consumer of consumers; track consumer.id) {
                    <button
                      type="button"
                      [class]="nodeCardClass(selectedNodeId() === consumer.id)"
                      [attr.aria-pressed]="selectedNodeId() === consumer.id"
                      (click)="selectedNodeId.set(consumer.id)"
                    >
                      <div class="flex items-start justify-between gap-2">
                        <div class="flex items-center gap-2">
                          <div
                            class="bg-muted text-foreground border-border/60 flex size-7 shrink-0 items-center justify-center rounded-md border"
                          >
                            @if (consumer.iconName === 'BarChart3') {
                              <lucide-icon [img]="BarChart3" class="text-info size-3.5" />
                            } @else if (consumer.iconName === 'ArrowRightLeft') {
                              <lucide-icon [img]="ArrowRightLeft" class="text-chart-2 size-3.5" />
                            } @else {
                              <lucide-icon [img]="LineChart" class="text-success size-3.5" />
                            }
                          </div>
                          <div class="min-w-0">
                            <span class="text-foreground block truncate font-medium">{{ consumer.name }}</span>
                            <span class="text-muted-foreground text-xs">{{ consumer.category }} · {{ consumer.tool }}</span>
                          </div>
                        </div>
                        <span ui-badge variant="secondary" class="shrink-0 text-xs">
                          {{ consumer.status }}
                        </span>
                      </div>
    
                      <div
                        class="border-border/50 text-muted-foreground mt-2.5 flex items-center justify-between border-t pt-2"
                      >
                        <span class="truncate">{{ consumer.audience }}</span>
                        <span class="shrink-0 font-mono text-xs">{{ consumer.frequency }}</span>
                      </div>
                    </button>
                  }
                </div>
              </div>
            </div>
    
            @if (selectedNodeDetails(); as details) {
              <div class="border-border/80 bg-muted/40 rounded-lg border p-3.5 text-xs">
                <div
                  class="border-border/60 flex flex-col gap-2 border-b pb-2 sm:flex-row sm:items-center sm:justify-between"
                >
                  <div class="flex items-center gap-2">
                    <span class="text-foreground font-mono font-bold">{{ details.title }}</span>
                    <span ui-badge variant="outline" class="font-mono text-xs">{{ details.badge }}</span>
                  </div>
                  <span class="text-muted-foreground text-xs">Node Inspector</span>
                </div>
                <p class="text-muted-foreground mt-2">{{ details.description }}</p>
                <div class="mt-3 grid grid-cols-2 gap-2 sm:grid-cols-4">
                  @for (item of details.metadata; track item.label) {
                    <div class="border-border/60 bg-card rounded-md border px-2.5 py-1.5">
                      <span class="text-muted-foreground block text-xs">{{ item.label }}:</span>
                      <span class="text-foreground block truncate font-mono font-medium">{{ item.value }}</span>
                    </div>
                  }
                </div>
              </div>
            }
          </section>
    
          <!-- Table Schema, Quality Tests & Governance Tabs -->
          <ui-tabs [value]="activeTab()" (valueChange)="activeTab.set($event)" class="w-full">
            <ui-tabs-list class="grid h-10 w-full grid-cols-2 p-1 sm:grid-cols-4">
              <button ui-tabs-trigger value="schema" class="gap-1.5 text-xs">
                <lucide-icon [img]="Table2" class="size-3.5" />
                <span>Schema & Columns</span>
                <span class="bg-muted rounded-full px-1.5 font-mono text-xs">{{ columnCount }}</span>
              </button>
    
              <button ui-tabs-trigger value="tests" class="gap-1.5 text-xs">
                <lucide-icon [img]="ShieldCheck" class="text-success size-3.5" />
                <span>Quality Tests</span>
                <span class="bg-success/10 text-success rounded-full px-1.5 font-mono text-xs font-semibold"> 6/6 </span>
              </button>
    
              <button ui-tabs-trigger value="sql" class="gap-1.5 text-xs">
                <lucide-icon [img]="FileCode2" class="size-3.5" />
                <span>dbt Model SQL</span>
              </button>
    
              <button ui-tabs-trigger value="governance" class="gap-1.5 text-xs">
                <lucide-icon [img]="Activity" class="size-3.5" />
                <span>SLA & Governance</span>
              </button>
            </ui-tabs-list>
    
            <ui-tabs-content value="schema" class="mt-4 space-y-4">
              <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
                <div class="relative w-full sm:w-72">
                  <lucide-icon
                    [img]="Search"
                    class="text-muted-foreground absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2"
                  />
                  <ui-input
                    [value]="searchQuery()"
                    (valueChange)="searchQuery.set($event)"
                    placeholder="Search columns, types, or terms..."
                    class="h-8.5 pl-8 text-xs"
                  />
                </div>
    
                <div class="flex items-center gap-2">
                  <div class="text-muted-foreground text-xs">
                    Showing <span class="text-foreground font-mono font-medium">{{ filteredColumns().length }}</span> of
                    {{ columnCount }} columns
                  </div>
                  <button ui-button variant="outline" size="sm" class="h-8.5 gap-1.5 text-xs" (click)="copyColumnNames()">
                    <lucide-icon [img]="Copy" class="size-3.5" />
                    <span>Copy Columns</span>
                  </button>
                </div>
              </div>
    
              <div ui-card class="border-border overflow-hidden border shadow-none">
                <div class="overflow-x-auto">
                  <table ui-table>
                    <thead ui-table-header>
                      <tr ui-table-row class="bg-muted/40 hover:bg-muted/40">
                        <th ui-table-head class="text-xs font-semibold">Column Name</th>
                        <th ui-table-head class="text-xs font-semibold">Data Type</th>
                        <th ui-table-head class="text-xs font-semibold">Nullability</th>
                        <th ui-table-head class="text-xs font-semibold">Key / Constraint</th>
                        <th ui-table-head class="text-xs font-semibold">Sample Value</th>
                        <th ui-table-head class="text-xs font-semibold">Description</th>
                      </tr>
                    </thead>
                    <tbody ui-table-body>
                      @for (col of filteredColumns(); track col.name) {
                        <tr ui-table-row class="text-xs">
                          <td ui-table-cell class="font-mono font-medium">
                            <div class="flex items-center gap-1.5">
                              @if (col.isPrimaryKey) {
                                <lucide-icon [img]="Key" class="text-warning size-3.5 shrink-0" />
                              }
                              <span [class]="col.isPrimaryKey ? 'text-warning font-bold' : ''">
                                {{ col.name }}
                              </span>
                            </div>
                          </td>
    
                          <td ui-table-cell>
                            <span ui-badge variant="secondary" class="font-mono text-xs font-normal">
                              {{ col.type }}
                            </span>
                          </td>
    
                          <td ui-table-cell>
                            @if (!col.nullable) {
                              <span ui-badge variant="outline" class="text-muted-foreground font-mono text-xs font-normal">
                                NOT NULL
                              </span>
                            } @else {
                              <span class="text-muted-foreground font-mono">NULLABLE</span>
                            }
                          </td>
    
                          <td ui-table-cell>
                            <div class="flex flex-wrap items-center gap-1">
                              @if (col.isPrimaryKey) {
                                <span ui-badge class="border-warning/30 bg-warning/15 text-warning text-xs font-semibold">
                                  PRIMARY KEY
                                </span>
                              } @else if (col.constraints) {
                                <span ui-badge variant="secondary" class="font-mono text-xs">
                                  {{ col.constraints }}
                                </span>
                              } @else {
                                <span class="text-muted-foreground">—</span>
                              }
                            </div>
                          </td>
    
                          <td ui-table-cell class="text-muted-foreground font-mono">
                            {{ col.sampleValue }}
                          </td>
    
                          <td ui-table-cell class="text-muted-foreground max-w-sm">
                            {{ col.description }}
                          </td>
                        </tr>
                      }
                    </tbody>
                  </table>
                </div>
              </div>
    
              <div class="grid grid-cols-2 gap-3 sm:grid-cols-4">
                <div class="border-border bg-card space-y-1 rounded-lg border p-3 text-xs">
                  <span class="text-muted-foreground">Total Schema Columns</span>
                  <span class="text-foreground block font-mono text-base font-bold">5</span>
                  <span class="text-muted-foreground text-xs">1 Primary Key, 4 Measures</span>
                </div>
    
                <div class="border-border bg-card space-y-1 rounded-lg border p-3 text-xs">
                  <span class="text-muted-foreground">Completeness Score</span>
                  <span class="text-success block font-mono text-base font-bold">97.5%</span>
                  <span class="text-muted-foreground text-xs">4/5 Non-Nullable fields</span>
                </div>
    
                <div class="border-border bg-card space-y-1 rounded-lg border p-3 text-xs">
                  <span class="text-muted-foreground">Table Storage Volume</span>
                  <span class="text-foreground block font-mono text-base font-bold">48.6 MB</span>
                  <span class="text-muted-foreground text-xs">Micro-partition compressed</span>
                </div>
    
                <div class="border-border bg-card space-y-1 rounded-lg border p-3 text-xs">
                  <span class="text-muted-foreground">Governance Tier</span>
                  <span class="text-primary block font-mono text-base font-bold">Gold Tier 1</span>
                  <span class="text-muted-foreground text-xs">Certified Financial Source</span>
                </div>
              </div>
            </ui-tabs-content>
    
            <ui-tabs-content value="tests" class="mt-4 space-y-4">
              <div ui-card class="border-border bg-card overflow-hidden border shadow-none">
                <div ui-card-header class="border-border/80 border-b p-4 sm:p-5">
                  <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
                    <div class="space-y-1">
                      <div class="flex items-center gap-2">
                        <lucide-icon [img]="ShieldCheck" class="text-success size-5" />
                        <h3 ui-card-title class="text-base font-semibold">Data Quality & Integrity Assertions</h3>
                        <span ui-badge class="border-success/20 bg-success/10 text-success font-mono text-xs">
                          100% Passing (6/6)
                        </span>
                      </div>
                      <p ui-card-description class="text-xs">
                        Automated dbt-expectations, Monte Carlo ML anomaly detection, and referential integrity test suite.
                      </p>
                    </div>
    
                    <button
                      ui-button
                      variant="outline"
                      size="sm"
                      class="h-8.5 shrink-0 gap-1.5 self-start text-xs sm:self-auto"
                      [disabled]="isRunningTests()"
                      (click)="runSimulatedTests()"
                    >
                      <lucide-icon [img]="RefreshCw" class="size-3.5" [class]="spinClass" />
                      <span>{{ isRunningTests() ? 'Running 6 Tests...' : 'Re-run Test Suite' }}</span>
                    </button>
                  </div>
                </div>
    
                <div ui-card-content class="space-y-4 p-4 sm:p-5">
                  <div class="flex flex-wrap items-center gap-2">
                    <button
                      ui-button
                      variant="outline"
                      size="sm"
                      [class]="severityPillClass('all')"
                      (click)="filterSeverity.set('all')"
                    >
                      All Assertions (6)
                    </button>
                    <button
                      ui-button
                      variant="outline"
                      size="sm"
                      [class]="severityPillClass('critical')"
                      (click)="filterSeverity.set('critical')"
                    >
                      Primary Key & Referential (2)
                    </button>
                    <button
                      ui-button
                      variant="outline"
                      size="sm"
                      [class]="severityPillClass('schema')"
                      (click)="filterSeverity.set('schema')"
                    >
                      Format & Completeness (2)
                    </button>
                  </div>
    
                  <div class="space-y-2.5">
                    @for (assertion of filteredQualityTests(); track assertion.id) {
                      <div class="border-border bg-muted/20 hover:bg-muted/40 rounded-lg border p-3.5 transition-colors">
                        <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
                          <div class="flex items-start gap-2.5">
                            <div
                              class="bg-success/15 text-success mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-full"
                            >
                              <lucide-icon [img]="Check" class="size-3.5" />
                            </div>
                            <div class="space-y-1">
                              <div class="flex flex-wrap items-center gap-2">
                                <span class="text-foreground font-mono text-xs font-bold">{{ assertion.name }}</span>
                                <span ui-badge variant="secondary" class="font-mono text-xs">{{ assertion.type }}</span>
                                <span class="text-muted-foreground text-xs"
                                  >target: <code class="text-foreground font-mono">{{ assertion.targetColumn }}</code></span
                                >
                              </div>
                              <p class="text-muted-foreground text-xs">{{ assertion.description }}</p>
                              <div
                                class="text-muted-foreground bg-background/80 border-border/50 inline-block rounded border px-2 py-0.5 font-mono text-xs"
                              >
                                {{ assertion.ruleDefinition }}
                              </div>
                            </div>
                          </div>
    
                          <div class="flex shrink-0 items-center gap-3 self-end text-xs sm:self-center">
                            <div class="text-right">
                              <span class="text-muted-foreground block text-xs">Duration</span>
                              <span class="text-foreground font-mono font-medium">{{ assertion.executionTime }}</span>
                            </div>
                            <span ui-badge class="border-success/20 bg-success/10 text-success font-mono text-xs">
                              Passed
                            </span>
                          </div>
                        </div>
                      </div>
                    }
                  </div>
                </div>
              </div>
            </ui-tabs-content>
    
            <ui-tabs-content value="sql" class="mt-4">
              <div class="border-border bg-muted/40 overflow-hidden rounded-xl border">
                <div class="border-border bg-card flex items-center justify-between border-b px-4 py-2.5">
                  <div class="flex items-center gap-2">
                    <lucide-icon [img]="FileCode2" class="text-primary size-4" />
                    <span class="text-foreground font-mono text-xs font-semibold"
                      >models/marts/finance/fct_monthly_financials.sql</span
                    >
                    <span ui-badge variant="secondary" class="font-mono text-xs">dbt v1.8 Incremental</span>
                  </div>
                  <button ui-button variant="ghost" size="sm" class="h-7 gap-1 text-xs" (click)="copyCompiledSQL()">
                    @if (copiedQuery()) {
                      <lucide-icon [img]="Check" class="text-success size-3" />
                    } @else {
                      <lucide-icon [img]="Copy" class="size-3" />
                    }
                    <span>{{ copiedQuery() ? 'Copied Model' : 'Copy dbt Code' }}</span>
                  </button>
                </div>
    
                <div
                  class="overflow-x-auto bg-neutral-950 p-4 font-mono text-xs leading-relaxed text-neutral-100 dark:bg-neutral-950"
                >
                  <pre class="whitespace-pre"><code>{{ modelSnippet }}</code></pre>
                </div>
              </div>
            </ui-tabs-content>
    
            <ui-tabs-content value="governance" class="mt-4 space-y-4">
              <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
                <div ui-card class="border-border space-y-3 border p-4 shadow-none">
                  <div class="border-border/80 flex items-center gap-2 border-b pb-2">
                    <lucide-icon [img]="Users" class="text-primary size-4" />
                    <h3 class="text-foreground text-sm font-semibold">Table Ownership & Escalation</h3>
                  </div>
                  <div class="space-y-2 text-xs">
                    <div class="flex items-center justify-between">
                      <span class="text-muted-foreground">Owning Team:</span>
                      <span class="text-foreground font-semibold">Data Platform Squad</span>
                    </div>
                    <div class="flex items-center justify-between">
                      <span class="text-muted-foreground">Technical Lead:</span>
                      <span class="text-foreground font-mono">&#64;sarah.lin (Principal Data Eng)</span>
                    </div>
                    <div class="flex items-center justify-between">
                      <span class="text-muted-foreground">Slack Channel:</span>
                      <span ui-badge variant="secondary" class="font-mono text-xs">#data-platform-alerts</span>
                    </div>
                    <div class="flex items-center justify-between">
                      <span class="text-muted-foreground">On-Call Rotation:</span>
                      <span class="text-foreground">PagerDuty · Tier 1 High Severity</span>
                    </div>
                  </div>
                </div>
    
                <div ui-card class="border-border space-y-3 border p-4 shadow-none">
                  <div class="border-border/80 flex items-center gap-2 border-b pb-2">
                    <lucide-icon [img]="Clock" class="text-success size-4" />
                    <h3 class="text-foreground text-sm font-semibold">Freshness SLA & Cadence</h3>
                  </div>
                  <div class="space-y-2 text-xs">
                    <div class="flex items-center justify-between">
                      <span class="text-muted-foreground">Target SLA Threshold:</span>
                      <span class="text-foreground font-mono font-medium">&lt; 60 minutes</span>
                    </div>
                    <div class="flex items-center justify-between">
                      <span class="text-muted-foreground">Current Latency:</span>
                      <span class="text-success font-mono font-bold">18 minutes (Compliant)</span>
                    </div>
                    <div class="flex items-center justify-between">
                      <span class="text-muted-foreground">Cron Schedule:</span>
                      <code class="text-foreground bg-muted rounded px-1.5 py-0.5 font-mono text-xs"
                        >0 * * * * (Hourly)</code
                      >
                    </div>
                    <div class="flex items-center justify-between">
                      <span class="text-muted-foreground">Historical Uptime:</span>
                      <span class="text-foreground font-mono font-medium">99.98% (Last 90 days)</span>
                    </div>
                  </div>
                </div>
              </div>
            </ui-tabs-content>
          </ui-tabs>
        </div>
      `,
    })
    export class UiDataCatalogLineageComponent {
      protected readonly Activity = Activity
      protected readonly ArrowRightLeft = ArrowRightLeft
      protected readonly BarChart3 = BarChart3
      protected readonly Check = Check
      protected readonly CheckCircle2 = CheckCircle2
      protected readonly Clock = Clock
      protected readonly Coins = Coins
      protected readonly Copy = Copy
      protected readonly CreditCard = CreditCard
      protected readonly Database = Database
      protected readonly FileCode2 = FileCode2
      protected readonly GitFork = GitFork
      protected readonly Key = Key
      protected readonly Landmark = Landmark
      protected readonly Layers = Layers
      protected readonly LineChart = LineChart
      protected readonly RefreshCw = RefreshCw
      protected readonly Search = Search
      protected readonly ShieldCheck = ShieldCheck
      protected readonly Table2 = Table2
      protected readonly Terminal = Terminal
      protected readonly Users = Users
      protected readonly Workflow = Workflow
      protected readonly X = X
    
      protected readonly sources = sourceNodes
      protected readonly transforms = transformNodes
      protected readonly consumers = consumerNodes
      protected readonly columnCount = columns.length
      protected readonly modelSnippet = dbtModelSnippet
    
      @Input('class') className?: string
    
      readonly activeTab = signal('schema')
      readonly searchQuery = signal('')
      readonly copiedRef = signal(false)
      readonly copiedQuery = signal(false)
      readonly isRunningTests = signal(false)
      readonly isQueryModalOpen = signal(false)
      readonly selectedNodeId = signal<string | null>('target-table')
      readonly filterSeverity = signal<'all' | 'critical' | 'schema'>('all')
    
      readonly filteredColumns = computed(() => {
        const query = this.searchQuery().trim().toLowerCase()
        if (!query) return columns
        return columns.filter(
          (col) =>
            col.name.toLowerCase().includes(query) ||
            col.type.toLowerCase().includes(query) ||
            col.description.toLowerCase().includes(query),
        )
      })
    
      readonly filteredQualityTests = computed(() => {
        if (this.filterSeverity() === 'critical') {
          return qualityAssertions.filter((t) => t.type.includes('Primary') || t.type.includes('Referential'))
        }
        if (this.filterSeverity() === 'schema') {
          return qualityAssertions.filter((t) => t.type.includes('Format') || t.type.includes('Completeness'))
        }
        return qualityAssertions
      })
    
      readonly selectedNodeDetails = computed(() => {
        if (this.selectedNodeId() === 'target-table') {
          return {
            title: 'analytics.fct_monthly_financials',
            badge: 'Active Target Table',
            description:
              'Gold-tier core financial mart consolidating Stripe billing, FX rates, and Salesforce CRM accounts for executive reporting.',
            metadata: [
              { label: 'Layer', value: 'Marts / Core Analytics' },
              { label: 'Schema', value: 'production_dw' },
              { label: 'Total Rows', value: '240,000' },
              { label: 'Freshness SLA', value: '60 mins (Current: 18m)' },
            ],
          }
        }
    
        const src = sourceNodes.find((n) => n.id === this.selectedNodeId())
        if (src) {
          return {
            title: src.name,
            badge: 'Source Table',
            description: src.description,
            metadata: [
              { label: 'Connector', value: src.connector },
              { label: 'Ingestion Type', value: src.type },
              { label: 'Volume', value: src.rowCount },
              { label: 'Sync Cadence', value: src.latency },
            ],
          }
        }
    
        const tr = transformNodes.find((n) => n.id === this.selectedNodeId())
        if (tr) {
          return {
            title: tr.name,
            badge: 'dbt Intermediate Model',
            description: tr.description,
            metadata: [
              { label: 'Engine', value: tr.engine },
              { label: 'Materialization', value: tr.modelType },
              { label: 'Build Duration', value: tr.runtime },
              { label: 'Test Suite', value: tr.testsPassed },
            ],
          }
        }
    
        const cons = consumerNodes.find((n) => n.id === this.selectedNodeId())
        if (cons) {
          return {
            title: cons.name,
            badge: 'Downstream Consumer',
            description: cons.description,
            metadata: [
              { label: 'Tool Platform', value: cons.tool },
              { label: 'Type', value: cons.category },
              { label: 'Audience / Usage', value: cons.audience },
              { label: 'Sync Schedule', value: cons.frequency },
            ],
          }
        }
    
        return null
      })
    
      get rootClass(): string {
        return cn(
          'bg-background text-foreground border-border w-full space-y-6 rounded-xl border p-4 shadow-xs sm:p-6',
          this.className,
        )
      }
    
      get spinClass(): string {
        return cn(this.isRunningTests() && 'text-primary animate-spin')
      }
    
      get targetCardClass(): string {
        return cn(
          'focus-visible:ring-ring relative w-full cursor-pointer rounded-lg border-2 p-3 text-left text-xs shadow-xs transition-[border-color,box-shadow] focus-visible:ring-2 focus-visible:outline-none',
          this.selectedNodeId() === 'target-table'
            ? 'border-primary bg-primary/10 ring-primary/20 ring-2'
            : 'border-primary/60 bg-primary/5 hover:border-primary',
        )
      }
    
      nodeCardClass(selected: boolean): string {
        return cn(
          'group focus-visible:ring-ring relative w-full cursor-pointer rounded-lg border p-3 text-left text-xs shadow-xs transition-[background-color,border-color,box-shadow] focus-visible:ring-2 focus-visible:outline-none',
          selected
            ? 'border-primary bg-primary/5 ring-primary/30 ring-1'
            : 'border-border bg-card hover:border-border/80 hover:bg-muted/40',
        )
      }
    
      severityPillClass(sev: 'all' | 'critical' | 'schema'): string {
        return cn('h-7 text-xs', this.filterSeverity() === sev && 'bg-muted font-semibold')
      }
    
      resetLineageFocus(): void {
        this.selectedNodeId.set('target-table')
        this.activeTab.set('schema')
      }
    
      copyRef(text: string): void {
        if (typeof navigator !== 'undefined' && navigator.clipboard) {
          void navigator.clipboard.writeText(text)
          this.copiedRef.set(true)
          setTimeout(() => this.copiedRef.set(false), 2000)
        }
      }
    
      copyColumnNames(): void {
        this.copyRef(columns.map((c) => c.name).join(', '))
      }
    
      copyCompiledSQL(): void {
        if (typeof navigator !== 'undefined' && navigator.clipboard) {
          void navigator.clipboard.writeText(dbtModelSnippet)
          this.copiedQuery.set(true)
          setTimeout(() => this.copiedQuery.set(false), 2000)
        }
      }
    
      runSimulatedTests(): void {
        this.isRunningTests.set(true)
        setTimeout(() => this.isRunningTests.set(false), 1200)
      }
    }
    
  • src/app/components/blocks/data-catalog-lineage/index.ts0.2 kB

Raw manifest:https://uipkge.dev/r/angular/data-catalog-lineage.json