From 1f1dfd807bbc07b8d79821c5344aac1b69ad8d35 Mon Sep 17 00:00:00 2001 From: Pouzor Date: Tue, 10 Mar 2026 17:29:41 +0100 Subject: [PATCH] feat: add/remove services in detail panel + fix service URL detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Any TCP service (except port 22 and known non-HTTP protocols) is now clickable — fixes Sonarr, Radarr, Jellyfin, Proxmox web UI, etc. - HTTPS auto-detected for port 443/8443 or names containing ssl/tls/https - Non-HTTP blocklist: FTP, SMTP, DNS, LDAP, SMB, MySQL, Postgres, Redis, MongoDB, Kafka, Memcached, etc. - Inline "Add service" form in detail panel (name, port, tcp/udp) - Remove button (×) on hover for each service row - Group rect nodes excluded from detail panel - getServiceUrl extracted to utils/serviceUrl.ts - 13 new tests (79 total, all pass) --- .../src/components/panels/DetailPanel.tsx | 167 +++++++++++++----- .../panels/__tests__/DetailPanel.test.ts | 66 +++++++ frontend/src/utils/serviceUrl.ts | 34 ++++ 3 files changed, 223 insertions(+), 44 deletions(-) create mode 100644 frontend/src/components/panels/__tests__/DetailPanel.test.ts create mode 100644 frontend/src/utils/serviceUrl.ts diff --git a/frontend/src/components/panels/DetailPanel.tsx b/frontend/src/components/panels/DetailPanel.tsx index 71cbe7d..8dc8843 100644 --- a/frontend/src/components/panels/DetailPanel.tsx +++ b/frontend/src/components/panels/DetailPanel.tsx @@ -1,20 +1,31 @@ -import { X, Edit, Trash2, ExternalLink } from 'lucide-react' +import { useState } from 'react' +import { X, Edit, Trash2, ExternalLink, Plus } from 'lucide-react' import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' import { useCanvasStore } from '@/stores/canvasStore' import { NODE_TYPE_LABELS, STATUS_COLORS, type ServiceInfo } from '@/types' +import { getServiceUrl } from '@/utils/serviceUrl' interface DetailPanelProps { onEdit: (id: string) => void } export function DetailPanel({ onEdit }: DetailPanelProps) { - const { nodes, selectedNodeId, setSelectedNode, deleteNode } = useCanvasStore() + const { nodes, selectedNodeId, setSelectedNode, deleteNode, updateNode } = useCanvasStore() const node = nodes.find((n) => n.id === selectedNodeId) - if (!node) return null + const [addingService, setAddingService] = useState(false) + const [newSvc, setNewSvc] = useState<{ port: string; protocol: 'tcp' | 'udp'; service_name: string }>({ + port: '', + protocol: 'tcp', + service_name: '', + }) + + if (!node || node.data.type === 'groupRect') return null const { data } = node const statusColor = STATUS_COLORS[data.status] + const host = data.ip ?? data.hostname const handleDelete = () => { if (confirm(`Delete "${data.label}"?`)) { @@ -22,6 +33,24 @@ export function DetailPanel({ onEdit }: DetailPanelProps) { } } + const handleAddService = () => { + const port = parseInt(newSvc.port, 10) + if (!newSvc.service_name.trim() || isNaN(port) || port < 1 || port > 65535) return + const svc: ServiceInfo = { + port, + protocol: newSvc.protocol, + service_name: newSvc.service_name.trim(), + } + updateNode(node.id, { services: [...(data.services ?? []), svc] }) + setNewSvc({ port: '', protocol: 'tcp', service_name: '' }) + setAddingService(false) + } + + const handleRemoveService = (index: number) => { + const updated = data.services.filter((_, i) => i !== index) + updateNode(node.id, { services: updated }) + } + return (