Files
floci-ui/packages/frontend/src/features/eks/EKSPage.tsx
T
Zakaria 91ce11274d
CI / checks (pull_request) Successful in 25m47s
chore: show TheCloud instead of Floci in console text and messages
Labels, empty states, error messages and defaults the console shows now name
TheCloud or its runtimes. Identifiers that must match the emulator (FLOCI_*
settings, the X-Floci-CSRF header, emulator-returned host names) are unchanged.
2026-10-24 14:30:44 -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 TheCloud's runtime."
/>
) : clusters.length === 0 ? (
<EmptyState
icon={Boxes}
title="No EKS clusters"
description="No Kubernetes clusters were returned by TheCloud's runtime."
/>
) : (
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>
</>
);
}