|
|
|
@@ -37,8 +37,36 @@ type ColumnProfile = {
|
|
|
|
|
top_values?: { value: string; count: number }[]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type GxCheck = { suite: string; expectation: string; success: boolean; result: string; column?: string }
|
|
|
|
|
type SodaCheck = { suite: string; name: string; check: string; outcome: string; detail: string }
|
|
|
|
|
type Violations = {
|
|
|
|
|
violation_count?: number
|
|
|
|
|
violation_pct?: number
|
|
|
|
|
sample_rows?: { row_index: number; values: Record<string, unknown> }[]
|
|
|
|
|
location_hint?: string
|
|
|
|
|
affected_columns?: string[]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type DqCheck = {
|
|
|
|
|
id?: string
|
|
|
|
|
suite: string
|
|
|
|
|
success?: boolean
|
|
|
|
|
status?: string
|
|
|
|
|
outcome?: string
|
|
|
|
|
expectation?: string
|
|
|
|
|
name?: string
|
|
|
|
|
metric?: string
|
|
|
|
|
check?: string
|
|
|
|
|
test?: string
|
|
|
|
|
rule?: string
|
|
|
|
|
monitor?: string
|
|
|
|
|
result?: string
|
|
|
|
|
detail?: string
|
|
|
|
|
column?: string
|
|
|
|
|
violations?: Violations
|
|
|
|
|
meta?: Record<string, unknown>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type GxCheck = DqCheck & { expectation: string; success: boolean; result: string }
|
|
|
|
|
type SodaCheck = DqCheck & { name: string; check: string; outcome: string; detail: string }
|
|
|
|
|
|
|
|
|
|
type DocStructure = {
|
|
|
|
|
pages: number
|
|
|
|
@@ -67,10 +95,24 @@ type AssessResult = {
|
|
|
|
|
dimensions: Dimension[]
|
|
|
|
|
column_profiles: ColumnProfile[]
|
|
|
|
|
action_items: { priority: string; dimension: string; score: number; action: string }[]
|
|
|
|
|
checks?: { great_expectations: GxCheck[]; soda_core: SodaCheck[] }
|
|
|
|
|
checks_summary: {
|
|
|
|
|
great_expectations: { total: number; passed: number }
|
|
|
|
|
soda_core: { total: number; warnings: number }
|
|
|
|
|
checks?: {
|
|
|
|
|
great_expectations: GxCheck[]
|
|
|
|
|
soda_core: SodaCheck[]
|
|
|
|
|
deequ?: DqCheck[]
|
|
|
|
|
pandera?: DqCheck[]
|
|
|
|
|
dbt_expectations?: DqCheck[]
|
|
|
|
|
monte_carlo?: DqCheck[]
|
|
|
|
|
affirm?: DqCheck[]
|
|
|
|
|
}
|
|
|
|
|
checks_summary: Record<string, { total: number; passed?: number; warnings?: number }>
|
|
|
|
|
executive_summary?: {
|
|
|
|
|
headline: string
|
|
|
|
|
dataset: string
|
|
|
|
|
checks_run: number
|
|
|
|
|
issues_found: number
|
|
|
|
|
flagged_columns: number
|
|
|
|
|
tools: string[]
|
|
|
|
|
recommendation: string
|
|
|
|
|
}
|
|
|
|
|
docling?: { used: boolean; parse_id?: string; document_structure?: DocStructure; stats?: Record<string, number>; images?: DocImage[] }
|
|
|
|
|
rag_ingest?: { ok: boolean; duplicate?: boolean; chunks?: number; message?: string; error?: string }
|
|
|
|
@@ -140,8 +182,9 @@ export function DataQualityView() {
|
|
|
|
|
const [reports, setReports] = useState<ReportSummary[]>([])
|
|
|
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
|
const [expandedCol, setExpandedCol] = useState<string | null>(null)
|
|
|
|
|
const [showGx, setShowGx] = useState(false)
|
|
|
|
|
const [showSoda, setShowSoda] = useState(false)
|
|
|
|
|
const [expandedCheck, setExpandedCheck] = useState<string | null>(null)
|
|
|
|
|
const [expandedDim, setExpandedDim] = useState<string | null>(null)
|
|
|
|
|
const [activeTool, setActiveTool] = useState<string>('great_expectations')
|
|
|
|
|
|
|
|
|
|
const loadMeta = useCallback(async () => {
|
|
|
|
|
try {
|
|
|
|
@@ -240,7 +283,7 @@ export function DataQualityView() {
|
|
|
|
|
{caps && (
|
|
|
|
|
<div className="mt-3 grid gap-2 sm:grid-cols-2 lg:grid-cols-4">
|
|
|
|
|
<CapCard title="Maturity Engine" items={caps.maturity_dimensions.map((d) => d.label)} icon={Layers} />
|
|
|
|
|
<CapCard title="Data Quality Tools" items={['Great Expectations', 'Soda Core', 'Pandas Profiling']} icon={CheckCircle2} />
|
|
|
|
|
<CapCard title="Data Quality Tools" items={['Great Expectations', 'Soda Core', 'Deequ', 'Pandera', 'dbt', 'Monte Carlo', 'Affirm', 'Pandas']} icon={CheckCircle2} />
|
|
|
|
|
<CapCard title="Document Parsing" items={caps.tools.docling?.capabilities || ['PDF', 'PPTX', 'DOCX']} icon={FileText} />
|
|
|
|
|
<CapCard title="File formats" items={[...caps.supported_data_formats.slice(0, 4), ...caps.supported_document_formats.slice(0, 3)]} icon={Upload} />
|
|
|
|
|
</div>
|
|
|
|
@@ -287,8 +330,8 @@ export function DataQualityView() {
|
|
|
|
|
<StatCard label="Overall Score" value={`${assess.overall_score}`} sub="/100" accent />
|
|
|
|
|
<StatCard label="Maturity Level" value={assess.maturity_level} sub={assess.maturity_description} />
|
|
|
|
|
<StatCard label="Dataset" value={`${assess.rows.toLocaleString()}`} sub={`${assess.columns} columns`} />
|
|
|
|
|
<StatCard label="Great Expectations" value={`${assess.checks_summary.great_expectations.passed}/${assess.checks_summary.great_expectations.total}`} sub="checks passed" />
|
|
|
|
|
<StatCard label="Soda Core" value={String(assess.checks_summary.soda_core.warnings)} sub="warnings" warn={assess.checks_summary.soda_core.warnings > 0} />
|
|
|
|
|
<StatCard label="DQ Checks" value={String(assess.executive_summary?.checks_run || Object.values(assess.checks_summary).reduce((s, x) => s + (x.total || 0), 0))} sub="across 7 tools" />
|
|
|
|
|
<StatCard label="Issues" value={String(assess.executive_summary?.issues_found ?? 0)} sub="failed/warn checks" warn={(assess.executive_summary?.issues_found ?? 0) > 0} />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{assess.docling?.used && assess.docling.document_structure && (
|
|
|
|
@@ -316,38 +359,69 @@ export function DataQualityView() {
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{assess.executive_summary && (
|
|
|
|
|
<section className="rounded-lg border border-docker/30 bg-docker/5 p-4">
|
|
|
|
|
<h3 className="mb-2 text-[11px] font-semibold uppercase tracking-wider text-docker">Executive Summary — Customer Report</h3>
|
|
|
|
|
<p className="text-[13px] font-semibold text-foreground">{assess.executive_summary.headline}</p>
|
|
|
|
|
<p className="mt-1 text-[11px] text-foreground-muted">{assess.executive_summary.dataset}</p>
|
|
|
|
|
<div className="mt-2 flex flex-wrap gap-3 text-[10px]">
|
|
|
|
|
<span>{assess.executive_summary.checks_run} checks run</span>
|
|
|
|
|
<span className={assess.executive_summary.issues_found > 0 ? 'text-warning' : 'text-success'}>{assess.executive_summary.issues_found} issues found</span>
|
|
|
|
|
<span>{assess.executive_summary.flagged_columns} flagged columns</span>
|
|
|
|
|
</div>
|
|
|
|
|
<p className="mt-2 border-t border-border/50 pt-2 text-[11px] text-foreground-muted">
|
|
|
|
|
<strong className="text-foreground">Recommendation:</strong> {assess.executive_summary.recommendation}
|
|
|
|
|
</p>
|
|
|
|
|
</section>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
|
|
|
<a href={assess.report_url} target="_blank" rel="noreferrer" className={cn('rounded-md px-3 py-1.5 text-[11px] font-medium', subTabActive)}>
|
|
|
|
|
Full HTML report ↗
|
|
|
|
|
</a>
|
|
|
|
|
<button type="button" onClick={() => setShowGx(!showGx)} className={cn('rounded-md px-3 py-1.5 text-[11px]', showGx ? subTabActive : subTabIdle)}>
|
|
|
|
|
GE checks ({assess.checks?.great_expectations.length || 0})
|
|
|
|
|
</button>
|
|
|
|
|
<button type="button" onClick={() => setShowSoda(!showSoda)} className={cn('rounded-md px-3 py-1.5 text-[11px]', showSoda ? subTabActive : subTabIdle)}>
|
|
|
|
|
Soda checks ({assess.checks?.soda_core.length || 0})
|
|
|
|
|
</button>
|
|
|
|
|
<a href={assess.report_url + '/json'} target="_blank" rel="noreferrer" className={cn('rounded-md px-3 py-1.5 text-[11px]', subTabIdle)}>
|
|
|
|
|
JSON export ↗
|
|
|
|
|
</a>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{showGx && assess.checks?.great_expectations && (
|
|
|
|
|
<CheckTable title="Great Expectations" rows={assess.checks.great_expectations.map((c) => ({
|
|
|
|
|
name: c.column ? `${c.expectation} [${c.column}]` : c.expectation,
|
|
|
|
|
status: c.success ? 'pass' : 'fail',
|
|
|
|
|
detail: c.result,
|
|
|
|
|
}))} />
|
|
|
|
|
)}
|
|
|
|
|
{showSoda && assess.checks?.soda_core && (
|
|
|
|
|
<CheckTable title="Soda Core" rows={assess.checks.soda_core.map((c) => ({
|
|
|
|
|
name: c.name,
|
|
|
|
|
status: c.outcome,
|
|
|
|
|
detail: `${c.check} — ${c.detail}`,
|
|
|
|
|
}))} />
|
|
|
|
|
)}
|
|
|
|
|
<section>
|
|
|
|
|
<h3 className="mb-2 text-[11px] font-semibold uppercase tracking-wider text-foreground-faint">Data Quality Tools — click any row for error details</h3>
|
|
|
|
|
<div className="mb-2 flex flex-wrap gap-1">
|
|
|
|
|
{([
|
|
|
|
|
['great_expectations', 'Great Expectations'],
|
|
|
|
|
['soda_core', 'Soda Core'],
|
|
|
|
|
['deequ', 'AWS Deequ'],
|
|
|
|
|
['pandera', 'Pandera'],
|
|
|
|
|
['dbt_expectations', 'dbt Expectations'],
|
|
|
|
|
['monte_carlo', 'Monte Carlo'],
|
|
|
|
|
['affirm', 'Affirm'],
|
|
|
|
|
] as const).map(([key, label]) => {
|
|
|
|
|
const items = assess.checks?.[key] || []
|
|
|
|
|
if (!items.length) return null
|
|
|
|
|
const summary = assess.checks_summary?.[key]
|
|
|
|
|
const badge = summary?.passed !== undefined ? `${summary.passed}/${summary.total}` : `${items.length}`
|
|
|
|
|
return (
|
|
|
|
|
<button key={key} type="button" onClick={() => setActiveTool(key)} className={cn('rounded-md px-2.5 py-1 text-[10px]', activeTool === key ? subTabActive : subTabIdle)}>
|
|
|
|
|
{label} ({badge})
|
|
|
|
|
</button>
|
|
|
|
|
)
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
{assess.checks && (
|
|
|
|
|
<CheckTable
|
|
|
|
|
title={activeTool.replace(/_/g, ' ')}
|
|
|
|
|
checks={normalizeChecks(assess.checks[activeTool as keyof typeof assess.checks] || [])}
|
|
|
|
|
expandedId={expandedCheck}
|
|
|
|
|
onToggle={setExpandedCheck}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
<h3 className="mb-3 text-[11px] font-semibold uppercase tracking-wider text-foreground-faint">6 Maturity Dimensions</h3>
|
|
|
|
|
<div className="grid gap-3 lg:grid-cols-2 xl:grid-cols-3">
|
|
|
|
|
{assess.dimensions.map((d) => (
|
|
|
|
|
<DimensionCard key={d.id} dimension={d} />
|
|
|
|
|
<DimensionCard key={d.id} dimension={d} expanded={expandedDim === d.id} onToggle={() => setExpandedDim(expandedDim === d.id ? null : d.id)} />
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
@@ -383,7 +457,7 @@ export function DataQualityView() {
|
|
|
|
|
<div className="space-y-5">
|
|
|
|
|
<p className="text-[12px] leading-relaxed text-foreground-muted">
|
|
|
|
|
Docling extracts text, tables, images and document structure from PDF, PowerPoint, Word, Excel and images.
|
|
|
|
|
Resultaat: Markdown, HTML, JSON met pagina's, plaatjes, tabellen en outline.
|
|
|
|
|
Output: Markdown, HTML, JSON with pages, images, tables and document outline.
|
|
|
|
|
</p>
|
|
|
|
|
<UploadZone
|
|
|
|
|
loading={loading}
|
|
|
|
@@ -398,8 +472,8 @@ export function DataQualityView() {
|
|
|
|
|
{parse && (
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-6">
|
|
|
|
|
<StatCard label="Bestand" value={parse.filename.length > 20 ? parse.filename.slice(0, 18) + '…' : parse.filename} sub={parse.status} />
|
|
|
|
|
<StatCard label="Verwerking" value={`${(parse.processing_time_sec || 0).toFixed(1)}s`} sub={`Formats: ${parse.formats_available.join(', ')}`} />
|
|
|
|
|
<StatCard label="File" value={parse.filename.length > 20 ? parse.filename.slice(0, 18) + '…' : parse.filename} sub={parse.status} />
|
|
|
|
|
<StatCard label="Processing" value={`${(parse.processing_time_sec || 0).toFixed(1)}s`} sub={`Formats: ${parse.formats_available.join(', ')}`} />
|
|
|
|
|
<StatCard label="Pages" value={String(parse.document_structure?.pages ?? parse.stats.pages ?? 0)} icon={Layers} />
|
|
|
|
|
<StatCard label="Images" value={String(parse.document_structure?.pictures ?? 0)} icon={Image} accent />
|
|
|
|
|
<StatCard label="Tables" value={String(parse.document_structure?.tables ?? 0)} icon={Table2} />
|
|
|
|
@@ -498,7 +572,7 @@ function ImageGallery({ images, parseId }: { images: DocImage[]; parseId: string
|
|
|
|
|
return (
|
|
|
|
|
<section className="rounded-lg border border-border bg-surface-overlay/30 p-4">
|
|
|
|
|
<h3 className="mb-2 text-[11px] font-semibold uppercase tracking-wider text-foreground-faint">
|
|
|
|
|
Images gedetecteerd ({images.length}) — no embedded export
|
|
|
|
|
Images detected ({images.length}) — no embedded export
|
|
|
|
|
</h3>
|
|
|
|
|
<p className="text-[11px] text-foreground-muted">Re-upload the document to extract images (embedded mode).</p>
|
|
|
|
|
</section>
|
|
|
|
@@ -508,7 +582,7 @@ function ImageGallery({ images, parseId }: { images: DocImage[]; parseId: string
|
|
|
|
|
<section className="rounded-lg border border-border bg-surface-overlay/30 p-4">
|
|
|
|
|
<h3 className="mb-3 flex items-center gap-2 text-[11px] font-semibold uppercase tracking-wider text-foreground-faint">
|
|
|
|
|
<Image className="h-4 w-4 text-docker" />
|
|
|
|
|
Images die Docling ziet ({available.length})
|
|
|
|
|
Images extracted by Docling ({available.length})
|
|
|
|
|
</h3>
|
|
|
|
|
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
|
|
|
|
|
{available.map((img) => (
|
|
|
|
@@ -601,9 +675,13 @@ function DocStructurePanel({ structure, title }: { structure: DocStructure; titl
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function DimensionCard({ dimension: d }: { dimension: Dimension }) {
|
|
|
|
|
function normalizeChecks(items: DqCheck[]): DqCheck[] {
|
|
|
|
|
return items.map((c, i) => ({ ...c, id: c.id || `${c.suite}_${i}` }))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function DimensionCard({ dimension: d, expanded, onToggle }: { dimension: Dimension; expanded?: boolean; onToggle?: () => void }) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="rounded-lg border border-border bg-surface-overlay/40 p-3">
|
|
|
|
|
<button type="button" onClick={onToggle} className="w-full rounded-lg border border-border bg-surface-overlay/40 p-3 text-left transition-colors hover:border-docker/30">
|
|
|
|
|
<div className="mb-1 flex items-center justify-between">
|
|
|
|
|
<span className="text-[12px] font-semibold">{d.label}</span>
|
|
|
|
|
<span className={cn('text-base font-bold', SCORE_COLOR(d.score))}>{d.score}</span>
|
|
|
|
@@ -617,10 +695,16 @@ function DimensionCard({ dimension: d }: { dimension: Dimension }) {
|
|
|
|
|
<li key={f} className="flex gap-1"><span className="text-docker">▸</span>{f}</li>
|
|
|
|
|
))}
|
|
|
|
|
</ul>
|
|
|
|
|
{d.recommended_actions && d.recommended_actions.length > 0 && (
|
|
|
|
|
<p className="mt-2 border-t border-border pt-2 text-[9px] text-warning">→ {d.recommended_actions[0]}</p>
|
|
|
|
|
{expanded && d.recommended_actions && d.recommended_actions.length > 0 && (
|
|
|
|
|
<div className="mt-2 border-t border-border pt-2">
|
|
|
|
|
<p className="mb-1 text-[9px] font-semibold uppercase text-foreground-faint">Recommended actions</p>
|
|
|
|
|
{d.recommended_actions.map((a) => <p key={a} className="text-[9px] text-warning">→ {a}</p>)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
{!expanded && d.recommended_actions && d.recommended_actions.length > 0 && (
|
|
|
|
|
<p className="mt-2 border-t border-border pt-2 text-[9px] text-foreground-faint">Click for {d.recommended_actions.length} recommended action(s)</p>
|
|
|
|
|
)}
|
|
|
|
|
</button>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -662,24 +746,75 @@ function ColumnTable({ profiles, expandedCol, onToggle }: { profiles: ColumnProf
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function CheckTable({ title, rows }: { title: string; rows: { name: string; status: string; detail: string }[] }) {
|
|
|
|
|
function checkLabel(c: DqCheck): string {
|
|
|
|
|
const base = c.expectation || c.name || c.metric || c.check || c.test || c.rule || c.monitor || 'check'
|
|
|
|
|
return c.column ? `${base} [${c.column}]` : base
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function checkStatus(c: DqCheck): string {
|
|
|
|
|
return c.status || (c.success === false ? 'fail' : c.success ? 'pass' : c.outcome || 'unknown')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function checkDetail(c: DqCheck): string {
|
|
|
|
|
return c.result || c.detail || ''
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function CheckTable({ title, checks, expandedId, onToggle }: { title: string; checks: DqCheck[]; expandedId: string | null; onToggle: (id: string | null) => void }) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="overflow-x-auto rounded-lg border border-border">
|
|
|
|
|
<p className="border-b border-border bg-surface-overlay px-3 py-2 text-[11px] font-semibold">{title}</p>
|
|
|
|
|
<p className="border-b border-border bg-surface-overlay px-3 py-2 text-[11px] font-semibold capitalize">{title}</p>
|
|
|
|
|
<table className="w-full text-[10px]">
|
|
|
|
|
<tbody>
|
|
|
|
|
{rows.map((r, i) => (
|
|
|
|
|
<tr key={i} className="border-t border-border">
|
|
|
|
|
<td className="px-3 py-1.5">
|
|
|
|
|
<span className={cn('mr-2 rounded px-1.5 py-0.5 text-[9px] font-bold uppercase',
|
|
|
|
|
r.status === 'pass' ? 'bg-success/20 text-success' : r.status === 'warn' ? 'bg-warning/20 text-warning' : 'bg-danger/20 text-danger')}>
|
|
|
|
|
{r.status}
|
|
|
|
|
</span>
|
|
|
|
|
{r.name}
|
|
|
|
|
</td>
|
|
|
|
|
<td className="px-3 py-1.5 text-foreground-muted">{r.detail}</td>
|
|
|
|
|
</tr>
|
|
|
|
|
))}
|
|
|
|
|
{checks.map((c) => {
|
|
|
|
|
const id = c.id || checkLabel(c)
|
|
|
|
|
const status = checkStatus(c)
|
|
|
|
|
const viol = c.violations
|
|
|
|
|
return (
|
|
|
|
|
<Fragment key={id}>
|
|
|
|
|
<tr className="cursor-pointer border-t border-border hover:bg-surface-overlay/50" onClick={() => onToggle(expandedId === id ? null : id)}>
|
|
|
|
|
<td className="px-3 py-1.5">
|
|
|
|
|
<span className={cn('mr-2 rounded px-1.5 py-0.5 text-[9px] font-bold uppercase',
|
|
|
|
|
status === 'pass' ? 'bg-success/20 text-success' : status === 'warn' ? 'bg-warning/20 text-warning' : 'bg-danger/20 text-danger')}>
|
|
|
|
|
{status}
|
|
|
|
|
</span>
|
|
|
|
|
{checkLabel(c)}
|
|
|
|
|
{viol?.violation_count ? <span className="ml-2 text-[9px] text-danger">{viol.violation_count} violations</span> : null}
|
|
|
|
|
</td>
|
|
|
|
|
<td className="px-3 py-1.5 text-foreground-muted">{checkDetail(c)}</td>
|
|
|
|
|
</tr>
|
|
|
|
|
{expandedId === id && (
|
|
|
|
|
<tr className="border-t border-border bg-surface-overlay/20">
|
|
|
|
|
<td colSpan={2} className="px-4 py-3 text-[10px]">
|
|
|
|
|
<p className="mb-2 font-semibold text-docker">Error location & samples</p>
|
|
|
|
|
{viol?.violation_count ? (
|
|
|
|
|
<>
|
|
|
|
|
<p className="text-foreground-muted">{viol.violation_count} rows affected ({viol.violation_pct}%){viol.location_hint ? ` · ${viol.location_hint}` : ''}</p>
|
|
|
|
|
{viol.sample_rows && viol.sample_rows.length > 0 && (
|
|
|
|
|
<div className="mt-2 overflow-x-auto rounded border border-border">
|
|
|
|
|
<table className="w-full text-[9px]">
|
|
|
|
|
<thead><tr className="bg-surface-overlay"><th className="px-2 py-1">Row #</th>{Object.keys(viol.sample_rows[0].values).map((k) => <th key={k} className="px-2 py-1">{k}</th>)}</tr></thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
{viol.sample_rows.map((s) => (
|
|
|
|
|
<tr key={s.row_index} className="border-t border-border/50">
|
|
|
|
|
<td className="px-2 py-1 font-mono text-warning">{s.row_index}</td>
|
|
|
|
|
{Object.values(s.values).map((v, j) => <td key={j} className="max-w-[120px] truncate px-2 py-1">{v === null ? 'NULL' : String(v)}</td>)}
|
|
|
|
|
</tr>
|
|
|
|
|
))}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<p className="text-success">No row-level violations — check passed.</p>
|
|
|
|
|
)}
|
|
|
|
|
{c.meta && <pre className="mt-2 rounded bg-black/30 p-2 font-mono text-[9px]">{JSON.stringify(c.meta, null, 2)}</pre>}
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
)}
|
|
|
|
|
</Fragment>
|
|
|
|
|
)
|
|
|
|
|
})}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|