c9fd4aa84c
- 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).
188 lines
6.5 KiB
TypeScript
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>
|
|
)
|
|
}
|