Files
floci-ui/packages/frontend/src/features/eks/EKSPage.tsx
T
2026-09-22 16:13:21 -04:00

311 lines
9.5 KiB
TypeScript

import { useEffect, useMemo, useState } from "react";
import {
Boxes,
Info,
RefreshCw,
Server,
ShieldCheck,
Tags,
} from "lucide-react";
import { EmptyState } from "@/components/EmptyState";
import type { EksCluster, EksNodegroup } from "@/api/aws/eks.api";
import {
useEksClustersQuery,
useEksNodegroupsQuery,
} from "@/api/aws/eks.queries";
function statusClass(status?: string) {
const normalized = status?.toLowerCase();
if (normalized === "active") return "healthy";
if (normalized === "creating" || normalized === "updating") return "degraded";
if (normalized === "failed" || normalized === "deleting") return "unavailable";
return "unknown";
}
function formatDate(value?: string) {
if (!value) return "—";
return new Intl.DateTimeFormat(undefined, {
dateStyle: "medium",
timeStyle: "short",
}).format(new Date(value));
}
function yesNo(value?: boolean) {
if (value === undefined) return "—";
return value ? "Enabled" : "Disabled";
}
function ClusterListItem({
cluster,
active,
onSelect,
}: {
cluster: EksCluster;
active: boolean;
onSelect: () => void;
}) {
return (
<button
className={`list-item ${active ? "active" : ""}`}
onClick={onSelect}
type="button"
>
<strong>{cluster.name}</strong>
<span>
Kubernetes {cluster.version ?? "—"} · {cluster.nodegroupCount ?? 0} nodegroups
</span>
</button>
);
}
function ClusterSummary({ cluster }: { cluster: EksCluster }) {
const vpc = cluster.resourcesVpcConfig;
return (
<div className="grid two">
<div className="widget">
<div className="widget-header">
<Boxes size={13} color="var(--accent)" />
<h3>Cluster</h3>
</div>
<div className="widget-body">
<div className="meta-grid">
<Meta label="Status" value={cluster.status ?? "unknown"} />
<Meta label="Kubernetes version" value={cluster.version ?? "—"} />
<Meta label="Platform version" value={cluster.platformVersion ?? "—"} />
<Meta label="Created" value={formatDate(cluster.createdAt)} />
<Meta label="Endpoint" value={cluster.endpoint ?? "—"} />
<Meta label="Role ARN" value={cluster.roleArn ?? "—"} />
</div>
</div>
</div>
<div className="widget">
<div className="widget-header">
<ShieldCheck size={13} color="var(--accent)" />
<h3>Networking</h3>
</div>
<div className="widget-body">
<div className="meta-grid">
<Meta label="VPC" value={vpc?.vpcId ?? "—"} />
<Meta label="Public endpoint" value={yesNo(vpc?.endpointPublicAccess)} />
<Meta label="Private endpoint" value={yesNo(vpc?.endpointPrivateAccess)} />
<Meta label="Subnets" value={String(vpc?.subnetIds.length ?? 0)} />
<Meta
label="Security groups"
value={String(vpc?.securityGroupIds.length ?? 0)}
/>
<Meta
label="Public CIDRs"
value={vpc?.publicAccessCidrs.join(", ") || "—"}
/>
</div>
</div>
</div>
</div>
);
}
function ClusterTags({ tags }: { tags: Record<string, string> }) {
const entries = Object.entries(tags);
return (
<div className="widget section-space">
<div className="widget-header">
<Tags size={13} color="var(--accent)" />
<h3>Tags</h3>
<span className="badge neutral">{entries.length}</span>
</div>
<div className="widget-body">
{entries.length === 0 ? (
<p className="muted compact-text">No tags returned for this cluster.</p>
) : (
<div className="metadata-tags">
{entries.map(([key, value]) => (
<span className="metadata-tag" key={`${key}:${value}`}>
<strong>{key}</strong>
<span>{value}</span>
</span>
))}
</div>
)}
</div>
</div>
);
}
function Meta({ label, value }: { label: string; value: string }) {
return (
<div className="meta-row">
<span className="meta-label">{label}</span>
<span className="meta-value">{value}</span>
</div>
);
}
function NodegroupTable({ nodegroups }: { nodegroups: EksNodegroup[] }) {
if (nodegroups.length === 0) {
return (
<EmptyState
icon={Server}
title="No nodegroups"
description="This cluster did not return any managed nodegroups."
/>
);
}
return (
<table className="table">
<thead>
<tr>
<th>Name</th>
<th>Status</th>
<th>Capacity</th>
<th>Instances</th>
<th>Scaling</th>
<th>Updated</th>
</tr>
</thead>
<tbody>
{nodegroups.map((nodegroup) => (
<tr key={nodegroup.arn ?? nodegroup.name}>
<td className="mono">{nodegroup.name}</td>
<td>
<span className={`status ${statusClass(nodegroup.status)}`}>
{nodegroup.status ?? "unknown"}
</span>
</td>
<td>{nodegroup.capacityType ?? "—"}</td>
<td className="mono">{nodegroup.instanceTypes.join(", ") || "—"}</td>
<td className="mono">
{nodegroup.scalingConfig
? `${nodegroup.scalingConfig.minSize ?? "?"}/${nodegroup.scalingConfig.desiredSize ?? "?"}/${nodegroup.scalingConfig.maxSize ?? "?"}`
: "—"}
</td>
<td>{formatDate(nodegroup.modifiedAt ?? nodegroup.createdAt)}</td>
</tr>
))}
</tbody>
</table>
);
}
export function EKSPage() {
const clustersQuery = useEksClustersQuery();
const clusters = useMemo(() => clustersQuery.data ?? [], [clustersQuery.data]);
const [selectedClusterName, setSelectedClusterName] = useState<string | null>(null);
useEffect(() => {
if (!selectedClusterName && clusters[0]) {
setSelectedClusterName(clusters[0].name);
}
}, [clusters, selectedClusterName]);
const selectedCluster = useMemo(
() => clusters.find((cluster) => cluster.name === selectedClusterName) ?? null,
[clusters, selectedClusterName],
);
const nodegroupsQuery = useEksNodegroupsQuery(selectedClusterName);
return (
<>
<div className="page-header">
<div className="page-title">
<h1>EKS</h1>
<span className="info-link">
<Info size={11} />
Kubernetes clusters
</span>
</div>
<button
className="button"
onClick={() => {
void clustersQuery.refetch();
void nodegroupsQuery.refetch();
}}
type="button"
>
<RefreshCw size={13} />
Refresh
</button>
</div>
<div className="split">
<aside className="list-pane">
<div className="widget-header">
<Boxes size={13} color="var(--text-2)" />
<h3>Clusters ({clusters.length})</h3>
</div>
{clustersQuery.isLoading ? (
<div className="empty compact"><p>Loading clusters...</p></div>
) : clustersQuery.isError ? (
<EmptyState
icon={Boxes}
title="Cannot load clusters"
description="EKS did not respond from the Floci endpoint."
/>
) : clusters.length === 0 ? (
<EmptyState
icon={Boxes}
title="No EKS clusters"
description="No Kubernetes clusters were returned by Floci."
/>
) : (
clusters.map((cluster) => (
<ClusterListItem
key={cluster.arn ?? cluster.name}
cluster={cluster}
active={selectedClusterName === cluster.name}
onSelect={() => setSelectedClusterName(cluster.name)}
/>
))
)}
</aside>
<section className="detail-pane">
{!selectedCluster ? (
<EmptyState
icon={Boxes}
title="Select a cluster"
description="Choose an EKS cluster to inspect its networking and nodegroups."
/>
) : (
<div className="content">
<div className="page-title" style={{ marginBottom: 16 }}>
<Boxes size={18} color="var(--accent)" />
<h2>{selectedCluster.name}</h2>
<span className={`status ${statusClass(selectedCluster.status)}`}>
{selectedCluster.status ?? "unknown"}
</span>
</div>
<ClusterSummary cluster={selectedCluster} />
<ClusterTags tags={selectedCluster.tags} />
<div className="table-panel section-space">
<div className="widget-header">
<Server size={13} color="var(--text-2)" />
<h3>Managed nodegroups</h3>
</div>
{nodegroupsQuery.isLoading ? (
<div className="empty compact"><p>Loading nodegroups...</p></div>
) : nodegroupsQuery.isError ? (
<EmptyState
icon={Server}
title="Cannot load nodegroups"
description="EKS did not return nodegroup details for this cluster."
/>
) : (
<NodegroupTable nodegroups={nodegroupsQuery.data ?? []} />
)}
</div>
</div>
)}
</section>
</div>
</>
);
}