diff --git a/frontend/src/components/panels/DetailPanel.tsx b/frontend/src/components/panels/DetailPanel.tsx index 24cfa32..93a0abc 100644 --- a/frontend/src/components/panels/DetailPanel.tsx +++ b/frontend/src/components/panels/DetailPanel.tsx @@ -1,5 +1,5 @@ import { useState } from 'react' -import { X, Edit, Trash2, ExternalLink, Plus } from 'lucide-react' +import { X, Edit, Trash2, ExternalLink, Plus, Pencil } from 'lucide-react' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { useCanvasStore } from '@/stores/canvasStore' @@ -10,16 +10,18 @@ interface DetailPanelProps { onEdit: (id: string) => void } +type SvcForm = { port: string; protocol: 'tcp' | 'udp'; service_name: string } + +const EMPTY_FORM: SvcForm = { port: '', protocol: 'tcp', service_name: '' } + export function DetailPanel({ onEdit }: DetailPanelProps) { const { nodes, selectedNodeId, setSelectedNode, deleteNode, updateNode } = useCanvasStore() const node = nodes.find((n) => n.id === selectedNodeId) const [addingService, setAddingService] = useState(false) - const [newSvc, setNewSvc] = useState<{ port: string; protocol: 'tcp' | 'udp'; service_name: string }>({ - port: '', - protocol: 'tcp', - service_name: '', - }) + const [newSvc, setNewSvc] = useState(EMPTY_FORM) + const [editingIndex, setEditingIndex] = useState(null) + const [editSvc, setEditSvc] = useState(EMPTY_FORM) if (!node || node.data.type === 'groupRect') return null @@ -42,13 +44,34 @@ export function DetailPanel({ onEdit }: DetailPanelProps) { service_name: newSvc.service_name.trim(), } updateNode(node.id, { services: [...(data.services ?? []), svc] }) - setNewSvc({ port: '', protocol: 'tcp', service_name: '' }) + setNewSvc(EMPTY_FORM) setAddingService(false) } const handleRemoveService = (index: number) => { const updated = data.services.filter((_, i) => i !== index) updateNode(node.id, { services: updated }) + if (editingIndex === index) setEditingIndex(null) + } + + const handleStartEdit = (index: number) => { + const svc = data.services[index] + setEditSvc({ port: String(svc.port), protocol: svc.protocol, service_name: svc.service_name }) + setEditingIndex(index) + setAddingService(false) + } + + const handleSaveEdit = () => { + if (editingIndex === null) return + const port = parseInt(editSvc.port, 10) + if (!editSvc.service_name.trim() || isNaN(port) || port < 1 || port > 65535) return + const updated = data.services.map((svc, i) => + i === editingIndex + ? { ...svc, port, protocol: editSvc.protocol, service_name: editSvc.service_name.trim() } + : svc + ) + updateNode(node.id, { services: updated }) + setEditingIndex(null) } return ( @@ -118,7 +141,7 @@ export function DetailPanel({ onEdit }: DetailPanelProps) { Services{data.services.length > 0 ? ` (${data.services.length})` : ''} - - - + setAddingService(false)} + confirmLabel="Add" + autoFocus + /> )} {data.services.length > 0 && (
- {data.services.map((svc, i) => ( - handleRemoveService(i)} - /> - ))} + {data.services.map((svc, i) => + editingIndex === i ? ( + setEditingIndex(null)} + confirmLabel="Save" + autoFocus + /> + ) : ( + handleStartEdit(i)} + onRemove={() => handleRemoveService(i)} + /> + ) + )}
)} @@ -232,6 +231,67 @@ function DetailRow({ label, value, mono }: { label: string; value: string; mono? ) } +function ServiceForm({ + form, + onChange, + onConfirm, + onCancel, + confirmLabel, + autoFocus, +}: { + form: { port: string; protocol: 'tcp' | 'udp'; service_name: string } + onChange: (f: { port: string; protocol: 'tcp' | 'udp'; service_name: string }) => void + onConfirm: () => void + onCancel: () => void + confirmLabel: string + autoFocus?: boolean +}) { + return ( +
+ onChange({ ...form, service_name: e.target.value })} + placeholder="Service name" + className="bg-[#21262d] border-[#30363d] text-xs h-7" + autoFocus={autoFocus} + onKeyDown={(e) => e.key === 'Enter' && onConfirm()} + /> +
+ onChange({ ...form, port: e.target.value })} + placeholder="Port" + min={1} + max={65535} + className="bg-[#21262d] border-[#30363d] font-mono text-xs h-7 w-20 shrink-0" + onKeyDown={(e) => e.key === 'Enter' && onConfirm()} + /> + +
+
+ + +
+
+ ) +} + const CATEGORY_COLORS: Record = { web: '#00d4ff', database: '#a855f7', @@ -241,7 +301,17 @@ const CATEGORY_COLORS: Record = { remote: '#8b949e', } -function ServiceBadge({ svc, host, onRemove }: { svc: ServiceInfo; host?: string; onRemove: () => void }) { +function ServiceBadge({ + svc, + host, + onEdit, + onRemove, +}: { + svc: ServiceInfo + host?: string + onEdit: () => void + onRemove: () => void +}) { const url = getServiceUrl(svc, host) const color = CATEGORY_COLORS[svc.category ?? ''] ?? '#8b949e' @@ -261,6 +331,13 @@ function ServiceBadge({ svc, host, onRemove }: { svc: ServiceInfo; host?: string
{svc.port}/{svc.protocol} {url && } +