Compare commits

..

6 Commits

Author SHA1 Message Date
1c4c76329f Merge pull request 'feat(kube): FreeLens parity — PTY shells, metrics, port-forward, and UX fixes' (#88) from feature/freelens-parity-complete into master
Some checks failed
Auto Tag / autotag (push) Successful in 7s
Auto Tag / wiki-sync (push) Successful in 9s
Auto Tag / changelog (push) Successful in 1m30s
Test / frontend-tests (push) Successful in 1m49s
Test / frontend-typecheck (push) Successful in 1m55s
Auto Tag / build-linux-amd64 (push) Successful in 11m32s
Auto Tag / build-macos-arm64 (push) Successful in 14m40s
Auto Tag / build-windows-amd64 (push) Successful in 13m9s
Auto Tag / build-linux-arm64 (push) Successful in 13m39s
Test / rust-fmt-check (push) Successful in 18m11s
Test / rust-clippy (push) Successful in 19m54s
Test / rust-tests (push) Successful in 21m51s
Renovate / renovate (push) Failing after 18s
Reviewed-on: #88
2026-06-10 02:10:29 +00:00
39c3011a9d Merge branch 'master' into feature/freelens-parity-complete
Some checks failed
PR Review Automation / review (pull_request) Has been cancelled
Test / rust-fmt-check (pull_request) Has been cancelled
Test / frontend-typecheck (pull_request) Has been cancelled
Test / rust-clippy (pull_request) Has been cancelled
Test / frontend-tests (pull_request) Has been cancelled
Test / rust-tests (pull_request) Has been cancelled
2026-06-10 02:10:18 +00:00
Shaun Arman
06d29b8042 fix(fmt): collapse single-expression restart count closure per rustfmt
Some checks failed
PR Review Automation / review (pull_request) Has been cancelled
Test / frontend-tests (pull_request) Successful in 1m46s
Test / frontend-typecheck (pull_request) Successful in 1m55s
Test / rust-fmt-check (pull_request) Successful in 12m2s
Test / rust-clippy (pull_request) Successful in 13m48s
Test / rust-tests (pull_request) Successful in 15m17s
2026-06-09 20:54:26 -05:00
Shaun Arman
f993672b78 fix(kube): configure Monaco for offline use and fix pod column data (IP/Node/CPU/Memory)
Some checks failed
Test / frontend-tests (pull_request) Successful in 1m48s
Test / frontend-typecheck (pull_request) Successful in 1m55s
PR Review Automation / review (pull_request) Successful in 4m9s
Test / rust-fmt-check (pull_request) Failing after 12m32s
Test / rust-clippy (pull_request) Has been cancelled
Test / rust-tests (pull_request) Has been cancelled
Monaco: install monaco-editor and configure @monaco-editor/react loader to use the locally
bundled module in main.tsx instead of the CDN, resolving the perpetual spinner in YamlEditor
under Tauri's offline WebView. Added worker format and optimizeDeps entries to vite.config.ts.

Pod columns: extend PodInfo struct with ip, node, and restarts fields; extract podIP, nodeName,
and restartCount from kubectl JSON output in parse_pods_json so the IP, Node columns render
real data instead of blanks.

Also fix ref-during-render lint error in useKeyboardShortcuts.
2026-06-09 20:39:10 -05:00
Shaun Arman
399ba30c6b 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
2026-06-09 20:38:24 -05:00
Shaun Arman
3afa97b517 feat(kube): add YAML edit action to NamespaceList
Namespaces had a read-only table with no actions. Adds an edit button per
row that fetches the namespace YAML via getResourceYamlCmd (cluster-scoped,
empty namespace param) and opens EditResourceModal.
2026-06-09 20:37:57 -05:00
16 changed files with 390 additions and 51 deletions

39
package-lock.json generated
View File

@ -20,6 +20,7 @@
"class-variance-authority": "^0.7",
"clsx": "^2",
"lucide-react": "latest",
"monaco-editor": "^0.55.1",
"react": "^19",
"react-chartjs-2": "^5.3.1",
"react-diff-viewer-continued": "^4",
@ -3001,6 +3002,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true
},
"node_modules/@types/unist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
@ -5706,6 +5714,15 @@
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
"node_modules/dompurify": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz",
"integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/domutils": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
@ -9404,6 +9421,18 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/marked": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz",
"integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@ -10603,6 +10632,16 @@
"node": ">=18.0.0"
}
},
"node_modules/monaco-editor": {
"version": "0.55.1",
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz",
"integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==",
"license": "MIT",
"dependencies": {
"dompurify": "3.2.7",
"marked": "14.0.0"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",

View File

@ -27,6 +27,7 @@
"class-variance-authority": "^0.7",
"clsx": "^2",
"lucide-react": "latest",
"monaco-editor": "^0.55.1",
"react": "^19",
"react-chartjs-2": "^5.3.1",
"react-diff-viewer-continued": "^4",

View File

@ -96,6 +96,9 @@ pub struct PodInfo {
pub ready: String,
pub age: String,
pub containers: Vec<String>,
pub restarts: Option<u32>,
pub ip: Option<String>,
pub node: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -1159,6 +1162,31 @@ fn parse_pods_json(json_str: &str) -> Result<Vec<PodInfo>, String> {
})
.unwrap_or_default();
let restarts = item
.get("status")
.and_then(|s| s.get("containerStatuses"))
.and_then(|c| c.as_array())
.map(|container_statuses| {
container_statuses
.iter()
.map(|c| c.get("restartCount").and_then(|r| r.as_u64()).unwrap_or(0) as u32)
.sum::<u32>()
});
let ip = item
.get("status")
.and_then(|s| s.get("podIP"))
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.map(|s| s.to_string());
let node = item
.get("spec")
.and_then(|s| s.get("nodeName"))
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.map(|s| s.to_string());
pods.push(PodInfo {
name,
namespace,
@ -1166,6 +1194,9 @@ fn parse_pods_json(json_str: &str) -> Result<Vec<PodInfo>, String> {
ready,
age,
containers,
restarts,
ip,
node,
});
}

View File

@ -69,5 +69,11 @@ function getStatusVariant(status: string): BadgeProps["variant"] {
if (normalized === "succeeded" || normalized === "completed" || normalized === "bound") {
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";
}

View File

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

View File

@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
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 {
Dialog,
DialogContent,
@ -12,6 +12,10 @@ import {
} from "@/components/ui";
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 {
clusterId: string;
namespace: string;

View File

@ -1,6 +1,9 @@
import React from "react";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Badge } from "@/components/ui";
import React, { useState } from "react";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Badge, Button } from "@/components/ui";
import { Pencil } from "lucide-react";
import type { NamespaceResourceInfo } from "@/lib/tauriCommands";
import { getResourceYamlCmd } from "@/lib/tauriCommands";
import { EditResourceModal } from "./EditResourceModal";
interface NamespaceListProps {
items: NamespaceResourceInfo[];
@ -14,21 +17,46 @@ function statusVariant(status: string): "success" | "destructive" | "secondary"
return "secondary";
}
export function NamespaceList({ items }: NamespaceListProps) {
export function NamespaceList({ items, clusterId }: NamespaceListProps) {
const [editState, setEditState] = useState<{
open: boolean;
name: string;
yaml: string;
} | null>(null);
const [editError, setEditError] = useState<string | null>(null);
const openEdit = async (ns: NamespaceResourceInfo) => {
setEditError(null);
try {
// Namespaces are cluster-scoped — pass empty string for namespace param
const yaml = await getResourceYamlCmd(clusterId, "namespaces", "", ns.name);
setEditState({ open: true, name: ns.name, yaml });
} catch (err) {
setEditError(err instanceof Error ? err.message : String(err));
}
};
return (
<div className="overflow-x-auto">
{editError && (
<div className="mb-2 rounded-md border border-destructive/50 bg-destructive/10 px-3 py-2 text-sm text-destructive">
{editError}
</div>
)}
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Status</TableHead>
<TableHead>Age</TableHead>
<TableHead className="w-16 text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{items.length === 0 ? (
<TableRow>
<TableCell colSpan={3} className="text-center text-muted-foreground">
<TableCell colSpan={4} className="text-center text-muted-foreground">
No namespaces found
</TableCell>
</TableRow>
@ -40,11 +68,35 @@ export function NamespaceList({ items }: NamespaceListProps) {
<Badge variant={statusVariant(ns.status)}>{ns.status}</Badge>
</TableCell>
<TableCell className="text-sm text-muted-foreground">{ns.age}</TableCell>
<TableCell className="text-right">
<Button
size="sm"
variant="ghost"
className="h-7 w-7 p-0"
title="Edit YAML"
onClick={() => void openEdit(ns)}
>
<Pencil className="h-3.5 w-3.5" />
<span className="sr-only">Edit</span>
</Button>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
{editState && (
<EditResourceModal
isOpen={editState.open}
clusterId={clusterId}
namespace=""
resourceType="namespaces"
resourceName={editState.name}
initialYaml={editState.yaml}
onClose={() => setEditState(null)}
/>
)}
</div>
);
}

View File

@ -4,8 +4,9 @@ import { Badge } from "@/components/ui";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } 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 { PortForwardDialog } from "./PortForwardDialog";
import { YamlEditor } from "./YamlEditor";
import { getPodLogsCmd } 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 [logsLoading, setLogsLoading] = React.useState(false);
const [logsError, setLogsError] = React.useState<string | null>(null);
const [portForwardOpen, setPortForwardOpen] = React.useState(false);
const fetchLogs = React.useCallback(
async (containerName: string) => {
@ -66,11 +68,25 @@ export function PodDetail({ clusterId, namespace, pod, onClose }: PodDetailProps
<h2 className="text-xl font-semibold">Pod: {pod.name}</h2>
<Badge variant="outline">{namespace}</Badge>
</div>
<Button variant="ghost" size="sm" onClick={onClose}>
<X className="w-4 h-4" />
</Button>
<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}>
<X className="w-4 h-4" />
</Button>
</div>
</div>
<PortForwardDialog
open={portForwardOpen}
onOpenChange={setPortForwardOpen}
clusterId={clusterId}
namespace={namespace}
podName={pod.name}
/>
<Tabs value={activeTab} onValueChange={handleTabChange}>
<TabsList className="grid grid-cols-3 mb-4">
<TabsTrigger value="overview">Overview</TabsTrigger>

View File

@ -1,6 +1,6 @@
import React, { useState } from "react";
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 type { PodInfo } 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 { DEFAULT_COLUMNS } from "@/config/defaultColumns";
import { ColumnConfigModal } from "@/components/tables/ColumnConfigModal";
import { QuickActionColumn } from "@/components/tables/QuickActionColumn";
interface PodListProps {
pods: PodInfo[];
@ -49,23 +48,6 @@ export function PodList({ pods, clusterId, namespace, onRefresh }: PodListProps)
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) => {
setEditError(null);
try {
@ -152,9 +134,7 @@ export function PodList({ pods, clusterId, namespace, onRefresh }: PodListProps)
)}
{isColumnVisible("status") && (
<TableCell>
<Badge className={`${getPodStatusColor(pod.status)} text-white`}>
{pod.status}
</Badge>
<StatusBadge status={pod.status} />
</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

@ -13,7 +13,10 @@ export interface KeyboardShortcut {
export function useKeyboardShortcuts(shortcuts: KeyboardShortcut[]): void {
const shortcutsRef = useRef(shortcuts);
shortcutsRef.current = shortcuts;
useEffect(() => {
shortcutsRef.current = shortcuts;
}, [shortcuts]);
const handleKeyDown = useCallback((event: KeyboardEvent) => {
for (const shortcut of shortcutsRef.current) {

View File

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

View File

@ -1,9 +1,15 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import { loader } from "@monaco-editor/react";
import * as monaco from "monaco-editor";
import App from "./App";
import "./styles/globals.css";
// Use the locally bundled Monaco instead of loading from CDN.
// Tauri's WebView has no internet access so the default CDN loader never resolves.
loader.config({ monaco });
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<BrowserRouter>

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

View File

@ -19,4 +19,14 @@ export default defineConfig(async () => ({
resolve: {
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",
],
},
}));