mirror of
https://github.com/YuzuZensai/Minikura.git
synced 2026-09-13 18:59:25 +00:00
🎨 style: refresh topology visualization
This commit is contained in:
@@ -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 <Badge className="bg-green-500 hover:bg-green-600 text-xs">Healthy</Badge>;
|
||||
case "degraded":
|
||||
return <Badge className="bg-yellow-500 hover:bg-yellow-600 text-xs">Degraded</Badge>;
|
||||
case "unhealthy":
|
||||
return <Badge className="bg-red-500 hover:bg-red-600 text-xs">Unhealthy</Badge>;
|
||||
default:
|
||||
return (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
Unknown
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-lg border bg-card p-3 shadow-md hover:shadow-lg transition-all w-[300px]",
|
||||
selected && "ring-2 ring-primary ring-offset-2 shadow-xl"
|
||||
)}
|
||||
<TopologyNodeCard
|
||||
selected={selected}
|
||||
icon={<Box className="h-4 w-4 text-blue-600" />}
|
||||
iconClassName="bg-blue-500/10"
|
||||
title={node.name || "Unknown"}
|
||||
description="Kubernetes Node"
|
||||
health={health}
|
||||
>
|
||||
<Handle type="target" position={Position.Top} className="w-3 h-3 !bg-gray-400" />
|
||||
|
||||
<div className="space-y-2.5">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
<div className="rounded-md bg-blue-500/10 p-1.5">
|
||||
<Box className="h-4 w-4 text-blue-600" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-bold text-sm truncate">{node.name || "Unknown"}</p>
|
||||
<p className="text-xs text-muted-foreground">Kubernetes Node</p>
|
||||
</div>
|
||||
</div>
|
||||
{getStatusBadge()}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<Badge variant="outline" className="text-[10px] h-5">
|
||||
{node.status}
|
||||
</Badge>
|
||||
{node.version && (
|
||||
<Badge variant="outline" className="text-[10px] h-5">
|
||||
{node.status}
|
||||
{node.version}
|
||||
</Badge>
|
||||
{node.version && (
|
||||
<Badge variant="outline" className="text-[10px] h-5">
|
||||
{node.version}
|
||||
</Badge>
|
||||
)}
|
||||
<Badge variant="outline" className="text-[10px] h-5">
|
||||
{node.roles}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between text-xs bg-muted/50 rounded px-2 py-1.5">
|
||||
<span className="text-muted-foreground">Total Pods</span>
|
||||
<span className="font-semibold text-blue-600">{podCount}</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1 text-[11px]">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground flex items-center gap-1">
|
||||
<ServerIcon className="h-3 w-3" /> Server Pods
|
||||
</span>
|
||||
<span className="font-medium">{serverPods.length}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground flex items-center gap-1">
|
||||
<Network className="h-3 w-3" /> Proxy Pods
|
||||
</span>
|
||||
<span className="font-medium">{proxyPods.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{metrics && (metrics.cpuUsage || metrics.memoryUsage) && (
|
||||
<div className="space-y-1 text-[11px] pt-1 border-t">
|
||||
{metrics.cpuUsage && (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground flex items-center gap-1">
|
||||
<Cpu className="h-3 w-3" /> CPU
|
||||
</span>
|
||||
<span className="font-semibold text-xs text-blue-600">{metrics.cpuUsage}</span>
|
||||
</div>
|
||||
)}
|
||||
{metrics.memoryUsage && (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground flex items-center gap-1">
|
||||
<HardDrive className="h-3 w-3" /> Memory
|
||||
</span>
|
||||
<span className="font-semibold text-xs text-blue-600">{metrics.memoryUsage}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Badge variant="outline" className="text-[10px] h-5">
|
||||
{node.roles}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<CompactRow
|
||||
label="Total Pods"
|
||||
className="text-xs bg-muted/50 rounded px-2 py-1.5"
|
||||
valueClassName="font-semibold text-blue-600"
|
||||
>
|
||||
{podCount}
|
||||
</CompactRow>
|
||||
|
||||
<div className="space-y-1 text-[11px]">
|
||||
<CompactRow label="Server Pods" icon={<ServerIcon className="h-3 w-3" />}>
|
||||
{serverPods.length}
|
||||
</CompactRow>
|
||||
<CompactRow label="Proxy Pods" icon={<Network className="h-3 w-3" />}>
|
||||
{proxyPods.length}
|
||||
</CompactRow>
|
||||
</div>
|
||||
|
||||
{metrics && (metrics.cpuUsage || metrics.memoryUsage) && (
|
||||
<div className="space-y-1 text-[11px] pt-1 border-t">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">Age</span>
|
||||
<span className="font-medium">{node.age}</span>
|
||||
</div>
|
||||
{node.hostname && (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">Hostname</span>
|
||||
<span className="font-medium font-mono text-[10px] truncate">{node.hostname}</span>
|
||||
</div>
|
||||
{metrics.cpuUsage && (
|
||||
<CompactRow
|
||||
label="CPU"
|
||||
icon={<Cpu className="h-3 w-3" />}
|
||||
valueClassName="font-semibold text-xs text-blue-600"
|
||||
>
|
||||
{metrics.cpuUsage}
|
||||
</CompactRow>
|
||||
)}
|
||||
{metrics.memoryUsage && (
|
||||
<CompactRow
|
||||
label="Memory"
|
||||
icon={<HardDrive className="h-3 w-3" />}
|
||||
valueClassName="font-semibold text-xs text-blue-600"
|
||||
>
|
||||
{metrics.memoryUsage}
|
||||
</CompactRow>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(node.internalIP || node.externalIP) && (
|
||||
<div className="space-y-1 text-[11px] pt-1 border-t">
|
||||
{node.internalIP && (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">Internal IP</span>
|
||||
<span className="font-mono text-[10px] font-medium">{node.internalIP}</span>
|
||||
</div>
|
||||
)}
|
||||
{node.externalIP && (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">External IP</span>
|
||||
<span className="font-mono text-[10px] font-medium">{node.externalIP}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-1 text-[11px] pt-1 border-t">
|
||||
<CompactRow label="Age">{node.age}</CompactRow>
|
||||
{node.hostname && (
|
||||
<CompactRow label="Hostname" valueClassName="font-mono text-[10px] truncate">
|
||||
{node.hostname}
|
||||
</CompactRow>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Handle type="source" position={Position.Bottom} className="w-3 h-3 !bg-gray-400" />
|
||||
</div>
|
||||
{(node.internalIP || node.externalIP) && (
|
||||
<div className="space-y-1 text-[11px] pt-1 border-t">
|
||||
{node.internalIP && (
|
||||
<CompactRow label="Internal IP" valueClassName="font-mono text-[10px]">
|
||||
{node.internalIP}
|
||||
</CompactRow>
|
||||
)}
|
||||
{node.externalIP && (
|
||||
<CompactRow label="External IP" valueClassName="font-mono text-[10px]">
|
||||
{node.externalIP}
|
||||
</CompactRow>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</TopologyNodeCard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 <Badge className="bg-green-500 hover:bg-green-600 text-xs">Healthy</Badge>;
|
||||
case "degraded":
|
||||
return <Badge className="bg-yellow-500 hover:bg-yellow-600 text-xs">Degraded</Badge>;
|
||||
case "unhealthy":
|
||||
return <Badge className="bg-red-500 hover:bg-red-600 text-xs">Unhealthy</Badge>;
|
||||
default:
|
||||
return (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
Unknown
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
};
|
||||
const { k8sNodes, connectionInfo, metrics, pods } = metadata;
|
||||
const restartCount = pods.reduce((sum, pod) => sum + (pod.restarts || 0), 0);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-lg border bg-card p-3 shadow-md hover:shadow-lg transition-all w-[300px]",
|
||||
selected && "ring-2 ring-primary ring-offset-2 shadow-xl"
|
||||
)}
|
||||
<TopologyNodeCard
|
||||
selected={selected}
|
||||
icon={<Globe className="h-4 w-4 text-blue-500" />}
|
||||
iconClassName="bg-blue-500/10"
|
||||
title={proxy.id}
|
||||
description={proxy.description}
|
||||
health={health}
|
||||
>
|
||||
<Handle type="target" position={Position.Top} className="w-3 h-3 !bg-gray-400" />
|
||||
|
||||
<div className="space-y-2.5">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
<div className="rounded-md bg-blue-500/10 p-1.5">
|
||||
<Globe className="h-4 w-4 text-blue-500" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-bold text-sm truncate">{proxy.id}</p>
|
||||
{proxy.description && (
|
||||
<p className="text-xs text-muted-foreground truncate">{proxy.description}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{getStatusBadge()}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<Badge variant="outline" className="text-[10px] h-5">
|
||||
{proxy.type}
|
||||
</Badge>
|
||||
{connectionInfo && (
|
||||
<Badge variant="outline" className="text-[10px] h-5">
|
||||
{proxy.type}
|
||||
{connectionInfo.type}
|
||||
</Badge>
|
||||
{connectionInfo && (
|
||||
<Badge variant="outline" className="text-[10px] h-5">
|
||||
{connectionInfo.type}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between text-xs bg-muted/50 rounded px-2 py-1.5">
|
||||
<span className="text-muted-foreground">Pods</span>
|
||||
<span
|
||||
className={cn(
|
||||
"font-semibold",
|
||||
readyPods === podCount ? "text-green-600" : "text-yellow-600"
|
||||
)}
|
||||
>
|
||||
{readyPods}/{podCount}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{proxy.node_port && (
|
||||
<div className="space-y-1 text-[11px]">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground flex items-center gap-1">
|
||||
<Network className="h-3 w-3" /> NodePort
|
||||
</span>
|
||||
<span className="font-medium">{proxy.node_port}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{connectionInfo?.connectionString && (
|
||||
<div className="space-y-1 text-[11px] pt-1 border-t">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">Address</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<code className="text-[10px] bg-muted px-1.5 py-0.5 rounded font-mono flex-1 truncate">
|
||||
{connectionInfo.connectionString}
|
||||
</code>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-5 w-5 shrink-0"
|
||||
onClick={handleCopy}
|
||||
title={copied ? "Copied!" : "Copy address"}
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="h-3 w-3 text-green-500" />
|
||||
) : (
|
||||
<Copy className="h-3 w-3" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
{connectionInfo.note && (
|
||||
<p className="text-[10px] text-muted-foreground italic">{connectionInfo.note}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-1 text-[11px] pt-1 border-t">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground flex items-center gap-1">
|
||||
<HardDrive className="h-3 w-3" /> Memory
|
||||
</span>
|
||||
<span className="font-medium text-xs">
|
||||
{metrics?.memoryUsage && (
|
||||
<span className="text-blue-600">{metrics.memoryUsage} / </span>
|
||||
)}
|
||||
<span className="text-muted-foreground">{proxy.memory}MB</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground flex items-center gap-1">
|
||||
<Cpu className="h-3 w-3" /> CPU
|
||||
</span>
|
||||
<span className="font-medium text-xs">
|
||||
{metrics?.cpuUsage && <span className="text-blue-600">{metrics.cpuUsage} / </span>}
|
||||
<span className="text-muted-foreground">
|
||||
{proxy.cpu_request || "N/A"}/{proxy.cpu_limit || "N/A"}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{"pods" in metadata && metadata.pods && metadata.pods.length > 0 && (
|
||||
<div className="space-y-1 text-[11px] pt-1 border-t">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">Restarts</span>
|
||||
<span
|
||||
className={cn(
|
||||
"font-medium",
|
||||
metadata.pods.reduce((sum, p) => sum + (p.restarts || 0), 0) > 0
|
||||
? "text-yellow-600"
|
||||
: "text-green-600"
|
||||
)}
|
||||
>
|
||||
{metadata.pods.reduce((sum, p) => sum + (p.restarts || 0), 0)}
|
||||
</span>
|
||||
</div>
|
||||
{metadata.pods[0]?.age && (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">Age</span>
|
||||
<span className="font-medium">{metadata.pods[0].age}</span>
|
||||
</div>
|
||||
)}
|
||||
{metadata.pods[0]?.ip && (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">Pod IP</span>
|
||||
<span className="font-medium font-mono text-[10px]">{metadata.pods[0].ip}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">Routing To</span>
|
||||
<span className="font-semibold text-blue-600">{connectedServers.length} servers</span>
|
||||
</div>
|
||||
{k8sNodes.length > 0 && (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground flex items-center gap-1">
|
||||
<Box className="h-3 w-3" /> K8s Node
|
||||
</span>
|
||||
<span className="font-medium">{k8sNodes[0]}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Handle type="source" position={Position.Bottom} className="w-3 h-3 !bg-gray-400" />
|
||||
</div>
|
||||
<CompactRow
|
||||
label="Pods"
|
||||
className="text-xs bg-muted/50 rounded px-2 py-1.5"
|
||||
valueClassName={cn(
|
||||
"font-semibold",
|
||||
readyPods === podCount ? "text-green-600" : "text-yellow-600"
|
||||
)}
|
||||
>
|
||||
{readyPods}/{podCount}
|
||||
</CompactRow>
|
||||
|
||||
{proxy.node_port && (
|
||||
<div className="space-y-1 text-[11px]">
|
||||
<CompactRow label="NodePort" icon={<Network className="h-3 w-3" />}>
|
||||
{proxy.node_port}
|
||||
</CompactRow>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{connectionInfo?.connectionString && (
|
||||
<div className="space-y-1 text-[11px] pt-1 border-t">
|
||||
<CompactRow label="Address" />
|
||||
<CopyableCode value={connectionInfo.connectionString} />
|
||||
{connectionInfo.note && (
|
||||
<p className="text-[10px] text-muted-foreground italic">{connectionInfo.note}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-1 text-[11px] pt-1 border-t">
|
||||
<MetricRow
|
||||
label="Memory"
|
||||
icon={<HardDrive className="h-3 w-3" />}
|
||||
usage={metrics?.memoryUsage}
|
||||
limit={`${proxy.memory}MB`}
|
||||
/>
|
||||
<MetricRow
|
||||
label="CPU"
|
||||
icon={<Cpu className="h-3 w-3" />}
|
||||
usage={metrics?.cpuUsage}
|
||||
limit={`${proxy.cpu_request || "N/A"}/${proxy.cpu_limit || "N/A"}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{pods.length > 0 && (
|
||||
<div className="space-y-1 text-[11px] pt-1 border-t">
|
||||
<CompactRow
|
||||
label="Restarts"
|
||||
valueClassName={restartCount > 0 ? "text-yellow-600" : "text-green-600"}
|
||||
>
|
||||
{restartCount}
|
||||
</CompactRow>
|
||||
{pods[0]?.age && <CompactRow label="Age">{pods[0].age}</CompactRow>}
|
||||
{pods[0]?.ip && (
|
||||
<CompactRow label="Pod IP" valueClassName="font-mono text-[10px]">
|
||||
{pods[0].ip}
|
||||
</CompactRow>
|
||||
)}
|
||||
<CompactRow label="Routing To" valueClassName="font-semibold text-blue-600">
|
||||
{connectedServers.length} servers
|
||||
</CompactRow>
|
||||
{k8sNodes.length > 0 && (
|
||||
<CompactRow label="K8s Node" icon={<Box className="h-3 w-3" />}>
|
||||
{k8sNodes[0]}
|
||||
</CompactRow>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</TopologyNodeCard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 <Badge className="bg-green-500 hover:bg-green-600 text-xs">Healthy</Badge>;
|
||||
case "degraded":
|
||||
return <Badge className="bg-yellow-500 hover:bg-yellow-600 text-xs">Degraded</Badge>;
|
||||
case "unhealthy":
|
||||
return <Badge className="bg-red-500 hover:bg-red-600 text-xs">Unhealthy</Badge>;
|
||||
default:
|
||||
return (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
Unknown
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
};
|
||||
const { k8sNodes, connectedProxies, connectionInfo, metrics, pods } = metadata;
|
||||
const restartCount = pods.reduce((sum, pod) => sum + (pod.restarts || 0), 0);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-lg border bg-card p-3 shadow-md hover:shadow-lg transition-all w-[300px]",
|
||||
selected && "ring-2 ring-primary ring-offset-2 shadow-xl"
|
||||
)}
|
||||
<TopologyNodeCard
|
||||
selected={selected}
|
||||
icon={<Server className="h-4 w-4 text-primary" />}
|
||||
iconClassName="bg-primary/10"
|
||||
title={server.id}
|
||||
description={server.description}
|
||||
health={health}
|
||||
>
|
||||
<Handle type="target" position={Position.Top} className="w-3 h-3 !bg-gray-400" />
|
||||
|
||||
<div className="space-y-2.5">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
<div className="rounded-md bg-primary/10 p-1.5">
|
||||
<Server className="h-4 w-4 text-primary" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-bold text-sm truncate">{server.id}</p>
|
||||
{server.description && (
|
||||
<p className="text-xs text-muted-foreground truncate">{server.description}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{getStatusBadge()}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<Badge variant="outline" className="text-[10px] h-5">
|
||||
{server.jar_type}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="text-[10px] h-5">
|
||||
MC {server.minecraft_version}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="text-[10px] h-5">
|
||||
{server.type}
|
||||
</Badge>
|
||||
{connectionInfo && (
|
||||
<Badge variant="outline" className="text-[10px] h-5">
|
||||
{server.jar_type}
|
||||
{connectionInfo.type}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="text-[10px] h-5">
|
||||
MC {server.minecraft_version}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="text-[10px] h-5">
|
||||
{server.type}
|
||||
</Badge>
|
||||
{connectionInfo && (
|
||||
<Badge variant="outline" className="text-[10px] h-5">
|
||||
{connectionInfo.type}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between text-xs bg-muted/50 rounded px-2 py-1.5">
|
||||
<span className="text-muted-foreground">Pods</span>
|
||||
<span
|
||||
className={cn(
|
||||
"font-semibold",
|
||||
readyPods === podCount ? "text-green-600" : "text-yellow-600"
|
||||
)}
|
||||
>
|
||||
{readyPods}/{podCount}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1 text-[11px]">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground flex items-center gap-1">
|
||||
<HardDrive className="h-3 w-3" /> Memory
|
||||
</span>
|
||||
<span className="font-medium text-xs">
|
||||
{metrics?.memoryUsage && (
|
||||
<span className="text-blue-600">{metrics.memoryUsage} / </span>
|
||||
)}
|
||||
<span className="text-muted-foreground">
|
||||
{server.memory_request}/{server.memory}MB
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground flex items-center gap-1">
|
||||
<Cpu className="h-3 w-3" /> CPU
|
||||
</span>
|
||||
<span className="font-medium text-xs">
|
||||
{metrics?.cpuUsage && <span className="text-blue-600">{metrics.cpuUsage} / </span>}
|
||||
<span className="text-muted-foreground">
|
||||
{server.cpu_request || "N/A"}/{server.cpu_limit || "N/A"}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{connectionInfo?.connectionString && (
|
||||
<div className="space-y-1 text-[11px] pt-1 border-t">
|
||||
<div className="flex items-center gap-1">
|
||||
<code className="text-[10px] bg-muted px-1.5 py-0.5 rounded font-mono flex-1 truncate">
|
||||
{connectionInfo.connectionString}
|
||||
</code>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-5 w-5 shrink-0"
|
||||
onClick={handleCopy}
|
||||
title={copied ? "Copied!" : "Copy address"}
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="h-3 w-3 text-green-500" />
|
||||
) : (
|
||||
<Copy className="h-3 w-3" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
{connectionInfo.note && (
|
||||
<p className="text-[10px] text-muted-foreground italic">{connectionInfo.note}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{"pods" in metadata && metadata.pods && metadata.pods.length > 0 && (
|
||||
<div className="space-y-1 text-[11px] pt-1 border-t">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">Restarts</span>
|
||||
<span
|
||||
className={cn(
|
||||
"font-medium",
|
||||
metadata.pods.reduce((sum, p) => sum + (p.restarts || 0), 0) > 0
|
||||
? "text-yellow-600"
|
||||
: "text-green-600"
|
||||
)}
|
||||
>
|
||||
{metadata.pods.reduce((sum, p) => sum + (p.restarts || 0), 0)}
|
||||
</span>
|
||||
</div>
|
||||
{metadata.pods[0]?.age && (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">Age</span>
|
||||
<span className="font-medium">{metadata.pods[0].age}</span>
|
||||
</div>
|
||||
)}
|
||||
{metadata.pods[0]?.ip && (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">Pod IP</span>
|
||||
<span className="font-medium font-mono text-[10px]">{metadata.pods[0].ip}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(k8sNodes.length > 0 || connectedProxies.length > 0) && (
|
||||
<div className="space-y-1 text-[11px] pt-1 border-t">
|
||||
{connectedProxies.length > 0 && (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">Exposed By</span>
|
||||
<span className="font-semibold text-blue-600">
|
||||
{connectedProxies.length} proxies
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{k8sNodes.length > 0 && (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground flex items-center gap-1">
|
||||
<Box className="h-3 w-3" /> K8s Node
|
||||
</span>
|
||||
<span className="font-medium">{k8sNodes[0]}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Handle type="source" position={Position.Bottom} className="w-3 h-3 !bg-gray-400" />
|
||||
</div>
|
||||
<CompactRow
|
||||
label="Pods"
|
||||
className="text-xs bg-muted/50 rounded px-2 py-1.5"
|
||||
valueClassName={cn(
|
||||
"font-semibold",
|
||||
readyPods === podCount ? "text-green-600" : "text-yellow-600"
|
||||
)}
|
||||
>
|
||||
{readyPods}/{podCount}
|
||||
</CompactRow>
|
||||
|
||||
<div className="space-y-1 text-[11px]">
|
||||
<MetricRow
|
||||
label="Memory"
|
||||
icon={<HardDrive className="h-3 w-3" />}
|
||||
usage={metrics?.memoryUsage}
|
||||
limit={`${server.memory_request}/${server.memory}MB`}
|
||||
/>
|
||||
<MetricRow
|
||||
label="CPU"
|
||||
icon={<Cpu className="h-3 w-3" />}
|
||||
usage={metrics?.cpuUsage}
|
||||
limit={`${server.cpu_request || "N/A"}/${server.cpu_limit || "N/A"}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{connectionInfo?.connectionString && (
|
||||
<div className="space-y-1 text-[11px] pt-1 border-t">
|
||||
<CopyableCode value={connectionInfo.connectionString} />
|
||||
{connectionInfo.note && (
|
||||
<p className="text-[10px] text-muted-foreground italic">{connectionInfo.note}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{pods.length > 0 && (
|
||||
<div className="space-y-1 text-[11px] pt-1 border-t">
|
||||
<CompactRow
|
||||
label="Restarts"
|
||||
valueClassName={restartCount > 0 ? "text-yellow-600" : "text-green-600"}
|
||||
>
|
||||
{restartCount}
|
||||
</CompactRow>
|
||||
{pods[0]?.age && <CompactRow label="Age">{pods[0].age}</CompactRow>}
|
||||
{pods[0]?.ip && (
|
||||
<CompactRow label="Pod IP" valueClassName="font-mono text-[10px]">
|
||||
{pods[0].ip}
|
||||
</CompactRow>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(k8sNodes.length > 0 || connectedProxies.length > 0) && (
|
||||
<div className="space-y-1 text-[11px] pt-1 border-t">
|
||||
{connectedProxies.length > 0 && (
|
||||
<CompactRow label="Exposed By" valueClassName="font-semibold text-blue-600">
|
||||
{connectedProxies.length} proxies
|
||||
</CompactRow>
|
||||
)}
|
||||
{k8sNodes.length > 0 && (
|
||||
<CompactRow label="K8s Node" icon={<Box className="h-3 w-3" />}>
|
||||
{k8sNodes[0]}
|
||||
</CompactRow>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</TopologyNodeCard>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user