
3D Extruded Buildings & Urban Footprints
Real-world 3D building extrusions with dynamic sunlight shadows, terrain DEM elevations, and pitch/bearing camera controls.
Great Expectations and Soda style automated data quality assertions, schema validation, and drift detector dashboard: top header with overall quality score and run triggers, 4 assertion metric cards, data quality assertions table with category filters, and an interactive anomaly inspector drawer with SQL quarantine remediation.
Also available for React ->$pnpm dlx shadcn-vue@latest add https://uipkge.dev/r/vue/data-quality-metrics.json$npx shadcn-vue@latest add https://uipkge.dev/r/vue/data-quality-metrics.json$yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/data-quality-metrics.json$bunx shadcn-vue@latest add https://uipkge.dev/r/vue/data-quality-metrics.jsonnpx shadcn-vue@latest add @uipkge/data-quality-metricsInstalls to:app/components/blocks/| Name | Type / Values | Default | Required |
|---|---|---|---|
datasetTitle | string | 'production_analytics.dim_users' | optional |
suiteName | string | 'user_profile_integrity_v3' | optional |
qualityScore | number | 98.4 | optional |
lastRun | string | 'Ran 14m ago · 48 assertions evaluated' | optional |
passedAssertions | string | '47 / 48 Passing · 98%' | optional |
failedWarnings | string | '1 Warning · Null phone numbers 1.2%' | optional |
schemaDriftStatus | string | '0 breaking schema changes' | optional |
totalRowsAudited | string | '1 | optional |
assertions | AssertionItem[] | — | optional |
class | HTMLAttributes['class'] | — | optional |
Type aliases exported from this item's source. Use these to shape the data you pass in.
AnomalyRecordinterface AnomalyRecord {
recordId: string
column: string
value: string
reason: string
}AssertionIteminterface AssertionItem {
id: string
assertionType: string
columnName: string
category: AssertionCategory
ruleDefinition: string
targetThreshold: string
observedValue: string
observedPercent: number
failedCount: number
totalEvaluated: number
status: AssertionStatus
statusVariant: 'success' | 'warning' | 'destructive'
executionTime: string
expectationConfig: Record<string, unknown>
remediationQuery: string
anomalies: AnomalyRecord[]
}<script setup lang="ts">
import { computed, ref } from 'vue'
import type { HTMLAttributes } from 'vue'
import {
Activity,
AlertCircle,
AlertTriangle,
Check,
CheckCircle2,
Clock,
Copy,
Database,
Download,
FileCode2,
Filter,
GitBranch,
Layers,
RefreshCw,
Search,
ShieldCheck,
} 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 {
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
} from '@/components/ui/sheet'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
export type AssertionStatus = 'Passing' | 'Warning' | 'Failed'
export type AssertionCategory = 'Completeness' | 'Uniqueness' | 'Validity' | 'Volume' | 'Schema'
export interface AnomalyRecord {
recordId: string
column: string
value: string
reason: string
}
export interface AssertionItem {
id: string
assertionType: string
columnName: string
category: AssertionCategory
ruleDefinition: string
targetThreshold: string
observedValue: string
observedPercent: number
failedCount: number
totalEvaluated: number
status: AssertionStatus
statusVariant: 'success' | 'warning' | 'destructive'
executionTime: string
expectationConfig: Record<string, unknown>
remediationQuery: string
anomalies: AnomalyRecord[]
}
export interface DataQualityMetricsProps {
datasetTitle?: string
suiteName?: string
qualityScore?: number
lastRun?: string
passedAssertions?: string
failedWarnings?: string
schemaDriftStatus?: string
totalRowsAudited?: string
assertions?: AssertionItem[]
class?: HTMLAttributes['class']
}
const defaultAssertions: AssertionItem[] = [
{
id: 'dq-assert-1',
assertionType: 'expect_column_values_to_not_be_null',
columnName: 'user_id',
category: 'Completeness',
ruleDefinition: 'Primary key non-null identity constraint verification across all partition shards.',
targetThreshold: '100.0%',
observedValue: '100.0%',
observedPercent: 100.0,
failedCount: 0,
totalEvaluated: 1480000,
status: 'Passing',
statusVariant: 'success',
executionTime: '142ms',
expectationConfig: {
expectation_type: 'expect_column_values_to_not_be_null',
kwargs: {
column: 'user_id',
mostly: 1.0,
},
},
remediationQuery: `-- Primary key check passed cleanly. No quarantine needed.\nSELECT COUNT(*) FROM production_analytics.dim_users WHERE user_id IS NULL;`,
anomalies: [],
},
{
id: 'dq-assert-2',
assertionType: 'expect_column_values_to_be_unique',
columnName: 'email',
category: 'Uniqueness',
ruleDefinition: 'Global case-insensitive deduplication check across customer directory.',
targetThreshold: '100.0%',
observedValue: '100.0%',
observedPercent: 100.0,
failedCount: 0,
totalEvaluated: 1480000,
status: 'Passing',
statusVariant: 'success',
executionTime: '380ms',
expectationConfig: {
expectation_type: 'expect_column_values_to_be_unique',
kwargs: {
column: 'email',
mostly: 1.0,
},
},
remediationQuery: `-- Zero duplicate emails detected in active partition.\nSELECT LOWER(email), COUNT(*) FROM production_analytics.dim_users GROUP BY 1 HAVING COUNT(*) > 1;`,
anomalies: [],
},
{
id: 'dq-assert-3',
assertionType: 'expect_column_values_to_match_regex',
columnName: 'phone_e164',
category: 'Validity',
ruleDefinition: 'International telecommunications ITU-T E.164 format pattern matching (^\\+[1-9]\\d{1,14}$).',
targetThreshold: '≥ 98.0%',
observedValue: '98.8%',
observedPercent: 98.8,
failedCount: 17760,
totalEvaluated: 1480000,
status: 'Warning',
statusVariant: 'warning',
executionTime: '890ms',
expectationConfig: {
expectation_type: 'expect_column_values_to_match_regex',
kwargs: {
column: 'phone_e164',
regex: '^\\+[1-9]\\d{1,14}$',
mostly: 0.98,
},
},
remediationQuery: `-- Quarantine 17,760 legacy unformatted phone numbers for libphonenumber normalization:\nCREATE OR REPLACE TABLE staging.unformatted_phones AS\nSELECT user_id, email, phone_e164, 'MALFORMED_E164' AS failure_reason\nFROM production_analytics.dim_users\nWHERE phone_e164 NOT RLIKE '^\\+[1-9]\\d{1,14}$';`,
anomalies: [
{
recordId: 'usr_9941a8',
column: 'phone_e164',
value: '+1-555-019',
reason: 'Invalid E.164 length (incomplete national number)',
},
{
recordId: 'usr_1082fc',
column: 'phone_e164',
value: '0784910293',
reason: 'Missing international country dial prefix (+)',
},
{
recordId: 'usr_7729de',
column: 'phone_e164',
value: '+44 (0)20 7946',
reason: 'Unstripped parentheses and whitespace delimiter',
},
{
recordId: 'usr_4019ab',
column: 'phone_e164',
value: 'NULL',
reason: 'Unexpected empty field on SMS-enrolled user',
},
],
},
{
id: 'dq-assert-4',
assertionType: 'expect_table_row_count_to_be_between',
columnName: 'table: dim_users',
category: 'Volume',
ruleDefinition: 'Daily snapshot table row count volumetric boundary expectation between 1.0M and 2.0M rows.',
targetThreshold: '1.0M .. 2.0M',
observedValue: '1,480,000',
observedPercent: 100.0,
failedCount: 0,
totalEvaluated: 1480000,
status: 'Passing',
statusVariant: 'success',
executionTime: '95ms',
expectationConfig: {
expectation_type: 'expect_table_row_count_to_be_between',
kwargs: {
min_value: 1000000,
max_value: 2000000,
},
},
remediationQuery: `-- Row count volumetric validation passed within safety boundaries (1.48M in [1.0M, 2.0M]).\nSELECT COUNT(*) AS total_rows FROM production_analytics.dim_users;`,
anomalies: [],
},
{
id: 'dq-assert-5',
assertionType: 'expect_column_values_to_be_between',
columnName: 'age',
category: 'Validity',
ruleDefinition: 'Demographic account age boundary distribution constraint between 18 and 120 years.',
targetThreshold: '100.0%',
observedValue: '100.0%',
observedPercent: 100.0,
failedCount: 0,
totalEvaluated: 1480000,
status: 'Passing',
statusVariant: 'success',
executionTime: '210ms',
expectationConfig: {
expectation_type: 'expect_column_values_to_be_between',
kwargs: {
column: 'age',
min_value: 18,
max_value: 120,
mostly: 1.0,
},
},
remediationQuery: `-- All verified user ages lie cleanly between min 18 and max 94 (100.0% compliant).\nSELECT MIN(age), MAX(age), AVG(age) FROM production_analytics.dim_users;`,
anomalies: [],
},
]
const props = withDefaults(defineProps<DataQualityMetricsProps>(), {
datasetTitle: 'production_analytics.dim_users',
suiteName: 'user_profile_integrity_v3',
qualityScore: 98.4,
lastRun: 'Ran 14m ago · 48 assertions evaluated',
passedAssertions: '47 / 48 Passing · 98%',
failedWarnings: '1 Warning · Null phone numbers 1.2%',
schemaDriftStatus: '0 breaking schema changes',
totalRowsAudited: '1,480,000 rows',
})
const activeAssertions = computed(() => props.assertions ?? defaultAssertions)
const searchQuery = ref('')
const selectedStatus = ref<string>('all')
const selectedCategory = ref<string>('all')
const isRunningSuite = ref(false)
const currentLastRun = ref(props.lastRun)
const selectedAssertion = ref<AssertionItem | null>(null)
const isDrawerOpen = ref(false)
const copiedSnippetKey = ref<string | null>(null)
const exportSuccess = ref(false)
const anomalyExportSuccess = ref(false)
const availableCategories = computed(() => {
const categories = new Set<string>()
activeAssertions.value.forEach((item) => categories.add(item.category))
return ['all', ...Array.from(categories)]
})
const filteredAssertions = computed(() => {
const query = searchQuery.value.trim().toLowerCase()
const statusFilter = selectedStatus.value
const categoryFilter = selectedCategory.value
return activeAssertions.value.filter((item) => {
const matchesStatus = statusFilter === 'all' || item.status.toLowerCase() === statusFilter.toLowerCase()
const matchesCategory = categoryFilter === 'all' || item.category.toLowerCase() === categoryFilter.toLowerCase()
const matchesQuery =
!query ||
item.assertionType.toLowerCase().includes(query) ||
item.columnName.toLowerCase().includes(query) ||
item.ruleDefinition.toLowerCase().includes(query) ||
item.category.toLowerCase().includes(query)
return matchesStatus && matchesCategory && matchesQuery
})
})
function handleRunSuite() {
if (isRunningSuite.value) return
isRunningSuite.value = true
setTimeout(() => {
isRunningSuite.value = false
currentLastRun.value = 'Ran just now · 48 assertions evaluated'
}, 750)
}
function handleInspectAnomalies(assertion: AssertionItem) {
selectedAssertion.value = assertion
isDrawerOpen.value = true
}
function copyCodeSnippet(key: string, content: string) {
if (typeof navigator !== 'undefined' && navigator.clipboard) {
navigator.clipboard.writeText(content)
copiedSnippetKey.value = key
setTimeout(() => {
if (copiedSnippetKey.value === key) {
copiedSnippetKey.value = null
}
}, 2000)
}
}
function exportQualityReport() {
const report = {
dataset: props.datasetTitle,
suite: props.suiteName,
overallQualityScore: props.qualityScore,
evaluatedAt: new Date().toISOString(),
engine: 'Great Expectations v0.18 / SodaCL',
summary: {
passed: props.passedAssertions,
warnings: props.failedWarnings,
schemaDrift: props.schemaDriftStatus,
totalRows: props.totalRowsAudited,
},
assertions: activeAssertions.value,
}
const jsonStr = 'data:text/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(report, null, 2))
if (typeof document !== 'undefined') {
const link = document.createElement('a')
link.setAttribute('href', jsonStr)
link.setAttribute('download', `dq-report-${props.suiteName}-${new Date().toISOString().slice(0, 10)}.json`)
document.body.appendChild(link)
link.click()
link.remove()
exportSuccess.value = true
setTimeout(() => {
exportSuccess.value = false
}, 2000)
}
}
function exportAnomalyCsv() {
if (!selectedAssertion.value || selectedAssertion.value.anomalies.length === 0) return
const headers = ['Record ID', 'Column', 'Observed Value', 'Failure Reason']
const rows = selectedAssertion.value.anomalies.map((a) => [a.recordId, a.column, `"${a.value}"`, `"${a.reason}"`])
const csvContent = [headers.join(','), ...rows.map((r) => r.join(','))].join('\n')
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.setAttribute('href', url)
link.setAttribute('download', `anomalies-${selectedAssertion.value.columnName}.csv`)
document.body.appendChild(link)
link.click()
link.remove()
anomalyExportSuccess.value = true
setTimeout(() => {
anomalyExportSuccess.value = false
}, 2000)
}
</script>
<template>
<div data-slot="data-quality-metrics" :class="cn('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-6 lg:flex-row lg:items-center lg:justify-between">
<!-- Left: Dataset and Suite Metadata -->
<div class="space-y-2">
<div class="flex flex-wrap items-center gap-2.5">
<div
class="bg-muted text-foreground border-border flex size-9 shrink-0 items-center justify-center rounded-lg border shadow-xs"
>
<Database class="text-success size-4.5" />
</div>
<div>
<div class="flex flex-wrap items-center gap-2">
<h1 class="text-foreground font-mono text-lg font-bold tracking-tight break-all sm:text-xl">
{{ props.datasetTitle }}
</h1>
<Badge wrap variant="secondary" class="font-mono text-xs">
{{ props.suiteName }}
</Badge>
</div>
<div class="text-muted-foreground flex flex-wrap items-center gap-2 pt-1 text-xs">
<span class="flex items-center gap-1">
<Clock class="size-3.5" />
{{ currentLastRun }}
</span>
<span class="opacity-40">·</span>
<span class="font-mono">Snowflake DW</span>
<span class="opacity-40">·</span>
<span class="font-mono">dbt Core v1.8</span>
</div>
</div>
</div>
</div>
<!-- Right: Overall Score Card & Action Buttons -->
<div class="flex flex-col gap-4 sm:flex-row sm:items-center">
<!-- Overall Score Pill Card -->
<div
class="border-success/30 bg-success/10 flex flex-wrap items-center gap-3.5 rounded-xl border p-3.5 shadow-xs"
>
<div class="bg-success/20 text-success flex size-11 shrink-0 items-center justify-center rounded-lg">
<CheckCircle2 class="size-6" />
</div>
<div class="min-w-0 space-y-0.5">
<div class="flex flex-wrap items-baseline gap-2">
<span class="text-success text-success text-2xl font-bold tracking-tight tabular-nums sm:text-3xl">
{{ props.qualityScore }}%
</span>
<Badge wrap variant="success" class="gap-1 px-2 text-xs font-semibold">
<ShieldCheck class="size-3" />
Quality Score
</Badge>
</div>
<p class="text-muted-foreground text-xs">Automated Sodacl / GX Engine</p>
</div>
</div>
<!-- Action Buttons -->
<div class="flex flex-wrap gap-2.5 sm:flex-col">
<Button
variant="default"
size="sm"
class="gap-2 shadow-xs"
:disabled="isRunningSuite"
@click="handleRunSuite"
>
<RefreshCw :class="['size-4', isRunningSuite ? 'animate-spin' : '']" />
<span>{{ isRunningSuite ? 'Evaluating Suite...' : 'Run Assertions Suite' }}</span>
</Button>
<Button variant="outline" size="sm" class="gap-2 shadow-xs" @click="exportQualityReport">
<Check v-if="exportSuccess" class="text-success size-4" />
<Download v-else class="size-4" />
<span>{{ exportSuccess ? 'Report Downloaded!' : 'Export Quality Report' }}</span>
</Button>
</div>
</div>
</div>
</CardContent>
<!-- Sub-bar Contract Guarantee -->
<div
class="bg-muted/40 border-border 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>Schema Contract: Locked v3.2 · Zero Breaking Schema Drift</span>
</div>
<div class="flex items-center gap-3">
<span>Engine SLA: <strong class="text-foreground font-medium">Strict Production</strong></span>
<Separator orientation="vertical" class="hidden h-3 sm:block" />
<span>48 assertions evaluated</span>
</div>
</div>
</Card>
<!-- 4 Assertion Metric Cards -->
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<!-- Card 1: Passed Assertions -->
<Card class="border-border bg-card hover:border-border shadow-xs transition-colors">
<CardHeader class="p-4 pb-2">
<div class="flex items-start justify-between gap-2">
<p class="text-muted-foreground text-xs font-medium tracking-wider uppercase">Passed Assertions</p>
<div
class="border-success/20 bg-success/10 text-success flex size-7 items-center justify-center rounded-md border"
>
<CheckCircle2 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">47 / 48</span>
<Badge wrap variant="success" class="text-xs font-medium">98% Passing</Badge>
</div>
</CardHeader>
<CardContent class="space-y-2 p-4 pt-1">
<Progress :model-value="98" class="h-1.5 w-full" />
<p class="text-muted-foreground text-xs">{{ props.passedAssertions }}</p>
</CardContent>
</Card>
<!-- Card 2: Failed / Warnings -->
<Card class="border-border bg-card hover:border-border shadow-xs transition-colors">
<CardHeader class="p-4 pb-2">
<div class="flex items-start justify-between gap-2">
<p class="text-muted-foreground text-xs font-medium tracking-wider uppercase">Failed / Warnings</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">1 Warning</span>
<Badge wrap variant="warning" class="text-xs font-medium">1.2% Non-conformant</Badge>
</div>
</CardHeader>
<CardContent class="space-y-2 p-4 pt-1">
<Progress :model-value="1.2" class="h-1.5 w-full" />
<p class="text-muted-foreground text-xs">{{ props.failedWarnings }}</p>
</CardContent>
</Card>
<!-- Card 3: Schema Drift Status -->
<Card class="border-border bg-card hover:border-border shadow-xs transition-colors">
<CardHeader class="p-4 pb-2">
<div class="flex items-start justify-between gap-2">
<p class="text-muted-foreground text-xs font-medium tracking-wider uppercase">Schema Drift Status</p>
<div class="border-info/20 bg-info/10 text-info flex size-7 items-center justify-center rounded-md border">
<GitBranch 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 Breaking</span>
<Badge wrap variant="outline" class="text-success text-xs font-medium"> No Drift </Badge>
</div>
</CardHeader>
<CardContent class="space-y-2 p-4 pt-1">
<div class="bg-muted/40 flex items-center justify-between gap-x-2 rounded-md px-2 py-1 text-xs">
<span class="text-muted-foreground">Columns Synced</span>
<span class="text-foreground font-mono font-medium">14 / 14 matched</span>
</div>
<p class="text-muted-foreground text-xs">{{ props.schemaDriftStatus }}</p>
</CardContent>
</Card>
<!-- Card 4: Total Rows Audited -->
<Card class="border-border bg-card hover:border-border shadow-xs transition-colors">
<CardHeader class="p-4 pb-2">
<div class="flex items-start justify-between gap-2">
<p class="text-muted-foreground text-xs font-medium tracking-wider uppercase">Total Rows Audited</p>
<div
class="bg-muted text-foreground border-border flex size-7 items-center justify-center rounded-md border"
>
<Layers 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">1,480,000</span>
<Badge wrap variant="outline" class="font-mono text-xs font-normal">1.48M Rows</Badge>
</div>
</CardHeader>
<CardContent class="space-y-2 p-4 pt-1">
<div class="bg-muted/40 flex items-center justify-between gap-x-2 rounded-md px-2 py-1 text-xs">
<span class="text-muted-foreground">Partition Range</span>
<span class="text-foreground font-mono font-medium">1.0M .. 2.0M</span>
</div>
<p class="text-muted-foreground text-xs">{{ props.totalRowsAudited }}</p>
</CardContent>
</Card>
</div>
<!-- Data Quality Test Assertions Table Card -->
<Card class="border-border bg-card shadow-xs">
<CardHeader class="flex flex-col gap-4 pb-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<div class="flex items-center gap-2">
<CardTitle class="text-base font-semibold">Data Quality Test Assertions</CardTitle>
<Badge wrap variant="secondary" class="font-mono text-xs">
{{ filteredAssertions.length }} of {{ activeAssertions.length }} Assertions
</Badge>
</div>
<CardDescription class="text-xs">
Automated expectations, boundary validations, regex format rules, and volume anomaly checks.
</CardDescription>
</div>
<!-- Filter Pills by Status -->
<div class="flex flex-wrap items-center gap-1.5">
<button
type="button"
:class="[
'focus-visible:ring-ring inline-flex items-center rounded-full border px-2.5 py-1 text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none',
selectedStatus === 'all'
? 'border-primary bg-primary text-primary-foreground shadow-xs'
: 'border-border bg-card text-muted-foreground hover:bg-accent hover:text-foreground',
]"
@click="selectedStatus = 'all'"
>
All Assertions ({{ activeAssertions.length }})
</button>
<button
type="button"
:class="[
'focus-visible:ring-ring inline-flex items-center rounded-full border px-2.5 py-1 text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none',
selectedStatus === 'passing'
? 'border-success bg-success bg-success text-white shadow-xs'
: 'border-border bg-card text-muted-foreground hover:bg-accent hover:text-foreground',
]"
@click="selectedStatus = 'passing'"
>
Passing (4)
</button>
<button
type="button"
:class="[
'focus-visible:ring-ring inline-flex items-center rounded-full border px-2.5 py-1 text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none',
selectedStatus === 'warning'
? 'border-warning bg-warning bg-warning text-white shadow-xs'
: 'border-border bg-card text-muted-foreground hover:bg-accent hover:text-foreground',
]"
@click="selectedStatus = 'warning'"
>
Warnings (1)
</button>
</div>
</CardHeader>
<!-- Filter & Search 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 by assertion type, column name, or rule definition..."
class="border-border bg-background placeholder:text-muted-foreground focus-visible:ring-ring h-8 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" />
Category:
</span>
<button
v-for="cat in availableCategories"
:key="cat"
type="button"
:class="[
'focus-visible:ring-ring inline-flex items-center rounded-md border px-2 py-1 font-mono text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none',
selectedCategory === cat
? 'border-foreground bg-foreground text-background shadow-xs'
: 'border-border bg-card text-muted-foreground hover:bg-accent hover:text-foreground',
]"
@click="selectedCategory = cat"
>
{{ cat === 'all' ? 'All Categories' : cat }}
</button>
</div>
</div>
</div>
<!-- Table Container -->
<CardContent class="p-0">
<div class="border-border/60 overflow-x-auto border-t">
<Table>
<TableHeader class="bg-muted/50">
<TableRow>
<TableHead class="min-w-[260px] text-xs">Assertion & Target Column</TableHead>
<TableHead class="min-w-[100px] text-xs">Category</TableHead>
<TableHead class="min-w-[120px] text-xs">Target Threshold</TableHead>
<TableHead class="min-w-[120px] text-xs">Observed Value</TableHead>
<TableHead class="min-w-[110px] text-xs">Status</TableHead>
<TableHead class="min-w-[130px] text-right text-xs">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow
v-for="assertion in filteredAssertions"
:key="assertion.id"
class="hover:bg-muted/20 transition-colors"
>
<!-- Assertion Type & Target Column -->
<TableCell class="py-3">
<div class="space-y-1">
<div class="flex flex-wrap items-center gap-2">
<Badge wrap variant="outline" class="border-border font-mono text-xs font-semibold">
{{ assertion.columnName }}
</Badge>
<span class="text-foreground font-mono text-xs font-medium">{{ assertion.assertionType }}</span>
</div>
<p class="text-muted-foreground max-w-sm text-xs leading-relaxed sm:max-w-md">
{{ assertion.ruleDefinition }}
</p>
</div>
</TableCell>
<!-- Category -->
<TableCell class="py-3">
<Badge wrap variant="secondary" class="font-normal">
{{ assertion.category }}
</Badge>
</TableCell>
<!-- Target Threshold -->
<TableCell class="py-3">
<span class="text-muted-foreground font-mono text-xs font-medium">
{{ assertion.targetThreshold }}
</span>
</TableCell>
<!-- Observed Value -->
<TableCell class="py-3">
<div class="space-y-0.5 font-mono text-xs">
<span
:class="[
'font-bold tabular-nums',
assertion.status === 'Passing' ? 'text-success' : 'text-warning',
]"
>
{{ assertion.observedValue }}
</span>
<span class="text-muted-foreground/70 block text-xs"> {{ assertion.executionTime }} eval </span>
</div>
</TableCell>
<!-- Status Badge -->
<TableCell class="py-3">
<Badge wrap :variant="assertion.statusVariant" class="gap-1 text-xs font-medium">
<CheckCircle2 v-if="assertion.status === 'Passing'" class="size-3" />
<AlertTriangle v-else-if="assertion.status === 'Warning'" class="size-3" />
<AlertCircle v-else class="size-3" />
{{ assertion.status }}
</Badge>
</TableCell>
<!-- Action Button -->
<TableCell class="py-3 text-right">
<Button
variant="outline"
size="xs"
class="h-7 gap-1 text-xs font-medium shadow-xs"
@click="handleInspectAnomalies(assertion)"
>
<Activity class="size-3" />
<span>Inspect Anomalies</span>
</Button>
</TableCell>
</TableRow>
<TableRow v-if="filteredAssertions.length === 0">
<TableCell colspan="6" class="py-8 text-center">
<div class="flex flex-col items-center justify-center gap-1.5">
<ShieldCheck class="text-success size-7" />
<p class="text-foreground text-sm font-medium">No assertions match your filter query</p>
<p class="text-muted-foreground text-xs">All assertions meet standard quality constraints.</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>Great Expectations Assertions Runner: 100% evaluated</span>
</div>
<span class="font-mono text-xs"> Dataset: {{ props.datasetTitle }} · Last Batch: 1,480,000 rows </span>
</div>
</CardContent>
</Card>
<!-- Anomaly Inspector Drawer / Sheet -->
<Sheet v-model:open="isDrawerOpen">
<SheetContent class="w-full space-y-6 overflow-y-auto p-6 sm:max-w-xl md:max-w-2xl">
<SheetHeader v-if="selectedAssertion" class="space-y-2 p-0 text-left">
<div class="flex items-center justify-between gap-3 pr-6">
<div class="flex items-center gap-2">
<Badge wrap :variant="selectedAssertion.statusVariant" class="gap-1 font-mono text-xs font-semibold">
<CheckCircle2 v-if="selectedAssertion.status === 'Passing'" class="size-3" />
<AlertTriangle v-else class="size-3" />
{{ selectedAssertion.status.toUpperCase() }}
</Badge>
<Badge wrap variant="outline" class="font-mono text-xs font-semibold">
{{ selectedAssertion.category }}
</Badge>
</div>
<span class="text-muted-foreground font-mono text-xs">
Latency: {{ selectedAssertion.executionTime }}
</span>
</div>
<SheetTitle class="text-lg font-bold tracking-tight">
{{ selectedAssertion.assertionType }}
</SheetTitle>
<SheetDescription class="text-foreground/90 font-mono text-xs">
Target Column: <strong class="text-foreground font-semibold">{{ selectedAssertion.columnName }}</strong>
</SheetDescription>
</SheetHeader>
<div v-if="selectedAssertion" class="space-y-5 text-xs">
<!-- Summary Metrics Cards Grid -->
<div class="grid grid-cols-2 gap-2.5 sm:grid-cols-4">
<div class="bg-muted/40 border-border/80 rounded-lg border p-3">
<span class="text-muted-foreground text-xs">Target Threshold</span>
<p class="text-foreground mt-1 font-mono text-sm font-bold">{{ selectedAssertion.targetThreshold }}</p>
</div>
<div class="bg-muted/40 border-border/80 rounded-lg border p-3">
<span class="text-muted-foreground text-xs">Observed Pass Rate</span>
<p
:class="[
'mt-1 font-mono text-sm font-bold tabular-nums',
selectedAssertion.status === 'Passing' ? 'text-success' : 'text-warning',
]"
>
{{ selectedAssertion.observedValue }}
</p>
</div>
<div class="bg-muted/40 border-border/80 rounded-lg border p-3">
<span class="text-muted-foreground text-xs">Audited Records</span>
<p class="text-foreground mt-1 font-mono text-sm font-bold tabular-nums">
{{ selectedAssertion.totalEvaluated.toLocaleString() }}
</p>
</div>
<div class="bg-muted/40 border-border/80 rounded-lg border p-3">
<span class="text-muted-foreground text-xs">Failed Anomalies</span>
<p
:class="[
'mt-1 font-mono text-sm font-bold tabular-nums',
selectedAssertion.failedCount > 0 ? 'text-warning' : 'text-success',
]"
>
{{ selectedAssertion.failedCount.toLocaleString() }}
</p>
</div>
</div>
<!-- Expectation Configuration DSL Box -->
<div class="space-y-2">
<div class="flex items-center justify-between gap-x-2">
<h3 class="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
Great Expectations Specification
</h3>
<Button
variant="ghost"
size="xs"
class="h-6 gap-1 px-2 text-xs"
@click="copyCodeSnippet('dsl', JSON.stringify(selectedAssertion.expectationConfig, null, 2))"
>
<Check v-if="copiedSnippetKey === 'dsl'" class="text-success size-3" />
<Copy v-else class="size-3" />
<span>{{ copiedSnippetKey === 'dsl' ? 'Copied' : 'Copy Config' }}</span>
</Button>
</div>
<div class="overflow-hidden rounded-md border bg-zinc-950 font-mono text-xs text-zinc-100 shadow-inner">
<div
class="text-muted-foreground flex items-center gap-2 border-b border-zinc-800 bg-zinc-900/90 px-3 py-1.5 text-xs"
>
<FileCode2 class="size-3.5" />
<span>expectation_suite.json</span>
</div>
<pre class="overflow-x-auto p-3 text-xs leading-relaxed text-zinc-300 select-text">{{
JSON.stringify(selectedAssertion.expectationConfig, null, 2)
}}</pre>
</div>
</div>
<!-- Anomalous Samples Table or Clean Pass State -->
<div class="space-y-2">
<div class="flex items-center justify-between gap-x-2">
<h3 class="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
Anomalous Sample Records
</h3>
<span v-if="selectedAssertion.anomalies.length > 0" class="text-muted-foreground font-mono text-xs">
Showing {{ selectedAssertion.anomalies.length }} failure samples
</span>
</div>
<div
v-if="selectedAssertion.anomalies.length > 0"
class="border-border/80 overflow-hidden rounded-md border shadow-xs"
>
<Table>
<TableHeader class="bg-muted/40">
<TableRow>
<TableHead class="text-xs">Record ID</TableHead>
<TableHead class="text-xs">Column</TableHead>
<TableHead class="text-xs">Observed Value</TableHead>
<TableHead class="text-xs">Diagnostic Reason</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="anomaly in selectedAssertion.anomalies" :key="anomaly.recordId">
<TableCell class="font-mono text-xs font-medium">{{ anomaly.recordId }}</TableCell>
<TableCell class="font-mono text-xs">{{ anomaly.column }}</TableCell>
<TableCell class="text-destructive font-mono text-xs font-semibold">
{{ anomaly.value }}
</TableCell>
<TableCell class="text-muted-foreground text-xs">{{ anomaly.reason }}</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
<div
v-else
class="bg-muted/20 border-border/80 flex flex-col items-center justify-center gap-1.5 rounded-md border p-6 text-center"
>
<CheckCircle2 class="text-success size-6" />
<p class="text-foreground text-xs font-semibold">Zero Anomalies Detected</p>
<p class="text-muted-foreground text-xs">
All {{ selectedAssertion.totalEvaluated.toLocaleString() }} audited records strictly satisfied the
expectation rule.
</p>
</div>
</div>
<!-- Quarantine SQL Script -->
<div class="space-y-2">
<div class="flex items-center justify-between gap-x-2">
<h3 class="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
Quarantine & Remediation SQL
</h3>
<Button
variant="ghost"
size="xs"
class="h-6 gap-1 px-2 text-xs"
@click="copyCodeSnippet('sql', selectedAssertion.remediationQuery)"
>
<Check v-if="copiedSnippetKey === 'sql'" class="text-success size-3" />
<Copy v-else class="size-3" />
<span>{{ copiedSnippetKey === 'sql' ? 'Copied' : 'Copy SQL' }}</span>
</Button>
</div>
<div class="border-border bg-muted/60 rounded-md border p-3 font-mono text-xs leading-relaxed select-text">
<pre class="text-foreground overflow-x-auto whitespace-pre-wrap">{{
selectedAssertion.remediationQuery
}}</pre>
</div>
</div>
</div>
<SheetFooter
v-if="selectedAssertion"
class="border-border/80 flex flex-row items-center justify-end gap-2 border-t pt-4"
>
<SheetClose as-child>
<Button variant="outline" size="sm" class="text-xs"> Close </Button>
</SheetClose>
<Button
v-if="selectedAssertion.anomalies.length > 0"
variant="default"
size="sm"
class="gap-1.5 text-xs font-medium shadow-xs"
@click="exportAnomalyCsv"
>
<Check v-if="anomalyExportSuccess" class="text-success size-3.5" />
<Download v-else class="size-3.5" />
<span>{{ anomalyExportSuccess ? 'Anomalies Exported!' : 'Export Anomaly CSV' }}</span>
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
</div>
</template>
Raw manifest:https://uipkge.dev/r/vue/data-quality-metrics.json