Schema Drift Alert Board
blockdevopsAutomated data warehouse schema drift and structural anomaly detector with downstream blast radius impact analysis, interactive multi-tier lineage graph, schema mutation contract diffs, and remediation workflows.
Installation
$pnpm dlx shadcn-vue@latest add https://uipkge.dev/r/vue/schema-drift-alert-board.json$npx shadcn-vue@latest add https://uipkge.dev/r/vue/schema-drift-alert-board.json$yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/schema-drift-alert-board.json$bunx shadcn-vue@latest add https://uipkge.dev/r/vue/schema-drift-alert-board.jsonNamed registry:
npx shadcn-vue@latest add @uipkge/schema-drift-alert-boardInstalls to:app/components/blocks/Variants
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
title | string | 'Data Warehouse Schema Drift & Anomaly Alerts' | optional |
monitoredEnvironments | string[] | () => ['Snowflake Prod' | optional |
activeMutationsCount | number | — | optional |
incidents | SchemaDriftIncident[] | — | optional |
class | HTMLAttributes['class'] | — | optional |
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
BlastRadiusNodeinterface BlastRadiusNode {
id: string
name: string
layer: 'origin' | 'transform' | 'mart' | 'consumer'
layerLabel: string
status: 'mutation' | 'impacted' | 'healthy' | 'warning'
statusLabel: string
type: string
description: string
owner: string
runtime?: string
}SchemaDriftIncidentinterface SchemaDriftIncident {
id: string
targetTable: string
environment: 'Snowflake Prod' | 'Postgres OLTP' | 'Kafka Topics'
changeType: 'Unexpected Column Added' | 'Data Type Alteration' | 'Nullability Relaxation' | 'Enum Definition Extended'
driftDiff: string
diffMode: 'added' | 'altered' | 'relaxed'
severity: 'low' | 'medium' | 'high'
severityLabel: string
blastRadiusSummary: string
brokenModelsCount: number
impactedPipelinesCount: number
detectedAt: string
actionLabel: string
isAcknowledged: boolean
isContractSynced: boolean
contractYaml: string
remediationDdl: string
lineageNodes: BlastRadiusNode[]
}Files installed (1)
app/components/blocks/SchemaDriftAlertBoard.vue47.4 kB
<script setup lang="ts"> import { computed, ref } from 'vue' import type { HTMLAttributes } from 'vue' import { Activity, AlertCircle, AlertTriangle, ArrowUpRight, BellOff, BellRing, Check, CheckCircle2, Clock, Copy, Database, Eye, FileCode2, Filter, Layers, Network, RefreshCw, Search, ShieldAlert, ShieldCheck, Table2, Terminal, Workflow, } from 'lucide-vue-next' import { cn } from '@/lib/utils' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Progress } from '@/components/ui/progress' import { Separator } from '@/components/ui/separator' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' export interface BlastRadiusNode { id: string name: string layer: 'origin' | 'transform' | 'mart' | 'consumer' layerLabel: string status: 'mutation' | 'impacted' | 'healthy' | 'warning' statusLabel: string type: string description: string owner: string runtime?: string } export interface SchemaDriftIncident { id: string targetTable: string environment: 'Snowflake Prod' | 'Postgres OLTP' | 'Kafka Topics' changeType: 'Unexpected Column Added' | 'Data Type Alteration' | 'Nullability Relaxation' | 'Enum Definition Extended' driftDiff: string diffMode: 'added' | 'altered' | 'relaxed' severity: 'low' | 'medium' | 'high' severityLabel: string blastRadiusSummary: string brokenModelsCount: number impactedPipelinesCount: number detectedAt: string actionLabel: string isAcknowledged: boolean isContractSynced: boolean contractYaml: string remediationDdl: string lineageNodes: BlastRadiusNode[] } export interface SchemaDriftAlertBoardProps { title?: string monitoredEnvironments?: string[] activeMutationsCount?: number incidents?: SchemaDriftIncident[] class?: HTMLAttributes['class'] } const defaultIncidents: SchemaDriftIncident[] = [ { id: 'drift-inc-1', targetTable: 'public.users', environment: 'Postgres OLTP', changeType: 'Unexpected Column Added', driftDiff: '+ phone_country_code VARCHAR(8)', diffMode: 'added', severity: 'low', severityLabel: 'Low Non-Breaking', blastRadiusSummary: '0 models broken', brokenModelsCount: 0, impactedPipelinesCount: 0, detectedAt: '14m ago', actionLabel: 'Add to Data Contract', isAcknowledged: false, isContractSynced: false, contractYaml: `version: 2 models: - name: dim_users config: contract: enforced: true columns: - name: user_id data_type: varchar constraints: - type: not_null # Newly detected column added upstream in Postgres OLTP - name: phone_country_code data_type: varchar(8) description: "ISO-3166-1 alpha-2 / E.164 dial prefix added by auth service" constraints: - type: check expression: "length(phone_country_code) <= 8"`, remediationDdl: `-- Automated dbt / Snowflake staging sync ALTER TABLE raw_postgres.users ADD COLUMN IF NOT EXISTS phone_country_code VARCHAR(8); -- Mark schema contract version 2.4 compliant COMMENT ON COLUMN raw_postgres.users.phone_country_code IS 'Auto-registered by UIPKGE schema-drift-guardian';`, lineageNodes: [ { id: 'node-users-origin', name: 'public.users', layer: 'origin', layerLabel: 'Postgres Source Table', status: 'mutation', statusLabel: 'Column Added (+1 col)', type: 'Postgres Table', description: 'OLTP user management table with new phone_country_code attribute.', owner: '#team-auth', }, { id: 'node-users-ingest', name: 'fivetran.raw_postgres_users', layer: 'transform', layerLabel: 'CDC Connector Stream', status: 'healthy', statusLabel: 'CDC Auto-Synced', type: 'Fivetran Log-Based CDC', description: 'Automated column replication synced to Snowflake bronze raw zone.', owner: '#data-platform', runtime: '<2m latency', }, { id: 'node-users-stg', name: 'dbt.stg_users', layer: 'transform', layerLabel: 'dbt Staging View', status: 'healthy', statusLabel: '0 Breaking (Explicit Select)', type: 'dbt View Model', description: 'Explicit column projection prevents unmapped column propagation.', owner: '#data-eng', runtime: '12s run', }, { id: 'node-users-dim', name: 'marts.dim_customers', layer: 'mart', layerLabel: 'Core Dimension Mart', status: 'healthy', statusLabel: 'Contract Compliant', type: 'Snowflake Dynamic Table', description: 'Certified dimension table feeding marketing cohorts and customer 360.', owner: '#data-eng', }, { id: 'node-users-crm', name: 'HubSpot Reverse ETL Sync', layer: 'consumer', layerLabel: 'Operational Reverse ETL', status: 'healthy', statusLabel: 'Active · 0 Failures', type: 'Census Sync Pipeline', description: 'Outbound sync to HubSpot Marketing Hub CRM contacts.', owner: '#rev-ops', }, ], }, { id: 'drift-inc-2', targetTable: 'analytics.fct_orders', environment: 'Snowflake Prod', changeType: 'Data Type Alteration', driftDiff: 'discount_amount FLOAT -> DECIMAL(18,2)', diffMode: 'altered', severity: 'medium', severityLabel: 'Medium Warning', blastRadiusSummary: '2 dbt models affected', brokenModelsCount: 0, impactedPipelinesCount: 2, detectedAt: '32m ago', actionLabel: 'Inspect Model Lineage', isAcknowledged: false, isContractSynced: false, contractYaml: `version: 2 models: - name: fct_orders config: contract: enforced: true columns: - name: order_id data_type: varchar - name: gross_amount data_type: decimal(18,2) # Altered type: FLOAT replaced with exact high-precision DECIMAL(18,2) - name: discount_amount data_type: decimal(18,2) # Previously float64 description: "Monetary discount in USD with exact 2 decimal place precision" constraints: - type: check expression: "discount_amount >= 0"`, remediationDdl: `-- Model Migration Patch for dbt Intermediate Layers: -- Fix implicit float casting in int_daily_revenue and int_order_discounts: ALTER TABLE production_dw.analytics.fct_orders ALTER COLUMN discount_amount SET DATA TYPE DECIMAL(18,2); -- Update dbt schema contract definition: -- dbt run --select int_daily_revenue+ --vars '{ "allow_decimal_cast": true }'`, lineageNodes: [ { id: 'node-orders-origin', name: 'analytics.fct_orders', layer: 'origin', layerLabel: 'Warehouse Fact Table', status: 'mutation', statusLabel: 'Type Alteration (FLOAT -> DECIMAL)', type: 'Snowflake Incremental', description: 'Core financial transactions mart table receiving monetary precision fix.', owner: '#finance-data', }, { id: 'node-orders-int-rev', name: 'dbt.int_daily_revenue', layer: 'transform', layerLabel: 'dbt Intermediate Model', status: 'impacted', statusLabel: 'Impacted (Sum Float Warning)', type: 'dbt Incremental Model', description: 'Aggregates daily gross/net sales; needs explicit decimal cast in SUM expression.', owner: '#data-eng', runtime: '1m 18s', }, { id: 'node-orders-int-disc', name: 'dbt.int_order_discounts', layer: 'transform', layerLabel: 'dbt Intermediate Model', status: 'impacted', statusLabel: 'Impacted (Join Key Precision)', type: 'dbt View Model', description: 'Promotional discount matrix model joined on discount_amount thresholds.', owner: '#data-eng', runtime: '45s', }, { id: 'node-orders-mart', name: 'marts.fct_monthly_financials', layer: 'mart', layerLabel: 'Gold Financial Mart', status: 'healthy', statusLabel: 'Sanitized Output (Safe)', type: 'Snowflake Mart Table', description: 'GAAP revenue mart used for board reporting and audit ledger.', owner: '#finance-ops', }, { id: 'node-orders-looker', name: 'Looker ARR & Margin Dashboard', layer: 'consumer', layerLabel: 'Executive BI Dashboard', status: 'warning', statusLabel: '1 Tile Check Advised', type: 'Looker Explore Tile', description: 'C-Suite executive dashboard tracking daily net margin & discount rates.', owner: '#bi-analytics', }, { id: 'node-orders-netsuite', name: 'Finance NetSuite Journal Sync', layer: 'consumer', layerLabel: 'Accounting ERP Pipeline', status: 'healthy', statusLabel: 'Decimal Compatible (Safe)', type: 'Census Batch Sync', description: 'Hourly journal entry creation into Oracle NetSuite ERP general ledger.', owner: '#accounting', }, ], }, { id: 'drift-inc-3', targetTable: 'kafka.events.checkout', environment: 'Kafka Topics', changeType: 'Nullability Relaxation', driftDiff: 'zip_code became nullable', diffMode: 'relaxed', severity: 'low', severityLabel: 'Low', blastRadiusSummary: '0 broken', brokenModelsCount: 0, impactedPipelinesCount: 0, detectedAt: '2h ago', actionLabel: 'Add to Data Contract', isAcknowledged: false, isContractSynced: false, contractYaml: `version: 2 sources: - name: kafka_stream tables: - name: events_checkout columns: - name: session_id data_type: varchar constraints: - type: not_null # Nullability relaxed for international orders lacking postal codes - name: zip_code data_type: varchar(16) constraints: [] # NULLABLE accepted for international territories`, remediationDdl: `-- Event Stream Deserialization Schema Update (Avro/JSON Schema Registry): { "name": "zip_code", "type": ["null", "string"], "default": null, "doc": "Postal code optional for international checkout territories" }`, lineageNodes: [ { id: 'node-kafka-origin', name: 'kafka.events.checkout', layer: 'origin', layerLabel: 'Event Stream Topic', status: 'mutation', statusLabel: 'Nullability Relaxed', type: 'Kafka Partition Stream', description: 'Real-time e-commerce checkout session payload stream.', owner: '#checkout-squad', }, { id: 'node-kafka-flink', name: 'flink.checkout_stream_enrichment', layer: 'transform', layerLabel: 'Flink Stream Processing', status: 'healthy', statusLabel: 'COALESCE Handler Active', type: 'Apache Flink Job', description: 'Streaming enrichment with geo-IP fallback when zip_code is null.', owner: '#stream-eng', runtime: '<500ms streaming', }, { id: 'node-kafka-mart', name: 'analytics.fct_checkout_events', layer: 'mart', layerLabel: 'Realtime Bronze Table', status: 'healthy', statusLabel: 'Schema Compatible', type: 'Snowflake Raw Event Table', description: 'Append-only event log capturing checkout telemetry.', owner: '#analytics-eng', }, { id: 'node-kafka-bi', name: 'Realtime Checkout Conversion Board', layer: 'consumer', layerLabel: 'Operations Monitor', status: 'healthy', statusLabel: 'Active · 0 Failures', type: 'Grafana Live Dashboard', description: 'Ops NOC wallboard monitoring conversion rates per minute.', owner: '#growth-team', }, ], }, ] const props = withDefaults(defineProps<SchemaDriftAlertBoardProps>(), { title: 'Data Warehouse Schema Drift & Anomaly Alerts', monitoredEnvironments: () => ['Snowflake Prod', 'Postgres OLTP', 'Kafka Topics'], activeMutationsCount: 2, }) const incidentsList = ref<SchemaDriftIncident[]>( props.incidents ? JSON.parse(JSON.stringify(props.incidents)) : defaultIncidents, ) const selectedIncidentId = ref<string>('drift-inc-2') const searchQuery = ref('') const selectedEnvironment = ref<string>('all') const selectedSeverity = ref<string>('all') const isMutedNonBreaking = ref(false) const isSyncingAll = ref(false) const allAcknowledged = ref(false) const copiedKey = ref<string | null>(null) const activeTab = ref<'lineage' | 'yaml' | 'sql'>('lineage') const selectedIncident = computed(() => { return incidentsList.value.find((inc) => inc.id === selectedIncidentId.value) ?? incidentsList.value[0] }) const filteredIncidents = computed(() => { const query = searchQuery.value.trim().toLowerCase() return incidentsList.value.filter((item) => { if (isMutedNonBreaking.value && item.severity === 'low') { return false } const matchesEnv = selectedEnvironment.value === 'all' || item.environment === selectedEnvironment.value const matchesSev = selectedSeverity.value === 'all' || item.severity === selectedSeverity.value const matchesQuery = !query || item.targetTable.toLowerCase().includes(query) || item.changeType.toLowerCase().includes(query) || item.driftDiff.toLowerCase().includes(query) || item.environment.toLowerCase().includes(query) return matchesEnv && matchesSev && matchesQuery }) }) const activeMutationsSummary = computed(() => { const unacknowledged = incidentsList.value.filter((i) => !i.isAcknowledged) return `${unacknowledged.length} Active Schema Mutations Detected` }) function handleAcknowledgeAndSync() { if (isSyncingAll.value) return isSyncingAll.value = true setTimeout(() => { incidentsList.value.forEach((item) => { item.isAcknowledged = true item.isContractSynced = true }) isSyncingAll.value = false allAcknowledged.value = true }, 900) } function handleToggleMute() { isMutedNonBreaking.value = !isMutedNonBreaking.value } function handleRowAction(incident: SchemaDriftIncident) { selectedIncidentId.value = incident.id if (incident.changeType === 'Data Type Alteration') { activeTab.value = 'lineage' } else { incident.isContractSynced = !incident.isContractSynced incident.isAcknowledged = true } } function copyCode(key: string, code: string) { if (typeof navigator !== 'undefined' && navigator.clipboard) { navigator.clipboard.writeText(code) copiedKey.value = key setTimeout(() => { if (copiedKey.value === key) { copiedKey.value = null } }, 2000) } } </script> <template> <div data-slot="schema-drift-alert-board" :class="cn('text-foreground w-full space-y-6', props.class)"> <!-- Header Section --> <Card class="border-border bg-card shadow-xs"> <CardContent class="p-5 sm:p-6"> <div class="flex flex-col gap-5 lg:flex-row lg:items-center lg:justify-between"> <!-- Left: Title & Monitored Environments --> <div class="space-y-2"> <div class="flex flex-wrap items-center gap-2"> <div class="border-warning/30 bg-warning/10 text-warning text-warning flex size-9 shrink-0 items-center justify-center rounded-lg border shadow-xs" > <ShieldAlert class="size-4.5" /> </div> <div> <div class="flex flex-wrap items-center gap-2.5"> <h1 class="text-foreground font-mono text-lg font-bold tracking-tight break-all sm:text-xl"> {{ props.title }} </h1> <Badge :variant="allAcknowledged ? 'success' : 'warning'" class="gap-1.5 px-2.5 py-0.5 font-mono text-xs font-semibold whitespace-normal" > <span v-if="!allAcknowledged" class="relative flex size-2"> <span class="bg-warning absolute inline-flex size-full rounded-full opacity-75" /> <span class="bg-warning relative inline-flex size-2 rounded-full" /> </span> <CheckCircle2 v-else class="text-success size-3" /> {{ allAcknowledged ? 'All Contracts Synced' : activeMutationsSummary }} </Badge> </div> <div class="text-muted-foreground flex flex-wrap items-center gap-2 pt-1 text-xs"> <span class="text-muted-foreground font-medium">Monitored Environments:</span> <div class="flex flex-wrap items-center gap-1.5"> <span v-for="env in props.monitoredEnvironments" :key="env" class="border-border bg-muted/60 text-foreground inline-flex items-center gap-1 rounded-md border px-2 py-0.5 font-mono text-xs" > <Database v-if="env.includes('Snowflake')" class="text-info size-3" /> <Layers v-else-if="env.includes('Postgres')" class="text-chart-1 size-3" /> <Activity v-else class="text-warning size-3" /> {{ env }} </span> </div> </div> </div> </div> </div> <!-- Right: Top Action Buttons --> <div class="flex flex-wrap items-center gap-2.5"> <Button variant="outline" size="sm" :class=" cn( 'h-9 gap-1.5 text-xs shadow-xs', isMutedNonBreaking && 'border-warning/40 bg-warning/10 text-warning font-medium', ) " @click="handleToggleMute" > <BellOff v-if="isMutedNonBreaking" class="size-3.5" /> <BellRing v-else class="size-3.5" /> <span>{{ isMutedNonBreaking ? 'Muted (Non-Breaking Hidden)' : 'Mute Non-Breaking Alerts' }}</span> </Button> <Button variant="default" size="sm" class="h-9 gap-1.5 text-xs font-semibold shadow-xs" :disabled="isSyncingAll" @click="handleAcknowledgeAndSync" > <RefreshCw :class="cn('size-3.5', isSyncingAll && 'animate-spin')" /> <Check v-if="allAcknowledged" class="text-success size-3.5" /> <span>{{ isSyncingAll ? 'Syncing Schema Contracts...' : allAcknowledged ? 'Contracts Synced & Locked' : 'Acknowledge & Sync Contracts' }}</span> </Button> </div> </div> </CardContent> <!-- Sub-banner SLA Bar --> <div class="border-border/80 bg-muted/30 text-muted-foreground flex flex-col gap-2 border-t px-5 py-2.5 text-xs sm:flex-row sm:items-center sm:justify-between" > <div class="flex items-center gap-2 font-mono"> <span class="bg-success size-2 rounded-full" /> <span>Realtime Schema Contract Gate: Active (CDC Triggers & dbt Artifact Audits)</span> </div> <div class="flex items-center gap-3"> <span >Detection Engine: <strong class="text-foreground font-mono font-medium">UIPKGE Drift Sentinel v2.4</strong></span > <Separator orientation="vertical" class="hidden h-3 sm:block" /> <span class="font-mono">Checked 14s ago</span> </div> </div> </Card> <!-- 4 Drift Telemetry Metric Cards --> <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"> <!-- Card 1: Active Drift Incidents --> <Card class="border-border bg-card hover:border-border shadow-xs transition-colors"> <CardHeader class="p-4 pb-2"> <div class="flex flex-wrap items-start justify-between gap-2"> <p class="text-muted-foreground text-xs font-medium tracking-wider uppercase">Active Drift Incidents</p> <div class="border-warning/20 bg-warning/10 text-warning flex size-7 items-center justify-center rounded-md border" > <AlertTriangle class="size-4" /> </div> </div> <div class="flex items-baseline gap-2 pt-1"> <span class="text-foreground font-mono text-2xl font-bold tracking-tight tabular-nums" >2 Anomalies Active</span > </div> </CardHeader> <CardContent class="space-y-2 p-4 pt-1"> <div class="bg-muted/40 flex items-center justify-between rounded-md px-2 py-1 text-xs"> <span class="text-muted-foreground">Severity Split</span> <span class="text-foreground font-mono font-medium">1 Warning · 1 Low</span> </div> <p class="text-muted-foreground text-xs">2 Active Schema Mutations Detected</p> </CardContent> </Card> <!-- Card 2: Broken Downstream Models --> <Card class="border-border bg-card hover:border-border shadow-xs transition-colors"> <CardHeader class="p-4 pb-2"> <div class="flex flex-wrap items-start justify-between gap-2"> <p class="text-muted-foreground text-xs font-medium tracking-wider uppercase">Broken Downstream Models</p> <div class="border-success/20 bg-success/10 text-success flex size-7 items-center justify-center rounded-md border" > <ShieldCheck class="size-4" /> </div> </div> <div class="flex items-baseline gap-2 pt-1"> <span class="text-foreground font-mono text-2xl font-bold tracking-tight tabular-nums" >0 Broken Models</span > </div> </CardHeader> <CardContent class="space-y-2 p-4 pt-1"> <div class="bg-muted/40 flex items-center justify-between rounded-md px-2 py-1 text-xs"> <span class="text-muted-foreground">Impacted Pipelines</span> <span class="text-warning font-mono font-medium">2 Impacted Pipelines</span> </div> <p class="text-muted-foreground text-xs">Zero outages · 100% DAG pipeline uptime maintained</p> </CardContent> </Card> <!-- Card 3: Schema Audits in 24h --> <Card class="border-border bg-card hover:border-border shadow-xs transition-colors"> <CardHeader class="p-4 pb-2"> <div class="flex flex-wrap items-start justify-between gap-2"> <p class="text-muted-foreground text-xs font-medium tracking-wider uppercase">Schema Audits in 24h</p> <div class="border-border bg-muted text-foreground flex size-7 items-center justify-center rounded-md border" > <Activity class="text-info size-4" /> </div> </div> <div class="flex items-baseline gap-2 pt-1"> <span class="text-foreground font-mono text-2xl font-bold tracking-tight tabular-nums">1,840</span> <Badge wrap variant="outline" class="font-mono text-xs">100% evaluated</Badge> </div> </CardHeader> <CardContent class="space-y-2 p-4 pt-1"> <Progress :model-value="100" class="h-1.5 w-full" /> <p class="text-muted-foreground text-xs">Continuous CDC & dbt schema contract verification</p> </CardContent> </Card> <!-- Card 4: Mean Time to Acknowledge --> <Card class="border-border bg-card hover:border-border shadow-xs transition-colors"> <CardHeader class="p-4 pb-2"> <div class="flex flex-wrap items-start justify-between gap-2"> <p class="text-muted-foreground text-xs font-medium tracking-wider uppercase">Mean Time to Acknowledge</p> <div class="border-chart-2/20 bg-chart-2/10 text-chart-2 flex size-7 items-center justify-center rounded-md border" > <Clock class="size-4" /> </div> </div> <div class="flex items-baseline gap-2 pt-1"> <span class="text-foreground font-mono text-2xl font-bold tracking-tight tabular-nums">12m</span> <Badge wrap variant="success" class="text-xs font-medium">-4.2m vs 30d</Badge> </div> </CardHeader> <CardContent class="space-y-2 p-4 pt-1"> <div class="bg-muted/40 flex items-center justify-between rounded-md px-2 py-1 text-xs"> <span class="text-muted-foreground">SLA Compliance</span> <span class="text-success font-mono font-medium">98.4% within SLA</span> </div> <p class="text-muted-foreground text-xs">Target < 30m response for Tier-1 Gold Marts</p> </CardContent> </Card> </div> <!-- Schema Drift Incidents Table Card --> <Card class="border-border bg-card shadow-xs"> <CardHeader class="flex flex-col gap-4 pb-3 sm:flex-row sm:items-center sm:justify-between"> <div> <div class="flex items-center gap-2"> <CardTitle class="text-base font-semibold">Schema Drift & Mutation Incidents</CardTitle> <Badge wrap variant="secondary" class="font-mono text-xs"> {{ filteredIncidents.length }} of {{ incidentsList.length }} Tracked </Badge> </div> <CardDescription class="text-xs"> Detected structural schema variances, missing or unmapped columns, type widening, and nullability relaxations. </CardDescription> </div> <!-- Filter Pills by Severity --> <div class="flex flex-wrap items-center gap-1.5"> <Button variant="outline" size="xs" :class=" cn( 'h-7 rounded-full text-xs', selectedSeverity === 'all' && 'border-primary bg-primary text-primary-foreground font-medium', ) " @click="selectedSeverity = 'all'" > All Severities ({{ incidentsList.length }}) </Button> <Button variant="outline" size="xs" :class=" cn( 'h-7 rounded-full text-xs', selectedSeverity === 'medium' && 'border-warning bg-warning font-medium text-white', ) " @click="selectedSeverity = 'medium'" > Medium Warning (1) </Button> <Button variant="outline" size="xs" :class=" cn('h-7 rounded-full text-xs', selectedSeverity === 'low' && 'border-info bg-info font-medium text-white') " @click="selectedSeverity = 'low'" > Low / Non-Breaking (2) </Button> </div> </CardHeader> <!-- Filter Toolbar --> <div class="border-border/60 border-t p-4 pt-3 pb-3"> <div class="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between"> <div class="relative flex-1"> <Search class="text-muted-foreground pointer-events-none absolute top-1/2 left-3 size-3.5 -translate-y-1/2" /> <input v-model="searchQuery" type="text" placeholder="Search table, column, environment, or mutation diff..." class="border-border bg-background placeholder:text-muted-foreground focus-visible:ring-ring h-8.5 w-full rounded-md border px-3 pl-8 text-xs focus-visible:ring-2 focus-visible:outline-none" /> </div> <div class="flex flex-wrap items-center gap-1.5"> <span class="text-muted-foreground mr-1 flex items-center gap-1 text-xs font-medium"> <Filter class="size-3" /> Environment: </span> <Button v-for="env in ['all', 'Snowflake Prod', 'Postgres OLTP', 'Kafka Topics']" :key="env" variant="outline" size="xs" :class=" cn( 'h-7 font-mono text-xs', selectedEnvironment === env && 'border-foreground bg-foreground text-background font-semibold', ) " @click="selectedEnvironment = env" > {{ env === 'all' ? 'All Environments' : env }} </Button> </div> </div> </div> <!-- Table --> <CardContent class="p-0"> <div class="border-border/60 overflow-x-auto border-t"> <Table> <TableHeader class="bg-muted/40"> <TableRow> <TableHead class="min-w-[200px] text-xs font-semibold">Target Entity & Env</TableHead> <TableHead class="min-w-[170px] text-xs font-semibold">Mutation Type</TableHead> <TableHead class="min-w-[260px] text-xs font-semibold">Schema Drift Diff</TableHead> <TableHead class="min-w-[130px] text-xs font-semibold">Severity</TableHead> <TableHead class="min-w-[140px] text-xs font-semibold">Blast Radius</TableHead> <TableHead class="min-w-[90px] text-xs font-semibold">Detected</TableHead> <TableHead class="min-w-[160px] text-right text-xs font-semibold">Action</TableHead> </TableRow> </TableHeader> <TableBody> <TableRow v-for="incident in filteredIncidents" :key="incident.id" :class=" cn( 'hover:bg-muted/30 cursor-pointer transition-colors', selectedIncidentId === incident.id && 'bg-primary/5 ring-primary/20 ring-1', ) " @click="selectedIncidentId = incident.id" > <!-- Target Table & Environment --> <TableCell class="py-3"> <div class="space-y-1"> <div class="text-foreground flex items-center gap-1.5 font-mono text-xs font-bold"> <Database v-if="incident.environment.includes('Snowflake')" class="text-info size-3.5 shrink-0" /> <Layers v-else-if="incident.environment.includes('Postgres')" class="text-chart-1 size-3.5 shrink-0" /> <Activity v-else class="text-warning size-3.5 shrink-0" /> <span>{{ incident.targetTable }}</span> </div> <Badge wrap variant="outline" class="border-border/80 text-muted-foreground font-mono text-xs font-normal" > {{ incident.environment }} </Badge> </div> </TableCell> <!-- Mutation Type --> <TableCell class="py-3"> <span class="text-foreground text-xs font-medium">{{ incident.changeType }}</span> </TableCell> <!-- Schema Drift Diff (Monospace diff representation) --> <TableCell class="py-3"> <div :class=" cn( 'inline-flex items-center gap-1.5 rounded-md border px-2.5 py-1 font-mono text-xs font-medium', incident.diffMode === 'added' && 'border-success/30 bg-success/10 text-success', incident.diffMode === 'altered' && 'border-warning/30 bg-warning/10 text-warning', incident.diffMode === 'relaxed' && 'border-info/30 bg-info/10 text-info', ) " > <code>{{ incident.driftDiff }}</code> </div> </TableCell> <!-- Severity Badge --> <TableCell class="py-3"> <Badge :variant="incident.severity === 'medium' ? 'warning' : 'secondary'" class="gap-1 text-xs font-medium whitespace-normal" > <AlertTriangle v-if="incident.severity === 'medium'" class="text-warning size-3" /> <AlertCircle v-else class="text-info size-3" /> {{ incident.severityLabel }} </Badge> </TableCell> <!-- Blast Radius Summary --> <TableCell class="py-3"> <div class="space-y-0.5"> <span :class=" cn( 'block font-mono text-xs font-medium', incident.impactedPipelinesCount > 0 ? 'text-warning font-semibold' : 'text-success', ) " > {{ incident.blastRadiusSummary }} </span> <span class="text-muted-foreground text-xs"> {{ incident.brokenModelsCount }} broken · {{ incident.impactedPipelinesCount }} impacted </span> </div> </TableCell> <!-- Detected Time --> <TableCell class="text-muted-foreground py-3 font-mono text-xs"> {{ incident.detectedAt }} </TableCell> <!-- Action Button --> <TableCell class="py-3 text-right"> <Button variant="outline" size="xs" :class=" cn( 'h-7 gap-1 text-xs font-medium shadow-xs', incident.isContractSynced && 'border-success/40 bg-success/10 text-success', ) " @click.stop="handleRowAction(incident)" > <Check v-if="incident.isContractSynced" class="size-3" /> <Eye v-else-if="incident.changeType === 'Data Type Alteration'" class="text-primary size-3" /> <Workflow v-else class="size-3" /> <span>{{ incident.isContractSynced ? 'Contract Synced' : incident.actionLabel }}</span> </Button> </TableCell> </TableRow> <TableRow v-if="filteredIncidents.length === 0"> <TableCell colspan="7" class="py-8 text-center"> <div class="flex flex-col items-center justify-center gap-1.5"> <CheckCircle2 class="text-success size-7" /> <p class="text-foreground text-sm font-medium">No schema drift incidents match active filters</p> <p class="text-muted-foreground text-xs"> All active monitored entities adhere strictly to baseline contracts. </p> </div> </TableCell> </TableRow> </TableBody> </Table> </div> <!-- Table Footer --> <div class="border-border/60 bg-muted/20 text-muted-foreground flex flex-col gap-2 border-t px-4 py-2.5 text-xs sm:flex-row sm:items-center sm:justify-between" > <div class="flex items-center gap-2"> <span class="bg-success size-2 rounded-full" /> <span>Click any incident row to trace downstream blast radius lineage and contract YAML.</span> </div> <span class="font-mono text-xs"> Active Incident Focus: <strong class="text-foreground font-bold">{{ selectedIncident.targetTable }}</strong> </span> </div> </CardContent> </Card> <!-- Blast Radius Lineage Tree Card --> <Card class="border-border bg-card shadow-xs"> <CardHeader class="border-border/80 border-b p-4 sm:p-5"> <div class="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between"> <div class="space-y-1"> <div class="flex flex-wrap items-center gap-2"> <Workflow class="text-primary size-5" /> <CardTitle class="text-base font-semibold"> Downstream Blast Radius & Lineage Impact Tree </CardTitle> <Badge wrap variant="outline" class="font-mono text-xs font-semibold"> {{ selectedIncident.targetTable }} </Badge> <Badge :variant="selectedIncident.severity === 'medium' ? 'warning' : 'secondary'" class="font-mono text-xs whitespace-normal" > {{ selectedIncident.changeType }} </Badge> </div> <CardDescription class="text-xs"> Automated topological DAG trace evaluating impacted dbt models, Snowflake views, Looker semantic tiles, and operational syncs. </CardDescription> </div> <!-- View Tabs (Lineage Graph / Contract YAML / Remediation SQL) --> <div class="flex flex-wrap items-center gap-1.5"> <Button variant="outline" size="sm" :class=" cn( 'h-8 gap-1.5 text-xs', activeTab === 'lineage' && 'border-primary bg-primary/10 text-primary font-semibold', ) " @click="activeTab = 'lineage'" > <Network class="size-3.5" /> <span>Lineage Tree</span> </Button> <Button variant="outline" size="sm" :class=" cn( 'h-8 gap-1.5 text-xs', activeTab === 'yaml' && 'border-primary bg-primary/10 text-primary font-semibold', ) " @click="activeTab = 'yaml'" > <FileCode2 class="size-3.5" /> <span>Contract YAML Diff</span> </Button> <Button variant="outline" size="sm" :class=" cn( 'h-8 gap-1.5 text-xs', activeTab === 'sql' && 'border-primary bg-primary/10 text-primary font-semibold', ) " @click="activeTab = 'sql'" > <Terminal class="size-3.5" /> <span>Remediation DDL</span> </Button> </div> </div> </CardHeader> <CardContent class="p-4 sm:p-5"> <!-- TAB 1: Multi-Tier Blast Radius Lineage Tree --> <div v-if="activeTab === 'lineage'" class="space-y-4"> <!-- Summary impact pill strip --> <div class="grid grid-cols-2 gap-2.5 sm:grid-cols-4"> <div class="border-border/80 bg-muted/30 space-y-1 rounded-lg border p-3 text-xs"> <span class="text-muted-foreground block">Mutation Origin</span> <span class="text-foreground block truncate font-mono font-bold">{{ selectedIncident.targetTable }}</span> <span class="text-muted-foreground text-xs">{{ selectedIncident.environment }}</span> </div> <div class="border-border/80 bg-muted/30 space-y-1 rounded-lg border p-3 text-xs"> <span class="text-muted-foreground block">Total Nodes in DAG</span> <span class="text-foreground block font-mono text-base font-bold" >{{ selectedIncident.lineageNodes.length }} Tiers</span > <span class="text-muted-foreground text-xs">Topologically ordered</span> </div> <div class="border-border/80 bg-muted/30 space-y-1 rounded-lg border p-3 text-xs"> <span class="text-muted-foreground block">Impacted Pipelines</span> <span :class=" cn( 'block font-mono text-base font-bold', selectedIncident.impactedPipelinesCount > 0 ? 'text-warning' : 'text-success', ) " > {{ selectedIncident.impactedPipelinesCount }} Models Impacted </span> <span class="text-muted-foreground text-xs">Requires cast adjustment</span> </div> <div class="border-border/80 bg-muted/30 space-y-1 rounded-lg border p-3 text-xs"> <span class="text-muted-foreground block">Broken Models</span> <span class="text-success block font-mono text-base font-bold">0 Outages</span> <span class="text-muted-foreground text-xs">Zero pipeline failures</span> </div> </div> <!-- Interactive Multi-Tier Lineage Flow Nodes --> <div class="border-border bg-muted/10 space-y-3 rounded-xl border p-4"> <div class="border-border/60 flex items-center justify-between border-b pb-2 text-xs"> <span class="text-muted-foreground font-semibold tracking-wider uppercase"> Downstream Dependency DAG Flow (Left to Right) </span> <span class="text-muted-foreground font-mono text-xs"> Active Drift: <code class="text-foreground font-bold">{{ selectedIncident.driftDiff }}</code> </span> </div> <div class="grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-3"> <div v-for="(node, index) in selectedIncident.lineageNodes" :key="node.id" :class=" cn( 'space-y-2 rounded-lg border p-3 text-xs shadow-xs transition-colors', node.status === 'mutation' && 'border-warning/40 bg-warning/5', node.status === 'impacted' && 'border-warning/50 bg-warning/10 ring-warning/30 ring-1', node.status === 'warning' && 'border-yellow-500/40 bg-yellow-500/5', node.status === 'healthy' && 'border-border bg-card', ) " > <!-- Node Header --> <div class="flex flex-wrap items-start justify-between gap-2"> <div class="flex min-w-0 items-center gap-2"> <div :class=" cn( 'flex size-7 shrink-0 items-center justify-center rounded-md border text-xs', node.status === 'mutation' && 'border-warning/30 bg-warning/20 text-warning', node.status === 'impacted' && 'border-warning/40 bg-warning/20 text-warning', node.status === 'warning' && 'border-yellow-500/30 bg-yellow-500/20 text-yellow-600 dark:text-yellow-400', node.status === 'healthy' && 'border-border bg-muted text-foreground', ) " > <Database v-if="node.layer === 'origin'" class="size-3.5" /> <Layers v-else-if="node.layer === 'transform'" class="size-3.5" /> <Table2 v-else-if="node.layer === 'mart'" class="size-3.5" /> <ArrowUpRight v-else class="size-3.5" /> </div> <div class="min-w-0"> <div class="flex items-center gap-1.5"> <span class="text-muted-foreground font-mono text-xs">#{{ index + 1 }}</span> <span class="text-foreground block min-w-0 truncate font-mono font-bold">{{ node.name }}</span> </div> <span class="text-muted-foreground block truncate text-xs">{{ node.layerLabel }}</span> </div> </div> <Badge :variant=" node.status === 'healthy' ? 'outline' : node.status === 'warning' ? 'secondary' : 'warning' " class="shrink-0 font-mono text-xs font-medium whitespace-normal" > <CheckCircle2 v-if="node.status === 'healthy'" class="text-success size-3" /> <AlertTriangle v-else class="size-3" /> {{ node.statusLabel }} </Badge> </div> <!-- Node Description --> <p class="text-muted-foreground text-xs leading-relaxed"> {{ node.description }} </p> <!-- Node Meta Footer --> <div class="border-border/60 text-muted-foreground flex items-center justify-between border-t pt-2 font-mono text-xs" > <span>{{ node.type }}</span> <span>{{ node.owner }}</span> </div> </div> </div> </div> </div> <!-- TAB 2: Contract YAML Diff --> <div v-else-if="activeTab === 'yaml'" class="space-y-3"> <div class="flex items-center justify-between"> <div class="flex items-center gap-2"> <FileCode2 class="text-primary size-4" /> <span class="text-foreground font-mono text-xs font-semibold"> models/contracts/{{ selectedIncident.targetTable.replace('.', '_') }}_contract.yml </span> <Badge wrap variant="secondary" class="font-mono text-xs">dbt v1.8 Contract Definition</Badge> </div> <Button variant="ghost" size="xs" class="h-7 gap-1 text-xs" @click="copyCode('yaml', selectedIncident.contractYaml)" > <Check v-if="copiedKey === 'yaml'" class="text-success size-3" /> <Copy v-else class="size-3" /> <span>{{ copiedKey === 'yaml' ? 'Copied YAML!' : 'Copy YAML Contract' }}</span> </Button> </div> <div class="border-border overflow-x-auto rounded-lg border bg-neutral-950 p-4 font-mono text-xs leading-relaxed text-neutral-100 dark:bg-neutral-950" > <pre class="whitespace-pre"><code>{{ selectedIncident.contractYaml }}</code></pre> </div> </div> <!-- TAB 3: Remediation DDL --> <div v-else-if="activeTab === 'sql'" class="space-y-3"> <div class="flex items-center justify-between"> <div class="flex items-center gap-2"> <Terminal class="text-primary size-4" /> <span class="text-foreground font-mono text-xs font-semibold"> remediation_migration_{{ selectedIncident.targetTable.replace('.', '_') }}.sql </span> <Badge wrap variant="outline" class="font-mono text-xs">Warehouse DDL Patch</Badge> </div> <Button variant="ghost" size="xs" class="h-7 gap-1 text-xs" @click="copyCode('sql', selectedIncident.remediationDdl)" > <Check v-if="copiedKey === 'sql'" class="text-success size-3" /> <Copy v-else class="size-3" /> <span>{{ copiedKey === 'sql' ? 'Copied SQL!' : 'Copy Migration SQL' }}</span> </Button> </div> <div class="border-border overflow-x-auto rounded-lg border bg-neutral-950 p-4 font-mono text-xs leading-relaxed text-neutral-100 dark:bg-neutral-950" > <pre class="whitespace-pre"><code>{{ selectedIncident.remediationDdl }}</code></pre> </div> </div> </CardContent> </Card> </div> </template>
Raw manifest:https://uipkge.dev/r/vue/schema-drift-alert-board.json