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,11 +1,11 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Stack, Group, TextInput, Textarea, Text } from '@mantine/core'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, SimpleSelect, Button } from '@fn_library'
|
||||
import type { Entity, EntityTypePreset, EntityInput } from '../types'
|
||||
import { SimpleSelect } from '@fn_library'
|
||||
import { X } from 'lucide-react'
|
||||
|
||||
interface Props {
|
||||
presets: EntityTypePreset[]
|
||||
entity: Entity | null // null = create, non-null = edit
|
||||
entity: Entity | null
|
||||
onSubmit: (input: EntityInput) => void
|
||||
onClose: () => void
|
||||
}
|
||||
@@ -29,7 +29,6 @@ export function EntityDialog({ presets, entity, onSubmit, onClose }: Props) {
|
||||
const currentPreset = presets.find(p => p.type_ref === typeRef)
|
||||
const metadataFields = currentPreset?.metadata_fields ?? []
|
||||
|
||||
// When type changes, reset metadata fields to match new type
|
||||
useEffect(() => {
|
||||
if (!entity) {
|
||||
const m: Record<string, string> = {}
|
||||
@@ -44,7 +43,6 @@ export function EntityDialog({ presets, entity, onSubmit, onClose }: Props) {
|
||||
const cleanMeta: Record<string, unknown> = {}
|
||||
for (const [k, v] of Object.entries(metadata)) {
|
||||
if (v.trim()) {
|
||||
// Try parsing as number
|
||||
const num = Number(v)
|
||||
if (!isNaN(num) && v.trim() !== '') {
|
||||
cleanMeta[k] = num
|
||||
@@ -68,28 +66,23 @@ export function EntityDialog({ presets, entity, onSubmit, onClose }: Props) {
|
||||
})
|
||||
}
|
||||
|
||||
const inputStyle = {
|
||||
background: 'var(--input)',
|
||||
color: 'var(--foreground)',
|
||||
border: '1px solid var(--border)',
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center" style={{ background: 'rgba(0,0,0,0.6)' }}>
|
||||
<div className="w-[520px] max-h-[85vh] overflow-y-auto rounded-lg p-5" style={{ background: 'var(--card)', border: '1px solid var(--border)' }}>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-base font-semibold">{entity ? 'Edit Entity' : 'New Entity'}</h3>
|
||||
<button onClick={onClose} className="p-1"><X size={16} style={{ color: 'var(--muted-foreground)' }} /></button>
|
||||
</div>
|
||||
<Dialog open onOpenChange={(open: boolean) => { if (!open) onClose() }}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{entity ? 'Edit Entity' : 'New Entity'}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-xs mb-1" style={{ color: 'var(--muted-foreground)' }}>Name</label>
|
||||
<input value={name} onChange={e => setName(e.target.value)} className="w-full px-3 py-1.5 rounded text-sm" style={inputStyle} />
|
||||
</div>
|
||||
<Stack gap="sm">
|
||||
<TextInput
|
||||
label="Name"
|
||||
value={name}
|
||||
onChange={e => setName(e.currentTarget.value)}
|
||||
size="sm"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs mb-1" style={{ color: 'var(--muted-foreground)' }}>Type</label>
|
||||
<Text size="sm" fw={500} mb={4}>Type</Text>
|
||||
<SimpleSelect
|
||||
value={typeRef}
|
||||
onValueChange={setTypeRef}
|
||||
@@ -97,64 +90,61 @@ export function EntityDialog({ presets, entity, onSubmit, onClose }: Props) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs mb-1" style={{ color: 'var(--muted-foreground)' }}>Description</label>
|
||||
<input value={description} onChange={e => setDescription(e.target.value)} className="w-full px-3 py-1.5 rounded text-sm" style={inputStyle} />
|
||||
</div>
|
||||
<TextInput
|
||||
label="Description"
|
||||
value={description}
|
||||
onChange={e => setDescription(e.currentTarget.value)}
|
||||
size="sm"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs mb-1" style={{ color: 'var(--muted-foreground)' }}>Tags (comma separated)</label>
|
||||
<input value={tagsStr} onChange={e => setTagsStr(e.target.value)} className="w-full px-3 py-1.5 rounded text-sm" style={inputStyle} placeholder="osint, high-risk" />
|
||||
</div>
|
||||
<TextInput
|
||||
label="Tags (comma separated)"
|
||||
value={tagsStr}
|
||||
onChange={e => setTagsStr(e.currentTarget.value)}
|
||||
placeholder="osint, high-risk"
|
||||
size="sm"
|
||||
/>
|
||||
|
||||
{metadataFields.length > 0 && (
|
||||
<div>
|
||||
<label className="block text-xs mb-1 font-semibold" style={{ color: 'var(--muted-foreground)' }}>
|
||||
<Text size="xs" fw={600} c="dimmed" mb="xs">
|
||||
Metadata ({currentPreset?.label})
|
||||
</label>
|
||||
<div className="space-y-2">
|
||||
</Text>
|
||||
<Stack gap="xs">
|
||||
{metadataFields.map(field => (
|
||||
<div key={field} className="flex items-center gap-2">
|
||||
<span className="text-xs w-28 text-right" style={{ color: 'var(--muted-foreground)' }}>{field}</span>
|
||||
<input
|
||||
<Group key={field} gap="sm" align="center">
|
||||
<Text size="xs" c="dimmed" w={112} ta="right">{field}</Text>
|
||||
<TextInput
|
||||
value={metadata[field] ?? ''}
|
||||
onChange={e => setMetadata(prev => ({ ...prev, [field]: e.target.value }))}
|
||||
className="flex-1 px-2 py-1 rounded text-sm"
|
||||
style={inputStyle}
|
||||
onChange={e => setMetadata(prev => ({ ...prev, [field]: e.currentTarget.value }))}
|
||||
size="xs"
|
||||
flex={1}
|
||||
/>
|
||||
</div>
|
||||
</Group>
|
||||
))}
|
||||
</div>
|
||||
</Stack>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-xs mb-1" style={{ color: 'var(--muted-foreground)' }}>Notes</label>
|
||||
<textarea
|
||||
value={notes}
|
||||
onChange={e => setNotes(e.target.value)}
|
||||
rows={3}
|
||||
className="w-full px-3 py-1.5 rounded text-sm resize-none"
|
||||
style={inputStyle}
|
||||
placeholder="Operational notes..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Textarea
|
||||
label="Notes"
|
||||
value={notes}
|
||||
onChange={e => setNotes(e.currentTarget.value)}
|
||||
rows={3}
|
||||
placeholder="Operational notes..."
|
||||
size="sm"
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<div className="flex justify-end gap-2 mt-4">
|
||||
<button onClick={onClose} className="px-3 py-1.5 rounded text-sm" style={{ background: 'var(--secondary)', color: 'var(--secondary-foreground)' }}>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={!name.trim()}
|
||||
className="px-3 py-1.5 rounded text-sm font-medium disabled:opacity-40"
|
||||
style={{ background: 'var(--primary)', color: 'var(--primary-foreground)' }}
|
||||
>
|
||||
{entity ? 'Update' : 'Create'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Group justify="flex-end" gap="sm" mt="md">
|
||||
<Button variant="secondary" onClick={onClose}>Cancel</Button>
|
||||
<Button onClick={handleSubmit} disabled={!name.trim()}>
|
||||
{entity ? 'Update' : 'Create'}
|
||||
</Button>
|
||||
</Group>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user