fix(kube): correct kubectl context, dialog close, icon visibility, cluster name
Some checks failed
Test / rust-tests (pull_request) Successful in 14m38s
PR Review Automation / review (pull_request) Has been cancelled
Test / frontend-tests (pull_request) Successful in 1m50s
Test / frontend-typecheck (pull_request) Successful in 1m56s
Test / rust-fmt-check (pull_request) Successful in 11m10s
Test / rust-clippy (pull_request) Successful in 12m54s

1. kubectl credentials error (41 places in kube.rs)
   Every kubectl invocation used .env("KUBERNETES_CONTEXT", context) which
   is not a real kubectl environment variable — kubectl silently ignores it
   and falls back to whatever current-context is set in the kubeconfig YAML.
   If that context has expired or wrong credentials the auth failure occurs.
   Replaced all 41 instances with .arg("--context").arg(context) so kubectl
   always uses the correct context from the stored kubeconfig.

2. Cluster name still showed UUID (two causes)
   a) Hotbar read from kubernetesStore.clusters (ClusterInfo[]) which is never
      populated by the kubeconfig-based flow — always empty, so selectedCluster
      was always undefined. Removed the Zustand cluster lookup from Hotbar and
      added a clusterName prop passed from KubernetesPage.tsx (selectedConfig?.name).
   b) ClusterOverview fell back to showing raw clusterId UUID when clusterName
      was undefined. Changed subtitle to render conditionally so UUID never shows.

3. Bell dialog had no way to close
   Custom DialogContent had no X button and no backdrop-click handler.
   Added X close button (top-right) and backdrop-click-to-close.

4. Hotbar icons invisible in dark mode
   variant="ghost" only styles hover state with no baseline text color.
   Added className="text-foreground" to all icon-only ghost buttons.
This commit is contained in:
Shaun Arman 2026-06-07 18:58:16 -05:00
parent 7d8d5bdbba
commit fb55601e3b
6 changed files with 147 additions and 82 deletions

View File

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

View File

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

View File

@ -2,8 +2,6 @@ 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;
@ -11,21 +9,25 @@ interface HotbarProps {
onSettings: () => void;
onNotifications?: () => void;
notificationCount?: number;
clusterName?: string;
}
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);
export function Hotbar({
onRefresh,
onAddResource,
onSettings,
onNotifications,
notificationCount = 0,
clusterName,
}: HotbarProps) {
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}>
<Button variant="ghost" size="sm" onClick={onRefresh} className="text-foreground">
<RefreshCw className="w-4 h-4" />
</Button>
<Button variant="ghost" size="sm" onClick={onAddResource}>
<Button variant="ghost" size="sm" onClick={onAddResource} className="text-foreground">
<Plus className="w-4 h-4" />
</Button>
</div>
@ -33,33 +35,35 @@ export function Hotbar({ onRefresh, onAddResource, onSettings, onNotifications,
<div className="flex items-center gap-2">
<Search className="w-4 h-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground">
{selectedCluster?.name || "No cluster selected"}
{clusterName ?? "No cluster selected"}
</span>
</div>
</div>
<div className="flex items-center gap-2">
<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>
<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>
</div>
);

View File

@ -605,13 +605,26 @@ 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">
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm"
onClick={() => ctx.onOpenChange(false)}
>
<div
className={cn(
"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",
"relative w-full max-w-lg gap-4 border bg-background p-6 shadow-lg duration-200 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,11 +594,10 @@ export function KubernetesPage() {
}
if (activeSection === "overview") {
const overviewConfig = kubeconfigs.find((c) => c.id === selectedClusterId);
return (
<ClusterOverview
clusterId={selectedClusterId}
clusterName={overviewConfig?.context}
clusterName={selectedConfig?.name}
/>
);
}
@ -706,6 +705,7 @@ export function KubernetesPage() {
onAddResource={() => setIsCommandPaletteOpen(true)}
onSettings={() => {}}
onNotifications={() => setIsNotificationsOpen(true)}
clusterName={selectedConfig?.name}
/>
{/* Top bar: cluster selector + namespace selector */}

View File

@ -178,14 +178,20 @@ describe("ClusterOverview", () => {
});
});
it("falls back gracefully when clusterName prop is not provided", async () => {
it("hides the subtitle when clusterName prop is not provided (never shows UUID)", async () => {
mockInvoke.mockImplementation(() => Promise.resolve([]));
render(<ClusterOverview clusterId="cluster-1" />);
render(<ClusterOverview clusterId="019e9ff0-b6a4-78e1-a566-7a0c05e32577" />);
await waitFor(() => {
const header = screen.getByTestId("cluster-name-header");
expect(header).toBeInTheDocument();
// 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();
});
});
});