Fix remaining Dutch string in DataQualityView
This commit is contained in:
+15
-15
@@ -105,9 +105,9 @@ AGENTS = [
|
||||
"motto": "Guardian of every row",
|
||||
"capabilities": ["PostgreSQL", "MySQL", "MongoDB", "Cassandra", "Neo4j"],
|
||||
"suggested_prompts": [
|
||||
"Hoeveel data zit er in de databases?",
|
||||
"Wat staat er in PostgreSQL sales_orders?",
|
||||
"MongoDB supplychain overzicht",
|
||||
"How much data is in the databases?",
|
||||
"What is in PostgreSQL sales_orders?",
|
||||
"MongoDB supplychain overview",
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -332,21 +332,21 @@ async def ask_llm(
|
||||
log: Any | None = None,
|
||||
) -> str | None:
|
||||
agent = next(a for a in AGENTS if a["id"] == agent_id)
|
||||
system = f"""Je bent {agent['name']}, een autonomous ops agent in het Dell ATC data lab.
|
||||
Specialisatie: {agent['role']}.
|
||||
Motto: {agent.get('motto', '')}
|
||||
system = f"""You are {agent['name']}, an autonomous ops agent in the Dell ATC data lab.
|
||||
Specialization: {agent['role']}.
|
||||
Motto: {agent.get('motto', '')}.
|
||||
|
||||
Je antwoordt namens je domein maar hebt zicht op de HELE lab stack: Docker, databases, lakehouse (Trino/Spark/Kafka Connect), ETL (Airflow/Kafka), Hadoop HDFS, en GPU/vLLM.
|
||||
You respond on behalf of your domain but have visibility into the FULL lab stack: Docker, databases, lakehouse (Trino/Spark/Kafka Connect), ETL (Airflow/Kafka), Hadoop HDFS, and GPU/vLLM.
|
||||
|
||||
Regels:
|
||||
- Antwoord in dezelfde taal als de gebruiker (Nederlands of Engels).
|
||||
- Je hebt volledige zicht op de HELE cluster: alle VMs, zones, connectors, GPU, Hadoop, ObjectScale en Command Center.
|
||||
- Gebruik ALLEEN de live data hieronder — verzin geen hosts, poorten, cijfers of connector namen.
|
||||
- Gebruik exact de container/connector namen uit de data (bijv. mysql-hr-connector, niet "Debezium").
|
||||
- Als iets DOWN of 0 GB is, zeg dat eerlijk.
|
||||
- Kort en behulpzaam (max ~10 zinnen); bullet lists mogen als het overzicht helpt.
|
||||
Rules:
|
||||
- Always respond in English.
|
||||
- You have full visibility into the entire cluster: all VMs, zones, connectors, GPU, Hadoop, ObjectScale and Command Center.
|
||||
- Use ONLY the live data below — do not invent hosts, ports, numbers or connector names.
|
||||
- Use exact container/connector names from the data (e.g. mysql-hr-connector, not "Debezium").
|
||||
- If something is DOWN or 0 GB, say so honestly.
|
||||
- Be concise and helpful (max ~10 sentences); bullet lists are fine when they aid clarity.
|
||||
|
||||
--- LIVE LAB DATA (primary domain eerst, daarna volledige stack) ---
|
||||
--- LIVE LAB DATA (primary domain first, then full stack) ---
|
||||
{context}
|
||||
"""
|
||||
if log:
|
||||
|
||||
@@ -32,7 +32,7 @@ export function ActivityStream({ feed, agents, filterAgentId, opsOnly }: Props)
|
||||
<div className="scrollbar-thin flex-1 overflow-y-auto px-2 pb-2">
|
||||
{items.length === 0 && (
|
||||
<p className="py-8 text-center text-[11px] text-foreground-faint">
|
||||
{opsOnly ? 'Geen operationele events — antwords staan in Chat.' : 'No activity yet — agents are on standby.'}
|
||||
{opsOnly ? 'No operational events — responses appear in Chat.' : 'No activity yet — agents are on standby.'}
|
||||
</p>
|
||||
)}
|
||||
{items.map((e) => {
|
||||
|
||||
@@ -53,7 +53,7 @@ export function ChatDrawer({
|
||||
Chat & Activity
|
||||
{unread > 0 && (
|
||||
<span className="rounded-full bg-docker/20 px-2 py-0.5 font-mono text-[10px] text-docker">
|
||||
{unread} bericht{unread !== 1 ? 'en' : ''}
|
||||
{unread} message{unread !== 1 ? 's' : ''}
|
||||
</span>
|
||||
)}
|
||||
{approvals.length > 0 && (
|
||||
|
||||
@@ -21,9 +21,9 @@ export function CommandBar({ busy, selectedAgent, onSubmit }: Props) {
|
||||
}
|
||||
|
||||
const suggestions = selectedAgent?.suggested_prompts?.slice(0, 3) || [
|
||||
'Hoeveel data zit er in de databases?',
|
||||
'Wat staat er in PostgreSQL?',
|
||||
'MongoDB supplychain overzicht',
|
||||
'How much data is in the databases?',
|
||||
'What is in PostgreSQL?',
|
||||
'MongoDB supplychain overview',
|
||||
]
|
||||
|
||||
return (
|
||||
|
||||
@@ -58,7 +58,7 @@ export function CommsPanel({ messages, agents, selectedAgent, busy }: Props) {
|
||||
{busy && (
|
||||
<div className="flex items-center gap-2 px-2 py-2 text-[10px] text-foreground-muted">
|
||||
<span className="h-1.5 w-1.5 animate-bounce rounded-full bg-accent" />
|
||||
<span>Agent verzamelt cluster-data en vraagt Llama 70B… verwacht ~30–90 sec</span>
|
||||
<span>Agent gathering cluster data and querying Llama 70B… expect ~30–90 sec</span>
|
||||
</div>
|
||||
)}
|
||||
<div ref={bottomRef} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -43,7 +43,7 @@ export function InfraQuickAccess({
|
||||
<div className="mb-2 flex items-center justify-between gap-2">
|
||||
<div>
|
||||
<h3 className="text-[9px] font-semibold uppercase tracking-wider text-foreground-muted">Infrastructure & Apps</h3>
|
||||
<p className="text-[8px] text-foreground-faint">Klik voor inspector · Shell kopieert SSH en opent live terminal · UI opent de applicatie</p>
|
||||
<p className="text-[8px] text-foreground-faint">Click for inspector · Shell copies SSH and opens live terminal · UI opens the app</p>
|
||||
</div>
|
||||
{workload && (
|
||||
<span className="shrink-0 font-mono text-[8px] text-foreground-faint">
|
||||
|
||||
@@ -95,16 +95,16 @@ export function InspectorPanel({
|
||||
{!d && !agent && (
|
||||
<div className="scrollbar-thin flex-1 space-y-3 overflow-y-auto p-3">
|
||||
<div className="rounded-lg border border-docker/25 bg-docker-light/50 p-2.5 dark:bg-blue-500/10">
|
||||
<p className="mb-1.5 text-[10px] font-semibold text-foreground">Snel starten</p>
|
||||
<p className="mb-1.5 text-[10px] font-semibold text-foreground">Quick start</p>
|
||||
<ol className="list-decimal space-y-1 pl-4 text-[10px] leading-relaxed text-foreground-muted">
|
||||
<li>Select an <strong className="text-foreground">infrastructure card</strong> or topology node</li>
|
||||
<li>Klik <strong className="text-foreground">Shell</strong> voor SSH + live terminal output</li>
|
||||
<li>Klik <strong className="text-foreground">UI</strong> om Airflow, Trino, Kafka UI, etc. te openen</li>
|
||||
<li>Stel vragen via <strong className="text-foreground">Chat</strong> onderaan — agents zien de hele cluster</li>
|
||||
<li>Click <strong className="text-foreground">Shell</strong> for SSH + live terminal output</li>
|
||||
<li>Click <strong className="text-foreground">UI</strong> to open Airflow, Trino, Kafka UI, etc.</li>
|
||||
<li>Ask questions via <strong className="text-foreground">Chat</strong> below — agents see the full cluster</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div>
|
||||
<p className="mb-1.5 text-[9px] uppercase tracking-wider text-foreground-faint">Agents & domeinen</p>
|
||||
<p className="mb-1.5 text-[9px] uppercase tracking-wider text-foreground-faint">Agents & domains</p>
|
||||
<div className="space-y-1">
|
||||
{agents.filter((a) => !a.supervisor).map((a) => {
|
||||
const meta = getAgentMeta(a.id)
|
||||
@@ -201,7 +201,7 @@ export function InspectorPanel({
|
||||
{infra && (
|
||||
<Button size="sm" variant="outline" onClick={runShell} disabled={busy}>
|
||||
<Terminal className="h-3 w-3" />
|
||||
{shellCopied ? 'SSH gekopieerd' : 'Shell'}
|
||||
{shellCopied ? 'SSH copied' : 'Shell'}
|
||||
</Button>
|
||||
)}
|
||||
<Button size="sm" variant="outline" onClick={onProbe} disabled={busy}>
|
||||
@@ -275,7 +275,7 @@ export function InspectorPanel({
|
||||
)}
|
||||
</div>
|
||||
<form onSubmit={submit} className="flex gap-1 border-t border-border p-2">
|
||||
<Input value={input} onChange={(e) => setInput(e.target.value)} placeholder={`Vraag over ${d.label}…`} disabled={busy} className="text-xs" />
|
||||
<Input value={input} onChange={(e) => setInput(e.target.value)} placeholder={`Ask about ${d.label}…`} disabled={busy} className="text-xs" />
|
||||
<Button type="submit" size="sm" disabled={busy || !input.trim()}>Send</Button>
|
||||
</form>
|
||||
</>
|
||||
|
||||
@@ -49,11 +49,11 @@ export function TerminalDock({ subjectId, subjectLabel, lines, busy, expanded, o
|
||||
<div className="scrollbar-thin min-h-0 flex-1 overflow-y-auto px-3 pb-2 font-mono text-[9px] leading-relaxed">
|
||||
{infra && (
|
||||
<p className="mb-1 text-foreground-faint">
|
||||
<span className="text-docker">$</span> {infra.ssh} <span className="text-foreground-faint/70">(gekopieerd bij Shell-knop)</span>
|
||||
<span className="text-docker">$</span> {infra.ssh} <span className="text-foreground-faint/70">(copied via Shell button)</span>
|
||||
</p>
|
||||
)}
|
||||
{lines.length === 0 && (
|
||||
<p className="py-4 text-center text-foreground-faint">Selecteer een node of agent · klik Shell of Probe om output te zien</p>
|
||||
<p className="py-4 text-center text-foreground-faint">Select a node or agent · click Shell or Probe to see output</p>
|
||||
)}
|
||||
{lines.map((line) => (
|
||||
<div key={line.id} className={LEVEL[line.level] || 'text-foreground-muted'}>
|
||||
|
||||
@@ -55,7 +55,7 @@ export function WorkbenchPanel({ mode, agent, lines, busy, onSendPrompt }: Props
|
||||
e.preventDefault()
|
||||
}}
|
||||
className="flex h-2.5 shrink-0 cursor-ns-resize items-center justify-center border-b border-border/50 bg-surface-overlay/60 hover:bg-docker/10"
|
||||
title="Sleep omhoog/omlaag om grootte aan te passen"
|
||||
title="Drag up/down to resize"
|
||||
>
|
||||
<GripHorizontal className="h-3 w-3 text-foreground-faint" />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user