feat: service badges with clickable port links in detail panel
- Services now show as full-width rows with color-coded category dot - HTTP/HTTPS services are clickable links (opens ip:port in new tab) - Supports ports: 80, 443, 8080, 8443, 8000, 3000, 8888, 9000 + name-based detection - ExternalLink icon shown on clickable services - Category color coding: web=cyan, db=purple, monitoring=green, storage=amber, security=red - Service count shown in section header
This commit is contained in:
@@ -1,8 +1,7 @@
|
|||||||
import { X, Edit, Trash2 } from 'lucide-react'
|
import { X, Edit, Trash2, ExternalLink } from 'lucide-react'
|
||||||
import { Badge } from '@/components/ui/badge'
|
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { useCanvasStore } from '@/stores/canvasStore'
|
import { useCanvasStore } from '@/stores/canvasStore'
|
||||||
import { NODE_TYPE_LABELS, STATUS_COLORS } from '@/types'
|
import { NODE_TYPE_LABELS, STATUS_COLORS, type ServiceInfo } from '@/types'
|
||||||
|
|
||||||
interface DetailPanelProps {
|
interface DetailPanelProps {
|
||||||
onEdit: (id: string) => void
|
onEdit: (id: string) => void
|
||||||
@@ -64,16 +63,10 @@ export function DetailPanel({ onEdit }: DetailPanelProps) {
|
|||||||
{/* Services */}
|
{/* Services */}
|
||||||
{data.services.length > 0 && (
|
{data.services.length > 0 && (
|
||||||
<div className="px-4 py-3 border-t border-border">
|
<div className="px-4 py-3 border-t border-border">
|
||||||
<div className="text-xs text-muted-foreground mb-2">Services</div>
|
<div className="text-xs text-muted-foreground mb-2">Services ({data.services.length})</div>
|
||||||
<div className="flex flex-wrap gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
{data.services.map((svc) => (
|
{data.services.map((svc) => (
|
||||||
<Badge
|
<ServiceBadge key={`${svc.port}-${svc.protocol}`} svc={svc} ip={data.ip} />
|
||||||
key={`${svc.port}-${svc.protocol}`}
|
|
||||||
variant="secondary"
|
|
||||||
className="font-mono text-xs bg-[#21262d] text-[#8b949e] border-[#30363d]"
|
|
||||||
>
|
|
||||||
{svc.port}/{svc.protocol} {svc.service_name}
|
|
||||||
</Badge>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -123,3 +116,59 @@ function DetailRow({ label, value, mono }: { label: string; value: string; mono?
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CATEGORY_COLORS: Record<string, string> = {
|
||||||
|
web: '#00d4ff',
|
||||||
|
database: '#a855f7',
|
||||||
|
monitoring: '#39d353',
|
||||||
|
storage: '#e3b341',
|
||||||
|
security: '#f85149',
|
||||||
|
remote: '#8b949e',
|
||||||
|
}
|
||||||
|
|
||||||
|
function getServiceUrl(svc: ServiceInfo, ip?: string): string | null {
|
||||||
|
if (!ip) return null
|
||||||
|
const name = svc.service_name.toLowerCase()
|
||||||
|
const isHttps = name.includes('https') || name.includes('ssl') || svc.port === 443 || svc.port === 8443
|
||||||
|
const isHttp = name.includes('http') || [80, 8080, 8000, 3000, 8888, 9000, 8090, 7080].includes(svc.port)
|
||||||
|
if (isHttps) return `https://${ip}:${svc.port}`
|
||||||
|
if (isHttp) return `http://${ip}:${svc.port}`
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function ServiceBadge({ svc, ip }: { svc: ServiceInfo; ip?: string }) {
|
||||||
|
const url = getServiceUrl(svc, ip)
|
||||||
|
const color = CATEGORY_COLORS[svc.category ?? ''] ?? '#8b949e'
|
||||||
|
|
||||||
|
const inner = (
|
||||||
|
<div
|
||||||
|
className="flex items-center justify-between gap-2 px-2 py-1.5 rounded-md border text-xs transition-colors"
|
||||||
|
style={{
|
||||||
|
background: '#21262d',
|
||||||
|
borderColor: '#30363d',
|
||||||
|
...(url ? { cursor: 'pointer' } : {}),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-1.5 min-w-0">
|
||||||
|
<span
|
||||||
|
className="shrink-0 w-1.5 h-1.5 rounded-full"
|
||||||
|
style={{ backgroundColor: color }}
|
||||||
|
/>
|
||||||
|
<span className="font-medium truncate" style={{ color }}>{svc.service_name}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5 shrink-0">
|
||||||
|
<span className="font-mono text-[#8b949e]">{svc.port}/{svc.protocol}</span>
|
||||||
|
{url && <ExternalLink size={10} className="text-muted-foreground" />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
if (url) {
|
||||||
|
return (
|
||||||
|
<a href={url} target="_blank" rel="noopener noreferrer" className="block hover:opacity-80 transition-opacity">
|
||||||
|
{inner}
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return inner
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user