Files
fuzzygraph/frontend/src/components/AssertionPanel.tsx
T
dataforge c9fd4aa84c feat: enrichers, panel de ingest y menu contextual en el grafo
- Añade enricher.go + directorio enrichers/ para enriquecer entidades con fuentes externas.
- Nuevos componentes frontend: IngestPanel (panel de ingesta de datos) y NodeContextMenu (menu contextual sobre nodos del grafo).
- Retira SearchBar y lib/utils.ts; la busqueda se integra dentro de los paneles existentes.
- Ajusta tipos (types.go, types.ts, wailsjs/go) y theming (postcss + app.css + Mantine).
- Actualiza app.go y wails.json para exponer las nuevas capacidades.
- Añade directorio projects/ con estado inicial.
- Rebuild del frontend (dist actualizado).
2026-04-13 23:32:55 +02:00

188 lines
6.5 KiB
TypeScript

import { useState } from 'react'
import { Table, Group, Text, TextInput } from '@mantine/core'
import { IconPlus, IconPlayerPlay, IconTrash } from '@tabler/icons-react'
import { Card, CardHeader, CardTitle, CardContent, SimpleSelect, Button, FnActionIcon } from '@fn_library'
import * as WailsApp from '../wailsjs/go/main/App'
import type { Entity, Assertion, AssertionResult, AssertionInput } from '../types'
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)
}
return (
<Card variant="default" style={{ marginTop: 12 }}>
<CardHeader>
<Group justify="space-between" align="center" py="xs">
<CardTitle>Assertions</CardTitle>
<Group gap="sm">
<SimpleSelect
value={selectedEntity}
onValueChange={loadAssertions}
placeholder="Select entity..."
options={entities.map(e => ({ value: e.id, label: e.name }))}
/>
<Button size="sm" onClick={handleEval} disabled={!selectedEntity}>
<IconPlayerPlay size={14} style={{ marginRight: 4 }} /> Eval
</Button>
<Button size="sm" variant="outline" onClick={() => setShowAdd(!showAdd)} disabled={!selectedEntity}>
<IconPlus size={14} />
</Button>
</Group>
</Group>
</CardHeader>
{showAdd && (
<Group px="md" pb="sm" gap="sm" align="flex-end">
<TextInput
label="Name"
value={newName}
onChange={e => setNewName(e.currentTarget.value)}
size="xs"
flex={1}
/>
<div style={{ width: 96 }}>
<Text size="xs" fw={500} mb={4}>Kind</Text>
<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 style={{ width: 96 }}>
<Text size="xs" fw={500} mb={4}>Severity</Text>
<SimpleSelect
value={newSeverity}
onValueChange={setNewSeverity}
options={[
{ value: 'critical', label: 'critical' },
{ value: 'warning', label: 'warning' },
{ value: 'info', label: 'info' },
]}
/>
</div>
<TextInput
label="Rule (SQL expr)"
value={newRule}
onChange={e => setNewRule(e.currentTarget.value)}
placeholder="risk_score > 70"
size="xs"
flex={1}
/>
<Button size="sm" onClick={handleAdd}>Add</Button>
</Group>
)}
<CardContent style={{ padding: 0 }}>
<Table highlightOnHover>
<Table.Thead>
<Table.Tr>
<Table.Th>Name</Table.Th>
<Table.Th>Kind</Table.Th>
<Table.Th>Rule</Table.Th>
<Table.Th>Severity</Table.Th>
<Table.Th>Result</Table.Th>
<Table.Th ta="right">Actions</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{assertions.map(a => {
const result = results.find(r => r.assertion_id === a.id)
return (
<Table.Tr key={a.id}>
<Table.Td>{a.name}</Table.Td>
<Table.Td>
<Text size="sm" c="dimmed">{a.kind}</Text>
</Table.Td>
<Table.Td>
<Text size="xs" ff="monospace">{a.rule}</Text>
</Table.Td>
<Table.Td>
<Text size="sm" c={a.severity === 'critical' ? 'red' : a.severity === 'warning' ? 'yellow' : 'dimmed'}>
{a.severity}
</Text>
</Table.Td>
<Table.Td>
{result ? (
<Text size="sm" c={result.status === 'pass' ? 'teal' : 'red'}>
{result.status}
</Text>
) : '—'}
</Table.Td>
<Table.Td ta="right">
<FnActionIcon
icon={<IconTrash size={14} />}
variant="subtle"
size="sm"
color="red"
onClick={() => handleDelete(a.id)}
/>
</Table.Td>
</Table.Tr>
)
})}
{assertions.length === 0 && (
<Table.Tr>
<Table.Td colSpan={6}>
<Text ta="center" c="dimmed" py="xl">
{selectedEntity ? 'No assertions for this entity' : 'Select an entity to view assertions'}
</Text>
</Table.Td>
</Table.Tr>
)}
</Table.Tbody>
</Table>
</CardContent>
</Card>
)
}