f36c8906bc
Adds native Command Center features (no new containers) integrated as sub-tabs in the existing Data Explorer and Data Quality views: - Continuous Data Quality (dq_monitor.py): live completeness/uniqueness/validity/ freshness scorecards via Trino with rolling trends → DataQuality "Live Monitoring". - Ownership & stewardship (catalog_governance.py): owner/steward/tier matrix, orphan detection, business glossary; local store best-effort synced to OpenMetadata (owner PATCH) → Data Explorer "Ownership". - Access & policy posture: per-dataset compliance combining PII masking, ownership, live DQ and observability alerts vs data contracts → Data Explorer "Access & Policies". - Lineage (lineage.py): staged source→CDC→Spark→S3→Iceberg→Trino→serving graph with live row counts and column-level PII/masking tracing → Data Explorer "Lineage". - Observability (observability.py): volume/freshness/schema-drift monitoring with alerts → Data Explorer "Observability". - Shared lake_meta.py dataset registry + bounded Trino client; fast native row-count and PK-indexed freshness so monitors stay cheap on 25-54M-row tables. - LLM context (lab_context.py) enriched with DQ scores, ownership and active alerts.
118 lines
6.2 KiB
TypeScript
118 lines
6.2 KiB
TypeScript
import { useCallback, useEffect, useState } from 'react'
|
|
import { ShieldCheck, RefreshCw, Loader2, Lock, Unlock, Check, X, FileCheck2, AlertTriangle } from 'lucide-react'
|
|
import { cn } from '../../lib/utils'
|
|
|
|
type Check = { name: string; ok: boolean; value: unknown; target: unknown }
|
|
type Pii = { pii_count: number; all_masked: boolean; masked: number; unmasked: number }
|
|
type Alert = { type: string; severity: string; message: string }
|
|
type Posture = {
|
|
key: string; label: string; engine: string; color: string; table: string
|
|
owner?: string | null; steward?: string | null; tier?: string | null
|
|
pii: Pii; dq_score?: number | null; issues: string[]; alerts: Alert[]
|
|
contract: Record<string, number>; checks: Check[]; compliant: boolean
|
|
}
|
|
type Resp = { ok: boolean; datasets: Posture[]; summary: { total: number; compliant: number; non_compliant: number } }
|
|
|
|
export function GovernanceAccessView() {
|
|
const [data, setData] = useState<Resp | null>(null)
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
const load = useCallback(async () => {
|
|
setLoading(true)
|
|
try {
|
|
const r = await fetch('/api/governance/posture')
|
|
if (r.ok) setData(await r.json())
|
|
} catch { /* */ } finally {
|
|
setLoading(false)
|
|
}
|
|
}, [])
|
|
|
|
useEffect(() => { load() }, [load])
|
|
useEffect(() => { const t = setInterval(load, 12000); return () => clearInterval(t) }, [load])
|
|
|
|
const sev = (s: string) => (s === 'critical' ? 'text-rose-400' : s === 'warning' ? 'text-amber-400' : 'text-sky-400')
|
|
|
|
return (
|
|
<div className="scrollbar-thin flex h-full min-h-0 flex-col gap-2 overflow-y-auto">
|
|
<div className="grid shrink-0 grid-cols-2 gap-2 md:grid-cols-4">
|
|
<Kpi icon={FileCheck2} label="Compliant datasets" value={data ? `${data.summary.compliant}/${data.summary.total}` : '—'}
|
|
accent={data && data.summary.non_compliant === 0 ? '#34d399' : '#fbbf24'} />
|
|
<Kpi icon={AlertTriangle} label="Non-compliant" value={data ? String(data.summary.non_compliant) : '—'}
|
|
accent={data && data.summary.non_compliant ? '#f87171' : '#34d399'} />
|
|
<Kpi icon={Lock} label="Masking policy" value="Enforced" accent="#60a5fa" />
|
|
<Kpi icon={ShieldCheck} label="Contracts" value="Active" accent="#a78bfa" />
|
|
</div>
|
|
|
|
<div className="flex shrink-0 items-center px-1">
|
|
<span className="text-[10px] text-foreground-faint">Governance posture = ownership + PII masking + live DQ + observability alerts vs each data contract</span>
|
|
<button type="button" onClick={load} className="ml-auto inline-flex items-center gap-1.5 rounded-md border border-border px-2.5 py-1 text-[10px] text-foreground-muted hover:bg-surface-overlay">
|
|
{loading ? <Loader2 className="h-3 w-3 animate-spin" /> : <RefreshCw className="h-3 w-3" />} Refresh
|
|
</button>
|
|
</div>
|
|
|
|
<div className="grid gap-2 pb-2 lg:grid-cols-2">
|
|
{(data?.datasets || []).map((d) => (
|
|
<div key={d.key} className={cn('panel p-3', !d.compliant && 'ring-1 ring-rose-500/30')}>
|
|
<div className="mb-2 flex items-center gap-2">
|
|
<span className="h-2.5 w-2.5 rounded-full" style={{ backgroundColor: d.color }} />
|
|
<span className="text-[12px] font-semibold text-foreground">{d.label}</span>
|
|
<span className={cn('ml-auto inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[9px] font-medium',
|
|
d.compliant ? 'bg-emerald-500/15 text-emerald-300' : 'bg-rose-500/15 text-rose-300')}>
|
|
{d.compliant ? <Check className="h-3 w-3" /> : <X className="h-3 w-3" />} {d.compliant ? 'Compliant' : 'Action needed'}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-1.5">
|
|
{d.checks.map((c) => (
|
|
<div key={c.name} className="flex items-center gap-1.5 rounded bg-surface-overlay px-2 py-1 text-[10px]">
|
|
{c.ok ? <Check className="h-3 w-3 shrink-0 text-emerald-400" /> : <X className="h-3 w-3 shrink-0 text-rose-400" />}
|
|
<span className="flex-1 truncate text-foreground-muted">{c.name}</span>
|
|
<span className={cn('font-mono', c.ok ? 'text-foreground' : 'text-rose-300')}>{String(c.value)}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="mt-2 flex flex-wrap items-center gap-2 text-[9px] text-foreground-muted">
|
|
<span>Owner: <span className="text-foreground">{d.owner || '—'}</span></span>
|
|
<span>· Tier: {d.tier || '—'}</span>
|
|
<span className="flex items-center gap-1">·
|
|
{d.pii.unmasked === 0
|
|
? <><Lock className="h-3 w-3 text-emerald-400" /> {d.pii.masked}/{d.pii.pii_count} PII masked</>
|
|
: <><Unlock className="h-3 w-3 text-amber-400" /> {d.pii.unmasked} PII visible</>}
|
|
</span>
|
|
</div>
|
|
|
|
{d.alerts.length > 0 && (
|
|
<div className="mt-2 space-y-1 border-t border-border/50 pt-2">
|
|
{d.alerts.map((a, i) => (
|
|
<p key={i} className={cn('flex items-center gap-1 text-[9px]', sev(a.severity))}>
|
|
<AlertTriangle className="h-3 w-3" /> {a.message}
|
|
</p>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
<p className="mt-2 text-[8px] text-foreground-faint">
|
|
Contract: DQ ≥ {d.contract.min_score} · completeness ≥ {d.contract.min_completeness}% · freshness ≤ {d.contract.min_freshness_min}m · crit alerts ≤ {d.contract.max_critical_alerts}
|
|
</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function Kpi({ icon: Icon, label, value, accent }: { icon: typeof ShieldCheck; label: string; value: string; accent: string }) {
|
|
return (
|
|
<div className="panel flex items-center gap-3 px-3 py-2.5">
|
|
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg" style={{ backgroundColor: `${accent}1f`, color: accent }}>
|
|
<Icon className="h-4 w-4" />
|
|
</div>
|
|
<div className="min-w-0">
|
|
<p className="text-[9px] font-semibold uppercase tracking-wider text-foreground-muted">{label}</p>
|
|
<p className="truncate text-lg font-bold leading-tight text-foreground">{value}</p>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|