Some checks failed
Test / frontend-typecheck (pull_request) Successful in 1m48s
Test / frontend-tests (pull_request) Successful in 1m33s
PR Review Automation / review (pull_request) Successful in 6m23s
Test / rust-fmt-check (pull_request) Successful in 13m8s
Test / rust-tests (pull_request) Has been cancelled
Test / rust-clippy (pull_request) Has been cancelled
- Backend: kube module with ClusterClient, PortForwardSession, RefreshRegistry - 7 Tauri IPC commands: add_cluster, remove_cluster, list_clusters, start_port_forward, stop_port_forward, list_port_forwards, delete_port_forward, shutdown_port_forwards - AppState extended with clusters, port_forwards, refresh_registry fields - Version bumped to 1.1.0 in Cargo.toml and package.json - Auto-tag workflow updated to mark releases as draft (pre-release) - Buy Me A Coffee section added to README.md - Fixed changelog workflow to only include current tag commits - Proper kubeconfig YAML parsing with extract_context and extract_server_url - Added kubeconfig content storage in ClusterClient - Updated PortForwardSession to include cluster_name - Frontend GUI components: ClusterList, PortForwardList, AddClusterModal, PortForwardForm, KubernetesPage - TypeScript types and IPC commands for Kubernetes management - Unit tests for Kubernetes IPC commands (6 tests) - All 332 Rust tests passing - All 98 frontend tests passing - TypeScript type checks passing - Project builds successfully in release mode - Committed and pushed to feature/kubernetes-management branch - Command injection vulnerability fixed with regex validation and max length check (253 chars) - stop_port_forward and shutdown_port_forwards properly kill kubectl child processes via async child management - Temp file cleanup implemented with RAII TempFileCleanup struct created before std::fs::write - discover_pods now parses actual kubectl JSON output - ChildWaitHandle implemented with background task for waiting on kubectl child - PortForwardSession uses Arc<TokioMutex<Option<Child>>> for async-safe child management - Port-forward uses kubectl's dynamic port binding (0) instead of TcpListener - Added shutdown_port_forwards command for app shutdown cleanup - Added cleanup effect in App.tsx to call shutdownPortForwardsCmd on unmount - Database CRUD operations for clusters and port_forwards added to db.rs - validate_resource_name uses lazy_static! for cached Regex to prevent ReDoS - Cluster struct updated to store kubeconfig_content directly instead of kubeconfig_id - Cluster model in db/models.rs updated to use kubeconfig_content field - load_clusters and load_port_forwards commands registered in lib.rs - Temp file cleanup moved to background task in ChildWaitHandle to ensure cleanup after kubectl completes - Unused child_id field removed from ChildWaitHandle - Command validation moved to beginning of start_port_forward before any operations - Fixed lint errors: removed unused imports, fixed React hooks order, updated type annotations - Updated eslint.config.js to properly configure file patterns
186 lines
6.9 KiB
TypeScript
186 lines
6.9 KiB
TypeScript
import React, { useState, useEffect } from "react";
|
|
import { X, Loader2 } from "lucide-react";
|
|
import { Button } from "@/components/ui";
|
|
import type { PortForwardResponse } from "@/lib/tauriCommands";
|
|
import { startPortForwardCmd } from "@/lib/tauriCommands";
|
|
import { listClustersCmd } from "@/lib/tauriCommands";
|
|
|
|
interface PortForwardFormProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
onStart: (portForward: PortForwardResponse) => void;
|
|
}
|
|
|
|
export function PortForwardForm({ isOpen, onClose, onStart }: PortForwardFormProps) {
|
|
const [clusterId, setClusterId] = useState("");
|
|
const [namespace, setNamespace] = useState("default");
|
|
const [pod, setPod] = useState("");
|
|
const [containerPort, setContainerPort] = useState<string>("80");
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [error, setError] = useState("");
|
|
const [clusters, setClusters] = useState<{ id: string; name: string }[]>([]);
|
|
|
|
useEffect(() => {
|
|
if (isOpen) {
|
|
loadClusters();
|
|
}
|
|
}, [isOpen]);
|
|
|
|
if (!isOpen) return null;
|
|
|
|
const loadClusters = async () => {
|
|
try {
|
|
const clusters = await listClustersCmd();
|
|
setClusters(clusters.map((c) => ({ id: c.id, name: c.name })));
|
|
} catch (err) {
|
|
console.error("Failed to load clusters:", err);
|
|
}
|
|
};
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setError("");
|
|
|
|
if (!clusterId || !namespace || !pod || !containerPort) {
|
|
setError("All fields are required");
|
|
return;
|
|
}
|
|
|
|
if (containerPort.trim() === "") {
|
|
setError("Container port is required");
|
|
return;
|
|
}
|
|
|
|
const port = parseInt(containerPort, 10);
|
|
if (isNaN(port) || port < 1 || port > 65535) {
|
|
setError("Container port must be a valid port number (1-65535)");
|
|
return;
|
|
}
|
|
|
|
setIsLoading(true);
|
|
|
|
try {
|
|
const portForward = await startPortForwardCmd({
|
|
cluster_id: clusterId,
|
|
namespace,
|
|
pod,
|
|
container_port: port,
|
|
});
|
|
onStart(portForward);
|
|
onClose();
|
|
setClusterId("");
|
|
setNamespace("default");
|
|
setPod("");
|
|
setContainerPort("80");
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : "Failed to start port forward");
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
|
<div className="w-full max-w-lg rounded-lg border bg-background shadow-lg">
|
|
<div className="flex items-center justify-between border-b px-6 py-4">
|
|
<h3 className="text-lg font-semibold">Start Port Forward</h3>
|
|
<button
|
|
onClick={onClose}
|
|
className="rounded-md p-1 text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
|
>
|
|
<X className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit} className="p-6 space-y-4">
|
|
{error && (
|
|
<div className="rounded-md bg-destructive/15 px-4 py-3 text-sm text-destructive">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
|
Cluster
|
|
</label>
|
|
<select
|
|
value={clusterId}
|
|
onChange={(e) => setClusterId(e.target.value)}
|
|
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
|
disabled={isLoading}
|
|
>
|
|
<option value="" disabled>
|
|
Select a cluster
|
|
</option>
|
|
{clusters.map((c) => (
|
|
<option key={c.id} value={c.id}>
|
|
{c.name} ({c.id})
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
|
Namespace
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={namespace}
|
|
onChange={(e) => setNamespace(e.target.value)}
|
|
placeholder="default"
|
|
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
|
disabled={isLoading}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
|
Pod Name
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={pod}
|
|
onChange={(e) => setPod(e.target.value)}
|
|
placeholder="e.g., nginx-abc123"
|
|
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
|
disabled={isLoading}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
|
Container Port
|
|
</label>
|
|
<input
|
|
type="number"
|
|
value={containerPort}
|
|
onChange={(e) => setContainerPort(e.target.value)}
|
|
placeholder="80"
|
|
min="1"
|
|
max="65535"
|
|
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
|
disabled={isLoading}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex justify-end gap-2 pt-4">
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={onClose}
|
|
disabled={isLoading}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button type="submit" disabled={isLoading}>
|
|
{isLoading && <Loader2 className="w-4 h-4 mr-2 animate-spin" />}
|
|
Start Port Forward
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|