Compare commits

..

No commits in common. "a9f213abe5c5695f664d986bca939fdc73b9f466" and "5cf1806d64d646f83e11210cea61ca4a78444088" have entirely different histories.

6 changed files with 82 additions and 147 deletions

View File

@ -270,8 +270,7 @@ pub async fn test_cluster_connection(
let output = Command::new(kubectl_path)
.arg("cluster-info")
.env("KUBECONFIG", temp_path.to_string_lossy().to_string())
.arg("--context")
.arg(context.as_str())
.env("KUBERNETES_CONTEXT", context)
.output()
.await
.map_err(|e| format!("Failed to execute kubectl: {e}"))?;
@ -324,8 +323,7 @@ pub async fn discover_pods(
.arg("-o")
.arg("json")
.env("KUBECONFIG", temp_path.to_string_lossy().to_string())
.arg("--context")
.arg(context.as_str())
.env("KUBERNETES_CONTEXT", context)
.output()
.await
.map_err(|e| format!("Failed to execute kubectl: {e}"))?;
@ -460,9 +458,8 @@ pub async fn start_port_forward(
// Spawn kubectl subprocess
let child = Command::new(kubectl_path)
.args(&args)
.arg("--context")
.arg(cluster.context.as_str())
.env("KUBECONFIG", temp_path.to_string_lossy().to_string())
.env("KUBERNETES_CONTEXT", &cluster.context)
.spawn()
.map_err(|e| format!("Failed to spawn kubectl: {e}"))?;
@ -771,8 +768,7 @@ pub async fn list_namespaces(
.arg("-o")
.arg("json")
.env("KUBECONFIG", temp_path.to_string_lossy().to_string())
.arg("--context")
.arg(context.as_str())
.env("KUBERNETES_CONTEXT", context)
.output()
.await
.map_err(|e| format!("Failed to execute kubectl: {e}"))?;
@ -858,8 +854,7 @@ pub async fn list_pods(
.arg("-o")
.arg("json")
.env("KUBECONFIG", temp_path.to_string_lossy().to_string())
.arg("--context")
.arg(context.as_str())
.env("KUBERNETES_CONTEXT", context)
.output()
.await
.map_err(|e| format!("Failed to execute kubectl: {e}"))?;
@ -981,8 +976,7 @@ pub async fn list_services(
.arg("-o")
.arg("json")
.env("KUBECONFIG", temp_path.to_string_lossy().to_string())
.arg("--context")
.arg(context.as_str())
.env("KUBERNETES_CONTEXT", context)
.output()
.await
.map_err(|e| format!("Failed to execute kubectl: {e}"))?;
@ -1139,8 +1133,7 @@ pub async fn list_deployments(
.arg("-o")
.arg("json")
.env("KUBECONFIG", temp_path.to_string_lossy().to_string())
.arg("--context")
.arg(context.as_str())
.env("KUBERNETES_CONTEXT", context)
.output()
.await
.map_err(|e| format!("Failed to execute kubectl: {e}"))?;
@ -1273,8 +1266,7 @@ pub async fn list_statefulsets(
.arg("-o")
.arg("json")
.env("KUBECONFIG", temp_path.to_string_lossy().to_string())
.arg("--context")
.arg(context.as_str())
.env("KUBERNETES_CONTEXT", context)
.output()
.await
.map_err(|e| format!("Failed to execute kubectl: {e}"))?;
@ -1391,8 +1383,7 @@ pub async fn list_daemonsets(
.arg("-o")
.arg("json")
.env("KUBECONFIG", temp_path.to_string_lossy().to_string())
.arg("--context")
.arg(context.as_str())
.env("KUBERNETES_CONTEXT", context)
.output()
.await
.map_err(|e| format!("Failed to execute kubectl: {e}"))?;
@ -1556,8 +1547,7 @@ pub async fn get_pod_logs(
.arg("-c")
.arg(container_name)
.env("KUBECONFIG", temp_path.to_string_lossy().to_string())
.arg("--context")
.arg(context.as_str())
.env("KUBERNETES_CONTEXT", context)
.output()
.await
.map_err(|e| format!("Failed to execute kubectl: {e}"))?;
@ -1606,8 +1596,7 @@ pub async fn scale_deployment(
.arg("-n")
.arg(namespace)
.env("KUBECONFIG", temp_path.to_string_lossy().to_string())
.arg("--context")
.arg(context.as_str())
.env("KUBERNETES_CONTEXT", context)
.output()
.await
.map_err(|e| format!("Failed to execute kubectl: {e}"))?;
@ -1652,8 +1641,7 @@ pub async fn restart_deployment(
.arg("-n")
.arg(namespace)
.env("KUBECONFIG", temp_path.to_string_lossy().to_string())
.arg("--context")
.arg(context.as_str())
.env("KUBERNETES_CONTEXT", context)
.output()
.await
.map_err(|e| format!("Failed to execute kubectl: {e}"))?;
@ -1698,8 +1686,7 @@ pub async fn delete_resource(
.arg("-n")
.arg(namespace)
.env("KUBECONFIG", temp_path.to_string_lossy().to_string())
.arg("--context")
.arg(context.as_str())
.env("KUBERNETES_CONTEXT", context)
.output()
.await
.map_err(|e| format!("Failed to execute kubectl: {e}"))?;
@ -1767,8 +1754,7 @@ pub async fn exec_pod(
cmd.arg("--").arg(shell_cmd).arg("-c").arg(&command);
cmd.env("KUBECONFIG", temp_path.to_string_lossy().to_string())
.arg("--context")
.arg(context.as_str());
.env("KUBERNETES_CONTEXT", context);
let output = cmd
.output()
@ -2028,8 +2014,7 @@ pub async fn list_replicasets(
.arg("-o")
.arg("json")
.env("KUBECONFIG", temp_path.to_string_lossy().to_string())
.arg("--context")
.arg(context.as_str())
.env("KUBERNETES_CONTEXT", context)
.output()
.await
.map_err(|e| format!("Failed to execute kubectl: {e}"))?;
@ -2146,8 +2131,7 @@ pub async fn list_jobs(
.arg("-o")
.arg("json")
.env("KUBECONFIG", temp_path.to_string_lossy().to_string())
.arg("--context")
.arg(context.as_str())
.env("KUBERNETES_CONTEXT", context)
.output()
.await
.map_err(|e| format!("Failed to execute kubectl: {e}"))?;
@ -2302,8 +2286,7 @@ pub async fn list_cronjobs(
.arg("-o")
.arg("json")
.env("KUBECONFIG", temp_path.to_string_lossy().to_string())
.arg("--context")
.arg(context.as_str())
.env("KUBERNETES_CONTEXT", context)
.output()
.await
.map_err(|e| format!("Failed to execute kubectl: {e}"))?;
@ -2429,8 +2412,7 @@ pub async fn list_configmaps(
.arg("-o")
.arg("json")
.env("KUBECONFIG", temp_path.to_string_lossy().to_string())
.arg("--context")
.arg(context.as_str())
.env("KUBERNETES_CONTEXT", context)
.output()
.await
.map_err(|e| format!("Failed to execute kubectl: {e}"))?;
@ -2527,8 +2509,7 @@ pub async fn list_secrets(
.arg("-o")
.arg("json")
.env("KUBECONFIG", temp_path.to_string_lossy().to_string())
.arg("--context")
.arg(context.as_str())
.env("KUBERNETES_CONTEXT", context)
.output()
.await
.map_err(|e| format!("Failed to execute kubectl: {e}"))?;
@ -2626,8 +2607,7 @@ pub async fn list_nodes(
.arg("-o")
.arg("json")
.env("KUBECONFIG", temp_path.to_string_lossy().to_string())
.arg("--context")
.arg(context.as_str())
.env("KUBERNETES_CONTEXT", context)
.output()
.await
.map_err(|e| format!("Failed to execute kubectl: {e}"))?;
@ -2819,8 +2799,7 @@ pub async fn list_events(
.arg("-o")
.arg("json")
.env("KUBECONFIG", temp_path.to_string_lossy().to_string())
.arg("--context")
.arg(context.as_str())
.env("KUBERNETES_CONTEXT", context)
.output()
.await
.map_err(|e| format!("Failed to execute kubectl: {e}"))?;
@ -2948,8 +2927,7 @@ pub async fn list_ingresses(
.arg("-o")
.arg("json")
.env("KUBECONFIG", temp_path.to_string_lossy().to_string())
.arg("--context")
.arg(context.as_str())
.env("KUBERNETES_CONTEXT", context)
.output()
.await
.map_err(|e| format!("Failed to execute kubectl: {e}"))?;
@ -3074,8 +3052,7 @@ pub async fn list_persistentvolumeclaims(
.arg("-o")
.arg("json")
.env("KUBECONFIG", temp_path.to_string_lossy().to_string())
.arg("--context")
.arg(context.as_str())
.env("KUBERNETES_CONTEXT", context)
.output()
.await
.map_err(|e| format!("Failed to execute kubectl: {e}"))?;
@ -3199,8 +3176,7 @@ pub async fn list_persistentvolumes(
.arg("-o")
.arg("json")
.env("KUBECONFIG", temp_path.to_string_lossy().to_string())
.arg("--context")
.arg(context.as_str())
.env("KUBERNETES_CONTEXT", context)
.output()
.await
.map_err(|e| format!("Failed to execute kubectl: {e}"))?;
@ -3330,8 +3306,7 @@ pub async fn list_serviceaccounts(
.arg("-o")
.arg("json")
.env("KUBECONFIG", temp_path.to_string_lossy().to_string())
.arg("--context")
.arg(context.as_str())
.env("KUBERNETES_CONTEXT", context)
.output()
.await
.map_err(|e| format!("Failed to execute kubectl: {e}"))?;
@ -3428,8 +3403,7 @@ pub async fn list_roles(
.arg("-o")
.arg("json")
.env("KUBECONFIG", temp_path.to_string_lossy().to_string())
.arg("--context")
.arg(context.as_str())
.env("KUBERNETES_CONTEXT", context)
.output()
.await
.map_err(|e| format!("Failed to execute kubectl: {e}"))?;
@ -3513,8 +3487,7 @@ pub async fn list_clusterroles(
.arg("-o")
.arg("json")
.env("KUBECONFIG", temp_path.to_string_lossy().to_string())
.arg("--context")
.arg(context.as_str())
.env("KUBERNETES_CONTEXT", context)
.output()
.await
.map_err(|e| format!("Failed to execute kubectl: {e}"))?;
@ -3593,8 +3566,7 @@ pub async fn list_rolebindings(
.arg("-o")
.arg("json")
.env("KUBECONFIG", temp_path.to_string_lossy().to_string())
.arg("--context")
.arg(context.as_str())
.env("KUBERNETES_CONTEXT", context)
.output()
.await
.map_err(|e| format!("Failed to execute kubectl: {e}"))?;
@ -3689,8 +3661,7 @@ pub async fn list_clusterrolebindings(
.arg("-o")
.arg("json")
.env("KUBECONFIG", temp_path.to_string_lossy().to_string())
.arg("--context")
.arg(context.as_str())
.env("KUBERNETES_CONTEXT", context)
.output()
.await
.map_err(|e| format!("Failed to execute kubectl: {e}"))?;
@ -3780,8 +3751,7 @@ pub async fn list_horizontalpodautoscalers(
.arg("-o")
.arg("json")
.env("KUBECONFIG", temp_path.to_string_lossy().to_string())
.arg("--context")
.arg(context.as_str())
.env("KUBERNETES_CONTEXT", context)
.output()
.await
.map_err(|e| format!("Failed to execute kubectl: {e}"))?;
@ -3893,8 +3863,7 @@ pub async fn list_storageclasses(
.arg("-o")
.arg("json")
.env("KUBECONFIG", temp_path.to_string_lossy().to_string())
.arg("--context")
.arg(context.as_str())
.env("KUBERNETES_CONTEXT", context)
.output()
.await
.map_err(|e| format!("Failed to execute kubectl: {e}"))?;
@ -4003,8 +3972,7 @@ pub async fn list_networkpolicies(
.arg("-o")
.arg("json")
.env("KUBECONFIG", temp_path.to_string_lossy().to_string())
.arg("--context")
.arg(context.as_str())
.env("KUBERNETES_CONTEXT", context)
.output()
.await
.map_err(|e| format!("Failed to execute kubectl: {e}"))?;
@ -4114,8 +4082,7 @@ pub async fn list_resourcequotas(
.arg("-o")
.arg("json")
.env("KUBECONFIG", temp_path.to_string_lossy().to_string())
.arg("--context")
.arg(context.as_str())
.env("KUBERNETES_CONTEXT", context)
.output()
.await
.map_err(|e| format!("Failed to execute kubectl: {e}"))?;
@ -4235,8 +4202,7 @@ pub async fn list_limitranges(
.arg("-o")
.arg("json")
.env("KUBECONFIG", temp_path.to_string_lossy().to_string())
.arg("--context")
.arg(context.as_str())
.env("KUBERNETES_CONTEXT", context)
.output()
.await
.map_err(|e| format!("Failed to execute kubectl: {e}"))?;
@ -4327,8 +4293,7 @@ pub async fn cordon_node(
.arg("cordon")
.arg(node_name)
.env("KUBECONFIG", temp_path.to_string_lossy().to_string())
.arg("--context")
.arg(context.as_str())
.env("KUBERNETES_CONTEXT", context)
.output()
.await
.map_err(|e| format!("Failed to execute kubectl: {e}"))?;
@ -4368,8 +4333,7 @@ pub async fn uncordon_node(
.arg("uncordon")
.arg(node_name)
.env("KUBECONFIG", temp_path.to_string_lossy().to_string())
.arg("--context")
.arg(context.as_str())
.env("KUBERNETES_CONTEXT", context)
.output()
.await
.map_err(|e| format!("Failed to execute kubectl: {e}"))?;
@ -4412,8 +4376,7 @@ pub async fn drain_node(
.arg("--delete-emptydir-data")
.arg("--force")
.env("KUBECONFIG", temp_path.to_string_lossy().to_string())
.arg("--context")
.arg(context.as_str())
.env("KUBERNETES_CONTEXT", context)
.output()
.await
.map_err(|e| format!("Failed to execute kubectl: {e}"))?;
@ -4458,8 +4421,7 @@ pub async fn rollback_deployment(
.arg("-n")
.arg(namespace)
.env("KUBECONFIG", temp_path.to_string_lossy().to_string())
.arg("--context")
.arg(context.as_str())
.env("KUBERNETES_CONTEXT", context)
.output()
.await
.map_err(|e| format!("Failed to execute kubectl: {e}"))?;
@ -4504,8 +4466,7 @@ pub async fn create_resource(
.arg("-n")
.arg(namespace)
.env("KUBECONFIG", temp_path.to_string_lossy().to_string())
.arg("--context")
.arg(context.as_str())
.env("KUBERNETES_CONTEXT", context)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
@ -4567,8 +4528,7 @@ pub async fn edit_resource(
.arg("-n")
.arg(namespace)
.env("KUBECONFIG", temp_path.to_string_lossy().to_string())
.arg("--context")
.arg(context.as_str())
.env("KUBERNETES_CONTEXT", context)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());

View File

@ -117,11 +117,9 @@ export function ClusterOverview({ clusterId, clusterName }: ClusterOverviewProps
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-semibold">Cluster Overview</h2>
{clusterName && (
<p className="text-muted-foreground text-sm mt-0.5" data-testid="cluster-name-header">
{clusterName}
</p>
)}
<p className="text-muted-foreground text-sm mt-0.5" data-testid="cluster-name-header">
{clusterName ?? clusterId}
</p>
</div>
<button
onClick={() => void loadData()}

View File

@ -2,6 +2,8 @@ import React from "react";
import { Button } from "@/components/ui";
import { Settings, Bell, User, Search, Plus, RefreshCw } from "lucide-react";
import { Badge } from "@/components/ui";
import { useKubernetesStore } from "@/stores/kubernetesStore";
import { useStore } from "zustand";
interface HotbarProps {
onRefresh: () => void;
@ -9,25 +11,21 @@ interface HotbarProps {
onSettings: () => void;
onNotifications?: () => void;
notificationCount?: number;
clusterName?: string;
}
export function Hotbar({
onRefresh,
onAddResource,
onSettings,
onNotifications,
notificationCount = 0,
clusterName,
}: HotbarProps) {
export function Hotbar({ onRefresh, onAddResource, onSettings, onNotifications, notificationCount = 0 }: HotbarProps) {
const clusters = useStore(useKubernetesStore, (state) => state.clusters);
const selectedClusterId = useStore(useKubernetesStore, (state) => state.selectedClusterId);
const selectedCluster = clusters.find((c: { id: string }) => c.id === selectedClusterId);
return (
<div className="h-12 bg-background border-b flex items-center justify-between px-4">
<div className="flex items-center gap-2">
<div className="flex items-center gap-1">
<Button variant="ghost" size="sm" onClick={onRefresh} className="text-foreground">
<Button variant="ghost" size="sm" onClick={onRefresh}>
<RefreshCw className="w-4 h-4" />
</Button>
<Button variant="ghost" size="sm" onClick={onAddResource} className="text-foreground">
<Button variant="ghost" size="sm" onClick={onAddResource}>
<Plus className="w-4 h-4" />
</Button>
</div>
@ -35,35 +33,33 @@ export function Hotbar({
<div className="flex items-center gap-2">
<Search className="w-4 h-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground">
{clusterName ?? "No cluster selected"}
{selectedCluster?.name || "No cluster selected"}
</span>
</div>
</div>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={onNotifications}
aria-label="Notifications"
className="text-foreground"
>
<Bell className="w-4 h-4" />
{notificationCount > 0 && (
<Badge
variant="destructive"
className="h-4 w-4 flex items-center justify-center p-0 text-[10px]"
>
{notificationCount}
</Badge>
)}
</Button>
<Button variant="ghost" size="sm" onClick={onSettings} className="text-foreground">
<Settings className="w-4 h-4" />
</Button>
<Button variant="ghost" size="sm" className="text-foreground">
<User className="w-4 h-4" />
</Button>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={onNotifications}
aria-label="Notifications"
>
<Bell className="w-4 h-4" />
{notificationCount > 0 && (
<Badge variant="destructive" className="h-4 w-4 flex items-center justify-center p-0 text-[10px]">
{notificationCount}
</Badge>
)}
</Button>
<Button variant="ghost" size="sm" onClick={onSettings}>
<Settings className="w-4 h-4" />
</Button>
<Button variant="ghost" size="sm">
<User className="w-4 h-4" />
</Button>
</div>
</div>
</div>
);

View File

@ -605,26 +605,13 @@ export function DialogContent({ className, children }: { className?: string; chi
if (!ctx.open) return null;
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm"
onClick={() => ctx.onOpenChange(false)}
>
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
<div
className={cn(
"relative w-full max-w-lg gap-4 border bg-background p-6 shadow-lg duration-200 rounded-lg",
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 sm:rounded-lg",
className
)}
onClick={(e) => e.stopPropagation()}
>
<button
onClick={() => ctx.onOpenChange(false)}
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 text-foreground"
aria-label="Close"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M18 6 6 18"/><path d="m6 6 12 12"/>
</svg>
</button>
{children}
</div>
</div>

View File

@ -594,10 +594,11 @@ export function KubernetesPage() {
}
if (activeSection === "overview") {
const overviewConfig = kubeconfigs.find((c) => c.id === selectedClusterId);
return (
<ClusterOverview
clusterId={selectedClusterId}
clusterName={selectedConfig?.name}
clusterName={overviewConfig?.context}
/>
);
}
@ -705,7 +706,6 @@ export function KubernetesPage() {
onAddResource={() => setIsCommandPaletteOpen(true)}
onSettings={() => {}}
onNotifications={() => setIsNotificationsOpen(true)}
clusterName={selectedConfig?.name}
/>
{/* Top bar: cluster selector + namespace selector */}

View File

@ -178,20 +178,14 @@ describe("ClusterOverview", () => {
});
});
it("hides the subtitle when clusterName prop is not provided (never shows UUID)", async () => {
it("falls back gracefully when clusterName prop is not provided", async () => {
mockInvoke.mockImplementation(() => Promise.resolve([]));
render(<ClusterOverview clusterId="019e9ff0-b6a4-78e1-a566-7a0c05e32577" />);
render(<ClusterOverview clusterId="cluster-1" />);
await waitFor(() => {
// Heading still present
expect(screen.getByText("Cluster Overview")).toBeInTheDocument();
// UUID must NOT be rendered anywhere
expect(
screen.queryByText("019e9ff0-b6a4-78e1-a566-7a0c05e32577")
).not.toBeInTheDocument();
// Subtitle element should not exist when no name is passed
expect(screen.queryByTestId("cluster-name-header")).not.toBeInTheDocument();
const header = screen.getByTestId("cluster-name-header");
expect(header).toBeInTheDocument();
});
});
});