/**
* 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
),
},
]}
/>
)}
);
}