/** * ServicesPage — LoadBalancer services managed by kube-vip. * * Shows all type:LoadBalancer services with VIP assignments, ports, * kube-vip annotations, and egress status. */ import { Loader, NameValueTable, SectionBox, SectionHeader, SimpleTable, StatusLabel, } from '@kinvolk/headlamp-plugin/lib/CommonComponents'; import React, { useCallback, useEffect } from 'react'; import { useHistory, useLocation } from 'react-router-dom'; import { formatAge, getServiceVIPs, getVipHost, isEgressEnabled, isKubeVipService, isServiceIgnored, KUBE_VIP_ANNOTATION_PREFIX, KubeVipService, } from '../api/k8s'; import { useKubeVipContext } from '../api/KubeVipDataContext'; export default function ServicesPage() { const { loadBalancerServices, loading, error } = useKubeVipContext(); const location = useLocation(); const history = useHistory(); const selectedName = location.hash ? decodeURIComponent(location.hash.slice(1)) : null; const selectedService = selectedName ? loadBalancerServices.find(s => `${s.metadata.namespace}/${s.metadata.name}` === selectedName) : null; const closePanel = useCallback( () => history.push(location.pathname), [history, location.pathname] ); useEffect(() => { if (!selectedName) return; const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') closePanel(); }; window.addEventListener('keydown', handler); return () => window.removeEventListener('keydown', handler); }, [selectedName, closePanel]); if (loading) { return ; } if (error) { return ( {error} }]} /> ); } if (loadBalancerServices.length === 0) { return ( <>

No LoadBalancer services found.

); } return ( <> ( { e.preventDefault(); history.push(`${location.pathname}#${s.metadata.namespace}/${s.metadata.name}`); }} style={{ color: 'var(--mui-palette-primary-main, #1976d2)', cursor: 'pointer' }} > {s.metadata.name} ), }, { label: 'Namespace', getter: s => s.metadata.namespace ?? '—' }, { label: 'VIP', getter: s => getServiceVIPs(s).join(', ') || 'Pending' }, { label: 'Ports', getter: s => s.spec.ports ?.map( (p: { port: number; protocol?: string }) => `${p.port}/${p.protocol ?? 'TCP'}` ) .join(', ') ?? '—', }, { label: 'VIP Host', getter: s => getVipHost(s) ?? '—' }, { label: 'kube-vip', getter: s => ( {isKubeVipService(s) ? 'Yes' : '—'} ), }, { label: 'Egress', getter: s => (isEgressEnabled(s) ? 'Yes' : '—'), }, { label: 'Age', getter: s => formatAge(s.metadata.creationTimestamp) }, ]} data={loadBalancerServices} /> {/* Detail slide-in panel */} {selectedService && ( <>
)} ); } function ServiceDetailPanel({ service, onClose, }: { service: KubeVipService; onClose: () => void; }) { const vips = getServiceVIPs(service); const vipHost = getVipHost(service); const annotations = service.metadata.annotations ?? {}; const kubeVipAnnotations = Object.entries(annotations).filter(([key]) => key.startsWith(KUBE_VIP_ANNOTATION_PREFIX) ); return (

Service Details

{service.spec.ports && service.spec.ports.length > 0 && ( p.name ?? '—' }, { label: 'Port', getter: p => String(p.port) }, { label: 'Target', getter: p => String(p.targetPort ?? '—') }, { label: 'Protocol', getter: p => p.protocol ?? 'TCP' }, ...(service.spec.ports?.some((p: { nodePort?: number }) => p.nodePort) ? [ { label: 'NodePort', getter: (p: { nodePort?: number }) => String(p.nodePort ?? '—'), }, ] : []), ]} data={service.spec.ports} /> )} {kubeVipAnnotations.length > 0 && ( ({ name: key.replace(KUBE_VIP_ANNOTATION_PREFIX, ''), value, }))} /> )} {isServiceIgnored(service) && ( This service has kube-vip.io/ignore=true — kube-vip will not manage it ), }, ]} /> )}
); }