init: fuzzygraph app from fn_registry
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user