init: fuzzygraph app from fn_registry

This commit is contained in:
2026-04-06 00:56:50 +02:00
commit 23198eee0c
42 changed files with 5539 additions and 0 deletions
+170
View File
@@ -0,0 +1,170 @@
import { useState } from 'react'
import type { Entity, Assertion, AssertionResult, AssertionInput } from '../types'
import { Plus, Play, Trash2 } from 'lucide-react'
import { SimpleSelect } from '@fn_library'
import { Card, CardHeader, CardTitle, CardContent } from '@fn_library'
import { Button } from '@fn_library'
import * as WailsApp from '../wailsjs/go/main/App'
interface Props {
entities: Entity[]
}
export function AssertionPanel({ entities }: Props) {
const [assertions, setAssertions] = useState<Assertion[]>([])
const [results, setResults] = useState<AssertionResult[]>([])
const [selectedEntity, setSelectedEntity] = useState(entities[0]?.id ?? '')
const [showAdd, setShowAdd] = useState(false)
const [newName, setNewName] = useState('')
const [newKind, setNewKind] = useState('range')
const [newRule, setNewRule] = useState('')
const [newSeverity, setNewSeverity] = useState('warning')
const loadAssertions = async (entityId: string) => {
setSelectedEntity(entityId)
const list = await WailsApp.ListAssertions(entityId) as unknown as Assertion[]
setAssertions(list || [])
setResults([])
}
const handleAdd = async () => {
if (!newName || !newRule) return
const input: AssertionInput = {
entity_id: selectedEntity,
name: newName,
kind: newKind,
rule: newRule,
severity: newSeverity,
description: '',
}
await WailsApp.AddAssertion(input as never)
setShowAdd(false)
setNewName('')
setNewRule('')
loadAssertions(selectedEntity)
}
const handleEval = async () => {
const res = await WailsApp.EvalAssertions(selectedEntity) as unknown as AssertionResult[]
setResults(res || [])
}
const handleDelete = async (id: string) => {
await WailsApp.DeleteAssertion(id)
loadAssertions(selectedEntity)
}
const inputStyle = { background: 'var(--input)', color: 'var(--foreground)', border: '1px solid var(--border)' }
return (
<Card className="mt-3">
<CardHeader className="flex flex-row items-center justify-between py-3">
<CardTitle className="text-base">Assertions</CardTitle>
<div className="flex gap-2">
<SimpleSelect
value={selectedEntity}
onValueChange={loadAssertions}
placeholder="Select entity..."
className="w-48"
options={entities.map(e => ({ value: e.id, label: e.name }))}
/>
<Button size="sm" onClick={handleEval} disabled={!selectedEntity}>
<Play size={14} className="mr-1" /> Eval
</Button>
<Button size="sm" variant="outline" onClick={() => setShowAdd(!showAdd)} disabled={!selectedEntity}>
<Plus size={14} />
</Button>
</div>
</CardHeader>
{showAdd && (
<div className="px-4 pb-3 flex gap-2 items-end">
<div className="flex-1">
<label className="block text-xs mb-1" style={{ color: 'var(--muted-foreground)' }}>Name</label>
<input value={newName} onChange={e => setNewName(e.target.value)} className="w-full px-2 py-1 rounded text-sm" style={inputStyle} />
</div>
<div className="w-24">
<label className="block text-xs mb-1" style={{ color: 'var(--muted-foreground)' }}>Kind</label>
<SimpleSelect
value={newKind}
onValueChange={setNewKind}
options={[
{ value: 'range', label: 'range' },
{ value: 'null', label: 'null' },
{ value: 'statistical', label: 'statistical' },
{ value: 'consistency', label: 'consistency' },
{ value: 'freshness', label: 'freshness' },
]}
/>
</div>
<div className="w-24">
<label className="block text-xs mb-1" style={{ color: 'var(--muted-foreground)' }}>Severity</label>
<SimpleSelect
value={newSeverity}
onValueChange={setNewSeverity}
options={[
{ value: 'critical', label: 'critical' },
{ value: 'warning', label: 'warning' },
{ value: 'info', label: 'info' },
]}
/>
</div>
<div className="flex-1">
<label className="block text-xs mb-1" style={{ color: 'var(--muted-foreground)' }}>Rule (SQL expr)</label>
<input value={newRule} onChange={e => setNewRule(e.target.value)} className="w-full px-2 py-1 rounded text-sm" style={inputStyle} placeholder="risk_score > 70" />
</div>
<button onClick={handleAdd} className="px-3 py-1 rounded text-sm" style={{ background: 'var(--primary)', color: 'var(--primary-foreground)' }}>Add</button>
</div>
)}
<CardContent className="p-0">
<table className="w-full text-sm">
<thead>
<tr className="border-b" style={{ borderColor: 'var(--border)' }}>
<th className="text-left px-4 py-2 font-medium" style={{ color: 'var(--muted-foreground)' }}>Name</th>
<th className="text-left px-4 py-2 font-medium" style={{ color: 'var(--muted-foreground)' }}>Kind</th>
<th className="text-left px-4 py-2 font-medium" style={{ color: 'var(--muted-foreground)' }}>Rule</th>
<th className="text-left px-4 py-2 font-medium" style={{ color: 'var(--muted-foreground)' }}>Severity</th>
<th className="text-left px-4 py-2 font-medium" style={{ color: 'var(--muted-foreground)' }}>Result</th>
<th className="text-right px-4 py-2 font-medium" style={{ color: 'var(--muted-foreground)' }}>Actions</th>
</tr>
</thead>
<tbody>
{assertions.map(a => {
const result = results.find(r => r.assertion_id === a.id)
return (
<tr key={a.id} className="border-b" style={{ borderColor: 'var(--border)' }}>
<td className="px-4 py-2">{a.name}</td>
<td className="px-4 py-2" style={{ color: 'var(--muted-foreground)' }}>{a.kind}</td>
<td className="px-4 py-2 font-mono text-xs">{a.rule}</td>
<td className="px-4 py-2">
<span style={{ color: a.severity === 'critical' ? 'var(--destructive)' : a.severity === 'warning' ? 'var(--chart-3, #f59e0b)' : 'var(--muted-foreground)' }}>
{a.severity}
</span>
</td>
<td className="px-4 py-2">
{result ? (
<span style={{ color: result.status === 'pass' ? 'var(--success)' : 'var(--destructive)' }}>
{result.status}
</span>
) : '—'}
</td>
<td className="px-4 py-2 text-right">
<button onClick={() => handleDelete(a.id)} className="p-1 rounded" style={{ color: 'var(--destructive)' }}>
<Trash2 size={14} />
</button>
</td>
</tr>
)
})}
{assertions.length === 0 && (
<tr><td colSpan={6} className="px-4 py-8 text-center" style={{ color: 'var(--muted-foreground)' }}>
{selectedEntity ? 'No assertions for this entity' : 'Select an entity to view assertions'}
</td></tr>
)}
</tbody>
</table>
</CardContent>
</Card>
)
}