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).
This commit is contained in:
@@ -1,10 +1,9 @@
|
||||
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 { 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[]
|
||||
@@ -54,37 +53,39 @@ export function AssertionPanel({ entities }: Props) {
|
||||
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>
|
||||
<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 && (
|
||||
<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>
|
||||
<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}
|
||||
@@ -97,8 +98,8 @@ export function AssertionPanel({ entities }: Props) {
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-24">
|
||||
<label className="block text-xs mb-1" style={{ color: 'var(--muted-foreground)' }}>Severity</label>
|
||||
<div style={{ width: 96 }}>
|
||||
<Text size="xs" fw={500} mb={4}>Severity</Text>
|
||||
<SimpleSelect
|
||||
value={newSeverity}
|
||||
onValueChange={setNewSeverity}
|
||||
@@ -109,61 +110,77 @@ export function AssertionPanel({ entities }: Props) {
|
||||
]}
|
||||
/>
|
||||
</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>
|
||||
<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 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>
|
||||
<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 (
|
||||
<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)' }}>
|
||||
<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}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-2">
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
{result ? (
|
||||
<span style={{ color: result.status === 'pass' ? 'var(--success)' : 'var(--destructive)' }}>
|
||||
<Text size="sm" c={result.status === 'pass' ? 'teal' : 'red'}>
|
||||
{result.status}
|
||||
</span>
|
||||
</Text>
|
||||
) : '—'}
|
||||
</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>
|
||||
</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 && (
|
||||
<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>
|
||||
<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>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user