Data Catalog Lineage
blockdevopsAlation 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
$pnpm dlx uipkge-ng@latest add data-catalog-lineage$npx uipkge-ng@latest add data-catalog-lineage$yarn dlx uipkge-ng@latest add data-catalog-lineage$bunx uipkge-ng@latest add data-catalog-lineageManifest:
https://uipkge.dev/r/angular/data-catalog-lineage.jsonInstalls to:src/app/components/blocks/data-catalog-lineage/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
ColumnDefinterface ColumnDef {
name: string
type: string
nullable: boolean
isPrimaryKey?: boolean
description: string
sampleValue: string
distinctPct: string
constraints?: string
}QualityAssertioninterface QualityAssertion {
id: string
name: string
type: string
targetColumn: string
description: string
executionTime: string
lastRun: string
status: 'passed' | 'warning' | 'failed'
ruleDefinition: string
}LineageSourceNodeinterface 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
}LineageTransformNodeinterface LineageTransformNode {
id: string
name: string
modelType: string
engine: string
runtime: string
lastRun: string
testsPassed: string
status: 'passed' | 'running' | 'failed'
iconName: 'Layers' | 'Coins'
description: string
}LineageConsumerNodeinterface 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">@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">< 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
export { UiDataCatalogLineageComponent, type ColumnDef, type QualityAssertion, type LineageSourceNode, type LineageTransformNode, type LineageConsumerNode, } from './data-catalog-lineage.component'
Raw manifest:https://uipkge.dev/r/angular/data-catalog-lineage.json