fix(kube): fix PTY param names, ansi-to-react ESM interop, and dark mode badges

- Correct start_pty_exec_session and start_pty_attach_session invoke calls
  to use pod/container keys matching Rust command parameter names; drop
  unused shell arg from the invoke payload
- Fix ansi-to-react CJS/ESM interop in LogStreamPanel: unwrap .default on
  CJS module so React does not receive a plain object at render time; add
  optimizeDeps entry to vite.config.ts so Vite pre-bundles it in dev
- Replace Badge + getPodStatusColor with StatusBadge in PodList; remove
  now-unused helper; extend getStatusVariant in Badge.tsx to handle
  crashloopbackoff, OOM, backoff, terminating, and evicted states
- Fix pre-existing lint issues: remove unused listPodsCmd/listNamespacesCmd
  imports from PortForwardPage, wrap loadPortForwards in useCallback, and
  remove unused logLine variable from LogStreamPanel test
This commit is contained in:
Shaun Arman 2026-06-09 20:38:24 -05:00
parent 3afa97b517
commit 399ba30c6b
10 changed files with 253 additions and 46 deletions

View File

@ -69,5 +69,11 @@ function getStatusVariant(status: string): BadgeProps["variant"] {
if (normalized === "succeeded" || normalized === "completed" || normalized === "bound") { if (normalized === "succeeded" || normalized === "completed" || normalized === "bound") {
return "succeeded"; return "succeeded";
} }
if (normalized.includes("crash") || normalized.includes("error") || normalized.includes("oom") || normalized.includes("backoff")) {
return "failed";
}
if (normalized === "terminating" || normalized === "evicted") {
return "destructive";
}
return "unknown"; return "unknown";
} }

View File

@ -3,8 +3,9 @@ import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui";
import { Badge } from "@/components/ui"; import { Badge } from "@/components/ui";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui";
import { Button } from "@/components/ui"; import { Button } from "@/components/ui";
import { X, Loader2 } from "lucide-react"; import { Network, X, Loader2 } from "lucide-react";
import { YamlEditor } from "./YamlEditor"; import { YamlEditor } from "./YamlEditor";
import { PortForwardDialog } from "./PortForwardDialog";
import { scaleDeploymentCmd, restartDeploymentCmd, rollbackDeploymentCmd } from "@/lib/tauriCommands"; import { scaleDeploymentCmd, restartDeploymentCmd, rollbackDeploymentCmd } from "@/lib/tauriCommands";
import type { DeploymentInfo } from "@/lib/tauriCommands"; import type { DeploymentInfo } from "@/lib/tauriCommands";
@ -18,6 +19,7 @@ interface DeploymentDetailProps {
export function DeploymentDetail({ clusterId, namespace, deployment, onClose }: DeploymentDetailProps) { export function DeploymentDetail({ clusterId, namespace, deployment, onClose }: DeploymentDetailProps) {
const [activeTab, setActiveTab] = React.useState("overview"); const [activeTab, setActiveTab] = React.useState("overview");
const [replicaCount, setReplicaCount] = React.useState(deployment.replicas); const [replicaCount, setReplicaCount] = React.useState(deployment.replicas);
const [portForwardOpen, setPortForwardOpen] = React.useState(false);
const [scaleLoading, setScaleLoading] = React.useState(false); const [scaleLoading, setScaleLoading] = React.useState(false);
const [scaleError, setScaleError] = React.useState<string | null>(null); const [scaleError, setScaleError] = React.useState<string | null>(null);
@ -74,10 +76,24 @@ export function DeploymentDetail({ clusterId, namespace, deployment, onClose }:
<h2 className="text-xl font-semibold">Deployment: {deployment.name}</h2> <h2 className="text-xl font-semibold">Deployment: {deployment.name}</h2>
<Badge variant="outline">{namespace}</Badge> <Badge variant="outline">{namespace}</Badge>
</div> </div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={() => setPortForwardOpen(true)}>
<Network className="w-4 h-4 mr-1.5" />
Port Forward
</Button>
<Button variant="ghost" size="sm" onClick={onClose}> <Button variant="ghost" size="sm" onClick={onClose}>
<X className="w-4 h-4" /> <X className="w-4 h-4" />
</Button> </Button>
</div> </div>
</div>
<PortForwardDialog
open={portForwardOpen}
onOpenChange={setPortForwardOpen}
clusterId={clusterId}
namespace={namespace}
podName={undefined}
/>
<Tabs value={activeTab} onValueChange={setActiveTab}> <Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid grid-cols-3 mb-4"> <TabsList className="grid grid-cols-3 mb-4">

View File

@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useRef, useState } from "react"; import React, { useCallback, useEffect, useRef, useState } from "react";
import { listen, type UnlistenFn } from "@tauri-apps/api/event"; import { listen, type UnlistenFn } from "@tauri-apps/api/event";
import { Download, Search, Square, Trash2, Play, ChevronUp, ChevronDown, DownloadCloud } from "lucide-react"; import { Download, Search, Square, Trash2, Play, ChevronUp, ChevronDown, DownloadCloud } from "lucide-react";
import Ansi from "ansi-to-react"; import AnsiLib from "ansi-to-react";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -12,6 +12,10 @@ import {
} from "@/components/ui"; } from "@/components/ui";
import { streamPodLogsCmd, stopLogStreamCmd } from "@/lib/tauriCommands"; import { streamPodLogsCmd, stopLogStreamCmd } from "@/lib/tauriCommands";
// Handle CJS default export in both dev and production Vite builds
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const Ansi = ((AnsiLib as any).default ?? AnsiLib) as React.ComponentType<{ children: string }>;
interface LogStreamPanelProps { interface LogStreamPanelProps {
clusterId: string; clusterId: string;
namespace: string; namespace: string;

View File

@ -4,8 +4,9 @@ import { Badge } from "@/components/ui";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui";
import { Button } from "@/components/ui"; import { Button } from "@/components/ui";
import { Copy, X } from "lucide-react"; import { Copy, Network, X } from "lucide-react";
import { Loader2 } from "lucide-react"; import { Loader2 } from "lucide-react";
import { PortForwardDialog } from "./PortForwardDialog";
import { YamlEditor } from "./YamlEditor"; import { YamlEditor } from "./YamlEditor";
import { getPodLogsCmd } from "@/lib/tauriCommands"; import { getPodLogsCmd } from "@/lib/tauriCommands";
import type { PodInfo } from "@/lib/tauriCommands"; import type { PodInfo } from "@/lib/tauriCommands";
@ -23,6 +24,7 @@ export function PodDetail({ clusterId, namespace, pod, onClose }: PodDetailProps
const [logs, setLogs] = React.useState<string | null>(null); const [logs, setLogs] = React.useState<string | null>(null);
const [logsLoading, setLogsLoading] = React.useState(false); const [logsLoading, setLogsLoading] = React.useState(false);
const [logsError, setLogsError] = React.useState<string | null>(null); const [logsError, setLogsError] = React.useState<string | null>(null);
const [portForwardOpen, setPortForwardOpen] = React.useState(false);
const fetchLogs = React.useCallback( const fetchLogs = React.useCallback(
async (containerName: string) => { async (containerName: string) => {
@ -66,10 +68,24 @@ export function PodDetail({ clusterId, namespace, pod, onClose }: PodDetailProps
<h2 className="text-xl font-semibold">Pod: {pod.name}</h2> <h2 className="text-xl font-semibold">Pod: {pod.name}</h2>
<Badge variant="outline">{namespace}</Badge> <Badge variant="outline">{namespace}</Badge>
</div> </div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={() => setPortForwardOpen(true)}>
<Network className="w-4 h-4 mr-1.5" />
Port Forward
</Button>
<Button variant="ghost" size="sm" onClick={onClose}> <Button variant="ghost" size="sm" onClick={onClose}>
<X className="w-4 h-4" /> <X className="w-4 h-4" />
</Button> </Button>
</div> </div>
</div>
<PortForwardDialog
open={portForwardOpen}
onOpenChange={setPortForwardOpen}
clusterId={clusterId}
namespace={namespace}
podName={pod.name}
/>
<Tabs value={activeTab} onValueChange={handleTabChange}> <Tabs value={activeTab} onValueChange={handleTabChange}>
<TabsList className="grid grid-cols-3 mb-4"> <TabsList className="grid grid-cols-3 mb-4">

View File

@ -1,6 +1,6 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Button } from "@/components/ui"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Button } from "@/components/ui";
import { Badge } from "@/components/ui"; import { StatusBadge } from "@/components/Badge";
import { FileText, Terminal, Link, Pencil, Trash2, Zap, Settings } from "lucide-react"; import { FileText, Terminal, Link, Pencil, Trash2, Zap, Settings } from "lucide-react";
import type { PodInfo } from "@/lib/tauriCommands"; import type { PodInfo } from "@/lib/tauriCommands";
import { deleteResourceCmd, forceDeleteResourceCmd, getResourceYamlCmd } from "@/lib/tauriCommands"; import { deleteResourceCmd, forceDeleteResourceCmd, getResourceYamlCmd } from "@/lib/tauriCommands";
@ -14,7 +14,6 @@ import { useColumnConfig } from "@/hooks/useColumnConfig";
import { useMetrics } from "@/hooks/useMetrics"; import { useMetrics } from "@/hooks/useMetrics";
import { DEFAULT_COLUMNS } from "@/config/defaultColumns"; import { DEFAULT_COLUMNS } from "@/config/defaultColumns";
import { ColumnConfigModal } from "@/components/tables/ColumnConfigModal"; import { ColumnConfigModal } from "@/components/tables/ColumnConfigModal";
import { QuickActionColumn } from "@/components/tables/QuickActionColumn";
interface PodListProps { interface PodListProps {
pods: PodInfo[]; pods: PodInfo[];
@ -49,23 +48,6 @@ export function PodList({ pods, clusterId, namespace, onRefresh }: PodListProps)
metricsEnabled ? namespace : null metricsEnabled ? namespace : null
); );
const getPodStatusColor = (status: string) => {
switch (status.toLowerCase()) {
case "running":
return "bg-green-500";
case "pending":
return "bg-yellow-500";
case "succeeded":
case "completed":
return "bg-blue-500";
case "failed":
case "error":
return "bg-red-500";
default:
return "bg-gray-500";
}
};
const openEdit = async (pod: PodInfo) => { const openEdit = async (pod: PodInfo) => {
setEditError(null); setEditError(null);
try { try {
@ -152,9 +134,7 @@ export function PodList({ pods, clusterId, namespace, onRefresh }: PodListProps)
)} )}
{isColumnVisible("status") && ( {isColumnVisible("status") && (
<TableCell> <TableCell>
<Badge className={`${getPodStatusColor(pod.status)} text-white`}> <StatusBadge status={pod.status} />
{pod.status}
</Badge>
</TableCell> </TableCell>
)} )}
{isColumnVisible("ready") && <TableCell>{pod.ready}</TableCell>} {isColumnVisible("ready") && <TableCell>{pod.ready}</TableCell>}

View File

@ -0,0 +1,181 @@
import React from "react";
import { Loader2 } from "lucide-react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
Button,
Input,
Label,
} from "@/components/ui";
import { startPortForwardCmd } from "@/lib/tauriCommands";
interface PortForwardDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
clusterId: string;
namespace: string;
podName?: string;
}
export function PortForwardDialog({
open,
onOpenChange,
clusterId,
namespace,
podName,
}: PortForwardDialogProps) {
const [pod, setPod] = React.useState(podName ?? "");
const [containerPort, setContainerPort] = React.useState("");
const [localPort, setLocalPort] = React.useState("");
const [loading, setLoading] = React.useState(false);
const [error, setError] = React.useState<string | null>(null);
const [success, setSuccess] = React.useState(false);
React.useEffect(() => {
if (open) {
setPod(podName ?? "");
setContainerPort("");
setLocalPort("");
setError(null);
setSuccess(false);
}
}, [open, podName]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
setSuccess(false);
const podValue = pod.trim();
if (!podValue) {
setError("Pod name is required.");
return;
}
const portNum = parseInt(containerPort, 10);
if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
setError("Container port must be a valid number between 1 and 65535.");
return;
}
let localPortNum: number | undefined;
if (localPort.trim() !== "") {
localPortNum = parseInt(localPort, 10);
if (isNaN(localPortNum) || localPortNum < 1 || localPortNum > 65535) {
setError("Local port must be a valid number between 1 and 65535.");
return;
}
}
setLoading(true);
try {
await startPortForwardCmd({
cluster_id: clusterId,
namespace,
pod: podValue,
container_port: portNum,
local_port: localPortNum,
});
setSuccess(true);
onOpenChange(false);
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
} finally {
setLoading(false);
}
};
const isPodReadonly = podName !== undefined;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>Start Port Forward</DialogTitle>
</DialogHeader>
<form onSubmit={(e) => void handleSubmit(e)} className="space-y-4 py-2">
<div className="space-y-1.5">
<Label htmlFor="pfd-namespace">Namespace</Label>
<Input
id="pfd-namespace"
value={namespace}
readOnly
disabled
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="pfd-pod">Pod Name</Label>
<Input
id="pfd-pod"
value={pod}
onChange={(e) => setPod(e.target.value)}
placeholder="e.g. nginx-abc123"
readOnly={isPodReadonly}
disabled={isPodReadonly || loading}
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="pfd-container-port">Container Port</Label>
<Input
id="pfd-container-port"
type="number"
min={1}
max={65535}
value={containerPort}
onChange={(e) => setContainerPort(e.target.value)}
placeholder="80"
disabled={loading}
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="pfd-local-port">Local Port (optional)</Label>
<Input
id="pfd-local-port"
type="number"
min={1}
max={65535}
value={localPort}
onChange={(e) => setLocalPort(e.target.value)}
placeholder="auto"
disabled={loading}
/>
</div>
{error && (
<div className="rounded-md bg-destructive/15 px-4 py-3 text-sm text-destructive">
{error}
</div>
)}
{success && (
<div className="rounded-md bg-green-500/15 px-4 py-3 text-sm text-green-600">
Port forward started successfully.
</div>
)}
<DialogFooter className="pt-2">
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
disabled={loading}
>
Cancel
</Button>
<Button type="submit" disabled={loading}>
{loading && <Loader2 className="w-4 h-4 mr-2 animate-spin" />}
Start
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}

View File

@ -1530,14 +1530,13 @@ export const startPtyExecSessionCmd = (
namespace: string, namespace: string,
podName: string, podName: string,
containerName: string | null, containerName: string | null,
shell: string _shell: string
) => ) =>
invoke<string>("start_pty_exec_session", { invoke<string>("start_pty_exec_session", {
clusterId, clusterId,
namespace, namespace,
podName, pod: podName,
containerName, container: containerName,
shell,
}); });
export const startPtyAttachSessionCmd = ( export const startPtyAttachSessionCmd = (
@ -1549,8 +1548,8 @@ export const startPtyAttachSessionCmd = (
invoke<string>("start_pty_attach_session", { invoke<string>("start_pty_attach_session", {
clusterId, clusterId,
namespace, namespace,
podName, pod: podName,
containerName, container: containerName,
}); });
export const sendPtyStdinCmd = (sessionId: string, data: string) => export const sendPtyStdinCmd = (sessionId: string, data: string) =>

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect, useCallback } from "react";
import { Play, Square, Trash2, Plus, RefreshCw } from "lucide-react"; import { Play, Square, Trash2, Plus, RefreshCw } from "lucide-react";
import { useKubernetesStore } from "@/stores/kubernetesStore"; import { useKubernetesStore } from "@/stores/kubernetesStore";
import { import {
@ -17,8 +17,6 @@ import {
startPortForwardCmd, startPortForwardCmd,
stopPortForwardCmd, stopPortForwardCmd,
deletePortForwardCmd, deletePortForwardCmd,
listPodsCmd,
listNamespacesCmd,
} from "@/lib/tauriCommands"; } from "@/lib/tauriCommands";
import { PortForwardForm } from "@/components/Kubernetes"; import { PortForwardForm } from "@/components/Kubernetes";
@ -29,7 +27,7 @@ export function PortForwardPage() {
const [isFormOpen, setIsFormOpen] = useState(false); const [isFormOpen, setIsFormOpen] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const loadPortForwards = async () => { const loadPortForwards = useCallback(async () => {
if (!selectedClusterId) return; if (!selectedClusterId) return;
setIsLoading(true); setIsLoading(true);
setError(null); setError(null);
@ -41,13 +39,13 @@ export function PortForwardPage() {
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}; }, [selectedClusterId]);
useEffect(() => { useEffect(() => {
loadPortForwards(); loadPortForwards();
const interval = setInterval(loadPortForwards, 5000); const interval = setInterval(loadPortForwards, 5000);
return () => clearInterval(interval); return () => clearInterval(interval);
}, [selectedClusterId]); }, [loadPortForwards]);
const handleStop = async (id: string) => { const handleStop = async (id: string) => {
try { try {

View File

@ -30,9 +30,6 @@ describe("LogStreamPanel — ANSI color support", () => {
/> />
); );
// Simulate receiving log line with ANSI color codes
const logLine = "\x1b[31mError: something went wrong\x1b[0m";
// Component should render the ANSI-colored line // Component should render the ANSI-colored line
rerender( rerender(
<LogStreamPanel <LogStreamPanel

View File

@ -19,4 +19,14 @@ export default defineConfig(async () => ({
resolve: { resolve: {
alias: { "@": path.resolve(__dirname, "./src") }, alias: { "@": path.resolve(__dirname, "./src") },
}, },
worker: {
format: "es",
},
optimizeDeps: {
include: [
"ansi-to-react",
"monaco-editor/esm/vs/language/json/json.worker",
"monaco-editor/esm/vs/editor/editor.worker",
],
},
})); }));