-
-
-
-
Servers
+ {stats.map(({ label, value, icon: Icon }) => (
+
+
+
+ {label}
+
+
{value}
-
{metadata.totalServers}
-
-
-
-
- Proxies
-
-
{metadata.totalProxies}
-
-
-
-
- Nodes
-
-
{metadata.totalK8sNodes}
-
+ ))}
- {/* Health Status */}
-
-
+
{metadata.healthySystems} Healthy
-
-
-
+
+
{metadata.degradedSystems} Degraded
-
-
-
+
+
{metadata.unhealthySystems} Down
-
+
- {/* Search */}
- {/* Filters */}
diff --git a/apps/web/components/topology/layouts/hierarchical-layout.ts b/apps/web/components/topology/layouts/hierarchical-layout.ts
index d83fe25..232a908 100644
--- a/apps/web/components/topology/layouts/hierarchical-layout.ts
+++ b/apps/web/components/topology/layouts/hierarchical-layout.ts
@@ -1,10 +1,10 @@
import type { TopologyEdge, TopologyNode } from "@/lib/topology-types";
const LAYOUT_CONFIG = {
- TIER_SPACING: 300, // Vertical spacing between proxy and server tiers
- NODE_SPACING: 250, // Horizontal spacing between nodes
- START_X: 150, // Left padding
- START_Y: 100, // Top padding
+ TIER_SPACING: 300,
+ NODE_SPACING: 250,
+ START_X: 150,
+ START_Y: 100,
} as const;
export function applyHierarchicalLayout(
diff --git a/apps/web/components/topology/nodes/k8s-node.tsx b/apps/web/components/topology/nodes/k8s-node.tsx
index 0b58ffe..c5d05fe 100644
--- a/apps/web/components/topology/nodes/k8s-node.tsx
+++ b/apps/web/components/topology/nodes/k8s-node.tsx
@@ -1,142 +1,100 @@
import type { NodeProps } from "@xyflow/react";
-import { Handle, Position } from "@xyflow/react";
import { Box, Cpu, HardDrive, Network, Server as ServerIcon } from "lucide-react";
import { Badge } from "@/components/ui/badge";
-import { cn } from "@/lib/cn";
import type { K8sNodeMetadata, TopologyNodeData } from "@/lib/topology-types";
+import { CompactRow, TopologyNodeCard } from "../topology-primitives";
export function K8sNodeComponent({ data, selected }: NodeProps) {
const nodeData = data as TopologyNodeData;
const metadata = nodeData.metadata as K8sNodeMetadata;
const { node, podCount, serverPods, proxyPods, health, metrics } = metadata;
- const getStatusBadge = () => {
- switch (health) {
- case "healthy":
- return
Healthy;
- case "degraded":
- return
Degraded;
- case "unhealthy":
- return
Unhealthy;
- default:
- return (
-
- Unknown
-
- );
- }
- };
-
return (
-
}
+ iconClassName="bg-blue-500/10"
+ title={node.name || "Unknown"}
+ description="Kubernetes Node"
+ health={health}
>
-
-
-
-
-
-
-
-
-
-
{node.name || "Unknown"}
-
Kubernetes Node
-
-
- {getStatusBadge()}
-
-
-
+
+
+ {node.status}
+
+ {node.version && (
- {node.status}
+ {node.version}
- {node.version && (
-
- {node.version}
-
- )}
-
- {node.roles}
-
-
-
-
- Total Pods
- {podCount}
-
-
-
-
-
- Server Pods
-
- {serverPods.length}
-
-
-
- Proxy Pods
-
- {proxyPods.length}
-
-
-
- {metrics && (metrics.cpuUsage || metrics.memoryUsage) && (
-
- {metrics.cpuUsage && (
-
-
- CPU
-
- {metrics.cpuUsage}
-
- )}
- {metrics.memoryUsage && (
-
-
- Memory
-
- {metrics.memoryUsage}
-
- )}
-
)}
+
+ {node.roles}
+
+
+
+ {podCount}
+
+
+
+ }>
+ {serverPods.length}
+
+ }>
+ {proxyPods.length}
+
+
+
+ {metrics && (metrics.cpuUsage || metrics.memoryUsage) && (
-
- Age
- {node.age}
-
- {node.hostname && (
-
- Hostname
- {node.hostname}
-
+ {metrics.cpuUsage && (
+
}
+ valueClassName="font-semibold text-xs text-blue-600"
+ >
+ {metrics.cpuUsage}
+
+ )}
+ {metrics.memoryUsage && (
+
}
+ valueClassName="font-semibold text-xs text-blue-600"
+ >
+ {metrics.memoryUsage}
+
)}
+ )}
- {(node.internalIP || node.externalIP) && (
-
- {node.internalIP && (
-
- Internal IP
- {node.internalIP}
-
- )}
- {node.externalIP && (
-
- External IP
- {node.externalIP}
-
- )}
-
+
+ {node.age}
+ {node.hostname && (
+
+ {node.hostname}
+
)}
-
-
+ {(node.internalIP || node.externalIP) && (
+
+ {node.internalIP && (
+
+ {node.internalIP}
+
+ )}
+ {node.externalIP && (
+
+ {node.externalIP}
+
+ )}
+
+ )}
+
);
}
diff --git a/apps/web/components/topology/nodes/proxy-node.tsx b/apps/web/components/topology/nodes/proxy-node.tsx
index 869d752..6775542 100644
--- a/apps/web/components/topology/nodes/proxy-node.tsx
+++ b/apps/web/components/topology/nodes/proxy-node.tsx
@@ -1,206 +1,105 @@
import type { NodeProps } from "@xyflow/react";
-import { Handle, Position } from "@xyflow/react";
-import { Box, Check, Copy, Cpu, Globe, HardDrive, Network } from "lucide-react";
-import { useState } from "react";
+import { Box, Cpu, Globe, HardDrive, Network } from "lucide-react";
import { Badge } from "@/components/ui/badge";
-import { Button } from "@/components/ui/button";
import { cn } from "@/lib/cn";
import type { ProxyMetadata, TopologyNodeData } from "@/lib/topology-types";
+import { CompactRow, CopyableCode, MetricRow, TopologyNodeCard } from "../topology-primitives";
export function ProxyNode({ data, selected }: NodeProps) {
- const nodeData = data as TopologyNodeData | TopologyNodeData;
- const metadata = nodeData.metadata as ProxyMetadata | ProxyMetadata;
+ const nodeData = data as TopologyNodeData;
+ const metadata = nodeData.metadata as ProxyMetadata;
const { proxy, readyPods, podCount, health, connectedServers } = metadata;
-
- const k8sNodes = "k8sNodes" in metadata ? metadata.k8sNodes : [];
- const connectionInfo = "connectionInfo" in metadata ? metadata.connectionInfo : null;
- const metrics = "metrics" in metadata ? metadata.metrics : undefined;
-
- const [copied, setCopied] = useState(false);
-
- const handleCopy = async (e: React.MouseEvent) => {
- e.stopPropagation();
- if (connectionInfo?.connectionString) {
- await navigator.clipboard.writeText(connectionInfo.connectionString);
- setCopied(true);
- setTimeout(() => setCopied(false), 2000);
- }
- };
-
- const getStatusBadge = () => {
- switch (health) {
- case "healthy":
- return
Healthy;
- case "degraded":
- return
Degraded;
- case "unhealthy":
- return
Unhealthy;
- default:
- return (
-
- Unknown
-
- );
- }
- };
+ const { k8sNodes, connectionInfo, metrics, pods } = metadata;
+ const restartCount = pods.reduce((sum, pod) => sum + (pod.restarts || 0), 0);
return (
-
}
+ iconClassName="bg-blue-500/10"
+ title={proxy.id}
+ description={proxy.description}
+ health={health}
>
-
-
-
-
-
-
-
-
-
-
{proxy.id}
- {proxy.description && (
-
{proxy.description}
- )}
-
-
- {getStatusBadge()}
-
-
-
+
+
+ {proxy.type}
+
+ {connectionInfo && (
- {proxy.type}
+ {connectionInfo.type}
- {connectionInfo && (
-
- {connectionInfo.type}
-
- )}
-
-
-
- Pods
-
- {readyPods}/{podCount}
-
-
-
- {proxy.node_port && (
-
-
-
- NodePort
-
- {proxy.node_port}
-
-
- )}
-
- {connectionInfo?.connectionString && (
-
-
- Address
-
-
-
- {connectionInfo.connectionString}
-
-
-
- {connectionInfo.note && (
-
{connectionInfo.note}
- )}
-
- )}
-
-
-
-
- Memory
-
-
- {metrics?.memoryUsage && (
- {metrics.memoryUsage} /
- )}
- {proxy.memory}MB
-
-
-
-
- CPU
-
-
- {metrics?.cpuUsage && {metrics.cpuUsage} / }
-
- {proxy.cpu_request || "N/A"}/{proxy.cpu_limit || "N/A"}
-
-
-
-
-
- {"pods" in metadata && metadata.pods && metadata.pods.length > 0 && (
-
-
- Restarts
- sum + (p.restarts || 0), 0) > 0
- ? "text-yellow-600"
- : "text-green-600"
- )}
- >
- {metadata.pods.reduce((sum, p) => sum + (p.restarts || 0), 0)}
-
-
- {metadata.pods[0]?.age && (
-
- Age
- {metadata.pods[0].age}
-
- )}
- {metadata.pods[0]?.ip && (
-
- Pod IP
- {metadata.pods[0].ip}
-
- )}
-
- Routing To
- {connectedServers.length} servers
-
- {k8sNodes.length > 0 && (
-
-
- K8s Node
-
- {k8sNodes[0]}
-
- )}
-
)}
-
-
+
+ {readyPods}/{podCount}
+
+
+ {proxy.node_port && (
+
+ }>
+ {proxy.node_port}
+
+
+ )}
+
+ {connectionInfo?.connectionString && (
+
+
+
+ {connectionInfo.note && (
+
{connectionInfo.note}
+ )}
+
+ )}
+
+
+ }
+ usage={metrics?.memoryUsage}
+ limit={`${proxy.memory}MB`}
+ />
+ }
+ usage={metrics?.cpuUsage}
+ limit={`${proxy.cpu_request || "N/A"}/${proxy.cpu_limit || "N/A"}`}
+ />
+
+
+ {pods.length > 0 && (
+
+ 0 ? "text-yellow-600" : "text-green-600"}
+ >
+ {restartCount}
+
+ {pods[0]?.age && {pods[0].age}}
+ {pods[0]?.ip && (
+
+ {pods[0].ip}
+
+ )}
+
+ {connectedServers.length} servers
+
+ {k8sNodes.length > 0 && (
+ }>
+ {k8sNodes[0]}
+
+ )}
+
+ )}
+
);
}
diff --git a/apps/web/components/topology/nodes/server-node.tsx b/apps/web/components/topology/nodes/server-node.tsx
index 247a460..7d77c88 100644
--- a/apps/web/components/topology/nodes/server-node.tsx
+++ b/apps/web/components/topology/nodes/server-node.tsx
@@ -1,210 +1,109 @@
import type { NodeProps } from "@xyflow/react";
-import { Handle, Position } from "@xyflow/react";
-import { Box, Check, Copy, Cpu, HardDrive, Server } from "lucide-react";
-import { useState } from "react";
+import { Box, Cpu, HardDrive, Server } from "lucide-react";
import { Badge } from "@/components/ui/badge";
-import { Button } from "@/components/ui/button";
import { cn } from "@/lib/cn";
import type { ServerMetadata, TopologyNodeData } from "@/lib/topology-types";
+import { CompactRow, CopyableCode, MetricRow, TopologyNodeCard } from "../topology-primitives";
export function ServerNode({ data, selected }: NodeProps) {
- const nodeData = data as TopologyNodeData | TopologyNodeData;
- const metadata = nodeData.metadata as ServerMetadata | ServerMetadata;
+ const nodeData = data as TopologyNodeData;
+ const metadata = nodeData.metadata as ServerMetadata;
const { server, readyPods, podCount, health } = metadata;
-
- const k8sNodes = "k8sNodes" in metadata ? metadata.k8sNodes : [];
- const connectedProxies = "connectedProxies" in metadata ? metadata.connectedProxies : [];
- const connectionInfo = "connectionInfo" in metadata ? metadata.connectionInfo : null;
- const metrics = "metrics" in metadata ? metadata.metrics : undefined;
-
- const [copied, setCopied] = useState(false);
-
- const handleCopy = async (e: React.MouseEvent) => {
- e.stopPropagation();
- if (connectionInfo?.connectionString) {
- await navigator.clipboard.writeText(connectionInfo.connectionString);
- setCopied(true);
- setTimeout(() => setCopied(false), 2000);
- }
- };
-
- const getStatusBadge = () => {
- switch (health) {
- case "healthy":
- return
Healthy;
- case "degraded":
- return
Degraded;
- case "unhealthy":
- return
Unhealthy;
- default:
- return (
-
- Unknown
-
- );
- }
- };
+ const { k8sNodes, connectedProxies, connectionInfo, metrics, pods } = metadata;
+ const restartCount = pods.reduce((sum, pod) => sum + (pod.restarts || 0), 0);
return (
-
}
+ iconClassName="bg-primary/10"
+ title={server.id}
+ description={server.description}
+ health={health}
>
-
-
-
-
-
-
-
-
-
-
{server.id}
- {server.description && (
-
{server.description}
- )}
-
-
- {getStatusBadge()}
-
-
-
+
+
+ {server.jar_type}
+
+
+ MC {server.minecraft_version}
+
+
+ {server.type}
+
+ {connectionInfo && (
- {server.jar_type}
+ {connectionInfo.type}
-
- MC {server.minecraft_version}
-
-
- {server.type}
-
- {connectionInfo && (
-
- {connectionInfo.type}
-
- )}
-
-
-
- Pods
-
- {readyPods}/{podCount}
-
-
-
-
-
-
- Memory
-
-
- {metrics?.memoryUsage && (
- {metrics.memoryUsage} /
- )}
-
- {server.memory_request}/{server.memory}MB
-
-
-
-
-
- CPU
-
-
- {metrics?.cpuUsage && {metrics.cpuUsage} / }
-
- {server.cpu_request || "N/A"}/{server.cpu_limit || "N/A"}
-
-
-
-
-
- {connectionInfo?.connectionString && (
-
-
-
- {connectionInfo.connectionString}
-
-
-
- {connectionInfo.note && (
-
{connectionInfo.note}
- )}
-
- )}
-
- {"pods" in metadata && metadata.pods && metadata.pods.length > 0 && (
-
-
- Restarts
- sum + (p.restarts || 0), 0) > 0
- ? "text-yellow-600"
- : "text-green-600"
- )}
- >
- {metadata.pods.reduce((sum, p) => sum + (p.restarts || 0), 0)}
-
-
- {metadata.pods[0]?.age && (
-
- Age
- {metadata.pods[0].age}
-
- )}
- {metadata.pods[0]?.ip && (
-
- Pod IP
- {metadata.pods[0].ip}
-
- )}
-
- )}
-
- {(k8sNodes.length > 0 || connectedProxies.length > 0) && (
-
- {connectedProxies.length > 0 && (
-
- Exposed By
-
- {connectedProxies.length} proxies
-
-
- )}
- {k8sNodes.length > 0 && (
-
-
- K8s Node
-
- {k8sNodes[0]}
-
- )}
-
)}
-
-
+
+ {readyPods}/{podCount}
+
+
+
+ }
+ usage={metrics?.memoryUsage}
+ limit={`${server.memory_request}/${server.memory}MB`}
+ />
+ }
+ usage={metrics?.cpuUsage}
+ limit={`${server.cpu_request || "N/A"}/${server.cpu_limit || "N/A"}`}
+ />
+
+
+ {connectionInfo?.connectionString && (
+
+
+ {connectionInfo.note && (
+
{connectionInfo.note}
+ )}
+
+ )}
+
+ {pods.length > 0 && (
+
+ 0 ? "text-yellow-600" : "text-green-600"}
+ >
+ {restartCount}
+
+ {pods[0]?.age && {pods[0].age}}
+ {pods[0]?.ip && (
+
+ {pods[0].ip}
+
+ )}
+
+ )}
+
+ {(k8sNodes.length > 0 || connectedProxies.length > 0) && (
+
+ {connectedProxies.length > 0 && (
+
+ {connectedProxies.length} proxies
+
+ )}
+ {k8sNodes.length > 0 && (
+ }>
+ {k8sNodes[0]}
+
+ )}
+
+ )}
+
);
}
diff --git a/apps/web/components/topology/topology-canvas.tsx b/apps/web/components/topology/topology-canvas.tsx
index 5fc80d5..95505ad 100644
--- a/apps/web/components/topology/topology-canvas.tsx
+++ b/apps/web/components/topology/topology-canvas.tsx
@@ -52,7 +52,7 @@ export function TopologyCanvas({ graph }: TopologyCanvasProps) {
}, []);
return (
-
+
-
+
{
@@ -103,8 +103,8 @@ export function TopologyCanvas({ graph }: TopologyCanvasProps) {
};
return colors[data.status] || "#94a3b8";
}}
- maskColor="rgba(0, 0, 0, 0.05)"
- className="bg-white/80 backdrop-blur-sm border shadow-lg"
+ maskColor="rgba(29, 31, 26, 0.08)"
+ className="border bg-card/95 shadow-md backdrop-blur-sm"
/>
diff --git a/apps/web/components/topology/topology-primitives.tsx b/apps/web/components/topology/topology-primitives.tsx
new file mode 100644
index 0000000..02b7aa2
--- /dev/null
+++ b/apps/web/components/topology/topology-primitives.tsx
@@ -0,0 +1,214 @@
+"use client";
+
+import { Handle, Position } from "@xyflow/react";
+import { Check, Copy } from "lucide-react";
+import type { MouseEvent, ReactNode } from "react";
+import { useState } from "react";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import { cn } from "@/lib/cn";
+import type { HealthStatus } from "@/lib/topology-types";
+
+const healthLabels: Record = {
+ healthy: "Healthy",
+ degraded: "Degraded",
+ unhealthy: "Unhealthy",
+ unknown: "Unknown",
+};
+
+const solidHealthClasses: Record, string> = {
+ healthy: "bg-green-500 hover:bg-green-600",
+ degraded: "bg-yellow-500 hover:bg-yellow-600",
+ unhealthy: "bg-red-500 hover:bg-red-600",
+};
+
+interface HealthBadgeProps {
+ status: HealthStatus;
+ appearance?: "solid" | "summary" | "detail";
+ children?: ReactNode;
+ className?: string;
+}
+
+export function HealthBadge({
+ status,
+ appearance = "solid",
+ children,
+ className,
+}: HealthBadgeProps) {
+ const content = children ?? healthLabels[status];
+
+ if (appearance === "summary") {
+ const indicatorClasses = {
+ healthy: "bg-green-500",
+ degraded: "bg-yellow-500",
+ unhealthy: "bg-red-500",
+ unknown: "bg-gray-400",
+ }[status];
+
+ return (
+
+
+ {content}
+
+ );
+ }
+
+ if (appearance === "detail") {
+ return (
+
+ {content}
+
+ );
+ }
+
+ if (status === "unknown") {
+ return (
+
+ {content}
+
+ );
+ }
+
+ return {content};
+}
+
+interface TopologyNodeCardProps {
+ selected: boolean;
+ icon: ReactNode;
+ iconClassName: string;
+ title: ReactNode;
+ description?: ReactNode;
+ health: HealthStatus;
+ children: ReactNode;
+}
+
+export function TopologyNodeCard({
+ selected,
+ icon,
+ iconClassName,
+ title,
+ description,
+ health,
+ children,
+}: TopologyNodeCardProps) {
+ return (
+
+
+
+
+
+
{icon}
+
+
{title}
+ {description && (
+
{description}
+ )}
+
+
+
+
+ {children}
+
+
+
+ );
+}
+
+interface CompactRowProps {
+ label: ReactNode;
+ icon?: ReactNode;
+ children?: ReactNode;
+ className?: string;
+ valueClassName?: string;
+}
+
+export function CompactRow({ label, icon, children, className, valueClassName }: CompactRowProps) {
+ return (
+
+
+ {icon}
+ {label}
+
+ {children !== undefined && (
+ {children}
+ )}
+
+ );
+}
+
+interface MetricRowProps {
+ label: string;
+ icon: ReactNode;
+ usage?: string;
+ limit: ReactNode;
+}
+
+export function MetricRow({ label, icon, usage, limit }: MetricRowProps) {
+ return (
+
+ {usage && {usage} / }
+ {limit}
+
+ );
+}
+
+export function DetailSection({ title, children }: { title: string; children: ReactNode }) {
+ return (
+
+ );
+}
+
+export function DetailRow({ label, value }: { label: string; value: ReactNode }) {
+ return (
+
+ {label}:
+ {value}
+
+ );
+}
+
+interface CopyableCodeProps {
+ value: string;
+ title?: string;
+}
+
+export function CopyableCode({ value, title = "Copy address" }: CopyableCodeProps) {
+ const [copied, setCopied] = useState(false);
+
+ const handleCopy = async (event: MouseEvent) => {
+ event.stopPropagation();
+ await navigator.clipboard.writeText(value);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ };
+
+ return (
+
+
+ {value}
+
+
+
+ );
+}
diff --git a/apps/web/hooks/use-graph-layout.ts b/apps/web/hooks/use-graph-layout.ts
index 5e1c453..be3fe25 100644
--- a/apps/web/hooks/use-graph-layout.ts
+++ b/apps/web/hooks/use-graph-layout.ts
@@ -70,7 +70,7 @@ export function useGraphLayout({ nodes, edges }: LayoutInput) {
const k8sNodePositions = new Map();
k8sWithApps.forEach((k8sNode) => {
- const width = k8sNodeWidths.get(k8sNode.id)!;
+ const width = k8sNodeWidths.get(k8sNode.id) ?? nodeWidth;
const centerX = currentX + width / 2;
k8sNodePositions.set(k8sNode.id, { x: centerX, width });
diff --git a/apps/web/lib/k8s-metrics.ts b/apps/web/lib/k8s-metrics.ts
index 5b98fdf..590e360 100644
--- a/apps/web/lib/k8s-metrics.ts
+++ b/apps/web/lib/k8s-metrics.ts
@@ -4,8 +4,6 @@ export interface ResourceMetrics {
memoryUsage?: string;
memoryUsagePercent?: number;
}
-
-/** Convert CPU nanoseconds (e.g. "123456789n") to millicores (e.g. "123m") */
export function parseCpuUsage(cpuNano: string): string | undefined {
const usageNano = Number.parseInt(cpuNano.replace("n", ""), 10);
if (Number.isNaN(usageNano)) return undefined;
@@ -24,8 +22,6 @@ export function calculateCpuPercent(cpuNano: string, capacityNano: string): numb
return Math.round((usageNano / capNano) * 100);
}
-
-/** Convert memory kibibytes (e.g. "1024Ki") to mebibytes (e.g. "1Mi") */
export function parseMemoryUsage(memoryKi: string): string | undefined {
const usageKi = Number.parseInt(memoryKi.replace("Ki", ""), 10);
if (Number.isNaN(usageKi)) return undefined;
@@ -44,8 +40,6 @@ export function calculateMemoryPercent(memoryKi: string, capacityKi: string): nu
return Math.round((usageKi / capKi) * 100);
}
-
-/** Parse raw K8s metrics into a standardized format with optional percentages */
export function parseK8sMetrics(
cpuUsage?: string,
memoryUsage?: string,
diff --git a/apps/web/lib/topology-types.ts b/apps/web/lib/topology-types.ts
index 426683e..8ec3d38 100644
--- a/apps/web/lib/topology-types.ts
+++ b/apps/web/lib/topology-types.ts
@@ -23,8 +23,8 @@ export interface ResourceMetrics {
export interface K8sNodeMetadata {
node: K8sNodeSummary;
podCount: number;
- serverPods: string[]; // Pod names of servers
- proxyPods: string[]; // Pod names of proxies
+ serverPods: string[];
+ proxyPods: string[];
health: HealthStatus;
metrics?: ResourceMetrics;
}
@@ -35,8 +35,8 @@ export interface ServerMetadata {
readyPods: number;
pods: PodInfo[];
health: HealthStatus;
- connectedProxies: string[]; // IDs of reverse proxies pointing to this server
- k8sNodes: string[]; // Names of K8s nodes running this server's pods
+ connectedProxies: string[];
+ k8sNodes: string[];
connectionInfo?: ConnectionInfo | null;
metrics?: ResourceMetrics;
}
@@ -47,8 +47,8 @@ export interface ProxyMetadata {
readyPods: number;
pods: PodInfo[];
health: HealthStatus;
- connectedServers: string[]; // IDs of servers this proxy routes to
- k8sNodes: string[]; // Names of K8s nodes running this proxy's pods
+ connectedServers: string[];
+ k8sNodes: string[];
connectionInfo?: ConnectionInfo | null;
metrics?: ResourceMetrics;
}