feat: add inline edit for services in detail panel
Replaces service badge with in-place form when pencil icon is clicked. State is scoped to nodeId so switching nodes auto-resets edit/add forms.
This commit is contained in:
@@ -18,13 +18,16 @@ export function DetailPanel({ onEdit }: DetailPanelProps) {
|
|||||||
const { nodes, selectedNodeId, setSelectedNode, deleteNode, updateNode } = useCanvasStore()
|
const { nodes, selectedNodeId, setSelectedNode, deleteNode, updateNode } = useCanvasStore()
|
||||||
const node = nodes.find((n) => n.id === selectedNodeId)
|
const node = nodes.find((n) => n.id === selectedNodeId)
|
||||||
|
|
||||||
const [addingService, setAddingService] = useState(false)
|
const [addingForNode, setAddingForNode] = useState<string | null>(null)
|
||||||
const [newSvc, setNewSvc] = useState<SvcForm>(EMPTY_FORM)
|
const [newSvc, setNewSvc] = useState<SvcForm>(EMPTY_FORM)
|
||||||
const [editingIndex, setEditingIndex] = useState<number | null>(null)
|
const [editingFor, setEditingFor] = useState<{ nodeId: string; index: number } | null>(null)
|
||||||
const [editSvc, setEditSvc] = useState<SvcForm>(EMPTY_FORM)
|
const [editSvc, setEditSvc] = useState<SvcForm>(EMPTY_FORM)
|
||||||
|
|
||||||
if (!node || node.data.type === 'groupRect') return null
|
if (!node || node.data.type === 'groupRect') return null
|
||||||
|
|
||||||
|
const addingService = addingForNode === node.id
|
||||||
|
const editingIndex = editingFor?.nodeId === node.id ? editingFor.index : null
|
||||||
|
|
||||||
const { data } = node
|
const { data } = node
|
||||||
const statusColor = STATUS_COLORS[data.status]
|
const statusColor = STATUS_COLORS[data.status]
|
||||||
const host = data.ip ?? data.hostname
|
const host = data.ip ?? data.hostname
|
||||||
@@ -45,33 +48,34 @@ export function DetailPanel({ onEdit }: DetailPanelProps) {
|
|||||||
}
|
}
|
||||||
updateNode(node.id, { services: [...(data.services ?? []), svc] })
|
updateNode(node.id, { services: [...(data.services ?? []), svc] })
|
||||||
setNewSvc(EMPTY_FORM)
|
setNewSvc(EMPTY_FORM)
|
||||||
setAddingService(false)
|
setAddingForNode(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRemoveService = (index: number) => {
|
const handleRemoveService = (index: number) => {
|
||||||
const updated = data.services.filter((_, i) => i !== index)
|
const updated = (data.services ?? []).filter((_, i) => i !== index)
|
||||||
updateNode(node.id, { services: updated })
|
updateNode(node.id, { services: updated })
|
||||||
if (editingIndex === index) setEditingIndex(null)
|
if (editingIndex === index) setEditingFor(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleStartEdit = (index: number) => {
|
const handleStartEdit = (index: number) => {
|
||||||
const svc = data.services[index]
|
const svc = (data.services ?? [])[index]
|
||||||
|
if (!svc) return
|
||||||
setEditSvc({ port: String(svc.port), protocol: svc.protocol, service_name: svc.service_name })
|
setEditSvc({ port: String(svc.port), protocol: svc.protocol, service_name: svc.service_name })
|
||||||
setEditingIndex(index)
|
setEditingFor({ nodeId: node.id, index })
|
||||||
setAddingService(false)
|
setAddingForNode(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSaveEdit = () => {
|
const handleSaveEdit = () => {
|
||||||
if (editingIndex === null) return
|
if (editingIndex === null) return
|
||||||
const port = parseInt(editSvc.port, 10)
|
const port = parseInt(editSvc.port, 10)
|
||||||
if (!editSvc.service_name.trim() || isNaN(port) || port < 1 || port > 65535) return
|
if (!editSvc.service_name.trim() || isNaN(port) || port < 1 || port > 65535) return
|
||||||
const updated = data.services.map((svc, i) =>
|
const updated = (data.services ?? []).map((svc, i) =>
|
||||||
i === editingIndex
|
i === editingIndex
|
||||||
? { ...svc, port, protocol: editSvc.protocol, service_name: editSvc.service_name.trim() }
|
? { ...svc, port, protocol: editSvc.protocol, service_name: editSvc.service_name.trim() }
|
||||||
: svc
|
: svc
|
||||||
)
|
)
|
||||||
updateNode(node.id, { services: updated })
|
updateNode(node.id, { services: updated })
|
||||||
setEditingIndex(null)
|
setEditingFor(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -141,7 +145,7 @@ export function DetailPanel({ onEdit }: DetailPanelProps) {
|
|||||||
Services{data.services.length > 0 ? ` (${data.services.length})` : ''}
|
Services{data.services.length > 0 ? ` (${data.services.length})` : ''}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => { setAddingService((v) => !v); setEditingIndex(null) }}
|
onClick={() => { setAddingForNode((v) => v === node.id ? null : node.id); setEditingFor(null) }}
|
||||||
className="flex items-center gap-1 text-[10px] text-[#00d4ff] hover:text-[#00d4ff]/80 transition-colors"
|
className="flex items-center gap-1 text-[10px] text-[#00d4ff] hover:text-[#00d4ff]/80 transition-colors"
|
||||||
>
|
>
|
||||||
<Plus size={10} /> Add
|
<Plus size={10} /> Add
|
||||||
@@ -154,7 +158,7 @@ export function DetailPanel({ onEdit }: DetailPanelProps) {
|
|||||||
form={newSvc}
|
form={newSvc}
|
||||||
onChange={setNewSvc}
|
onChange={setNewSvc}
|
||||||
onConfirm={handleAddService}
|
onConfirm={handleAddService}
|
||||||
onCancel={() => setAddingService(false)}
|
onCancel={() => setAddingForNode(null)}
|
||||||
confirmLabel="Add"
|
confirmLabel="Add"
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
@@ -169,7 +173,7 @@ export function DetailPanel({ onEdit }: DetailPanelProps) {
|
|||||||
form={editSvc}
|
form={editSvc}
|
||||||
onChange={setEditSvc}
|
onChange={setEditSvc}
|
||||||
onConfirm={handleSaveEdit}
|
onConfirm={handleSaveEdit}
|
||||||
onCancel={() => setEditingIndex(null)}
|
onCancel={() => setEditingFor(null)}
|
||||||
confirmLabel="Save"
|
confirmLabel="Save"
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user