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).
151 lines
4.7 KiB
TypeScript
151 lines
4.7 KiB
TypeScript
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'
|
|
|
|
interface Props {
|
|
presets: EntityTypePreset[]
|
|
entity: Entity | null
|
|
onSubmit: (input: EntityInput) => void
|
|
onClose: () => void
|
|
}
|
|
|
|
export function EntityDialog({ presets, entity, onSubmit, onClose }: Props) {
|
|
const [name, setName] = useState(entity?.name ?? '')
|
|
const [typeRef, setTypeRef] = useState(entity?.type_ref ?? presets[0]?.type_ref ?? '')
|
|
const [description, setDescription] = useState(entity?.description ?? '')
|
|
const [notes, setNotes] = useState(entity?.notes ?? '')
|
|
const [tagsStr, setTagsStr] = useState((entity?.tags ?? []).join(', '))
|
|
const [metadata, setMetadata] = useState<Record<string, string>>(() => {
|
|
const m: Record<string, string> = {}
|
|
if (entity?.metadata) {
|
|
for (const [k, v] of Object.entries(entity.metadata)) {
|
|
m[k] = String(v ?? '')
|
|
}
|
|
}
|
|
return m
|
|
})
|
|
|
|
const currentPreset = presets.find(p => p.type_ref === typeRef)
|
|
const metadataFields = currentPreset?.metadata_fields ?? []
|
|
|
|
useEffect(() => {
|
|
if (!entity) {
|
|
const m: Record<string, string> = {}
|
|
for (const f of metadataFields) {
|
|
m[f] = metadata[f] ?? ''
|
|
}
|
|
setMetadata(m)
|
|
}
|
|
}, [typeRef]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
const handleSubmit = () => {
|
|
const cleanMeta: Record<string, unknown> = {}
|
|
for (const [k, v] of Object.entries(metadata)) {
|
|
if (v.trim()) {
|
|
const num = Number(v)
|
|
if (!isNaN(num) && v.trim() !== '') {
|
|
cleanMeta[k] = num
|
|
} else if (v === 'true') {
|
|
cleanMeta[k] = true
|
|
} else if (v === 'false') {
|
|
cleanMeta[k] = false
|
|
} else {
|
|
cleanMeta[k] = v.trim()
|
|
}
|
|
}
|
|
}
|
|
|
|
onSubmit({
|
|
name: name.trim(),
|
|
type_ref: typeRef,
|
|
description: description.trim(),
|
|
tags: tagsStr.split(',').map(t => t.trim()).filter(Boolean),
|
|
metadata: cleanMeta,
|
|
notes: notes.trim(),
|
|
})
|
|
}
|
|
|
|
return (
|
|
<Dialog open onOpenChange={(open: boolean) => { if (!open) onClose() }}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>{entity ? 'Edit Entity' : 'New Entity'}</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
<Stack gap="sm">
|
|
<TextInput
|
|
label="Name"
|
|
value={name}
|
|
onChange={e => setName(e.currentTarget.value)}
|
|
size="sm"
|
|
/>
|
|
|
|
<div>
|
|
<Text size="sm" fw={500} mb={4}>Type</Text>
|
|
<SimpleSelect
|
|
value={typeRef}
|
|
onValueChange={setTypeRef}
|
|
options={presets.map(p => ({ value: p.type_ref, label: p.label }))}
|
|
/>
|
|
</div>
|
|
|
|
<TextInput
|
|
label="Description"
|
|
value={description}
|
|
onChange={e => setDescription(e.currentTarget.value)}
|
|
size="sm"
|
|
/>
|
|
|
|
<TextInput
|
|
label="Tags (comma separated)"
|
|
value={tagsStr}
|
|
onChange={e => setTagsStr(e.currentTarget.value)}
|
|
placeholder="osint, high-risk"
|
|
size="sm"
|
|
/>
|
|
|
|
{metadataFields.length > 0 && (
|
|
<div>
|
|
<Text size="xs" fw={600} c="dimmed" mb="xs">
|
|
Metadata ({currentPreset?.label})
|
|
</Text>
|
|
<Stack gap="xs">
|
|
{metadataFields.map(field => (
|
|
<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.currentTarget.value }))}
|
|
size="xs"
|
|
flex={1}
|
|
/>
|
|
</Group>
|
|
))}
|
|
</Stack>
|
|
</div>
|
|
)}
|
|
|
|
<Textarea
|
|
label="Notes"
|
|
value={notes}
|
|
onChange={e => setNotes(e.currentTarget.value)}
|
|
rows={3}
|
|
placeholder="Operational notes..."
|
|
size="sm"
|
|
/>
|
|
</Stack>
|
|
|
|
<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>
|
|
)
|
|
}
|