tftsr-devops_investigation/src/components/Kubernetes/CustomResourceList.tsx
Shaun Arman f7b4e591f9 fix(performance): resolve memory leaks and add polish features
- Fix LogStreamPanel event listener cleanup with synchronous unlisten
- Fix eventBus async-unsafe unsubscribe with proper error handling
- Fix KubernetesPage infinite loading by resetting state on section change
- Add ErrorBoundary component with reset capability
- Add Badge component with multiple variants
- Add ResourceDetailsDrawer for slide-out details panel
- Add useFavorites hook with localStorage persistence
- Add useKeyboardShortcuts hook for declarative shortcuts
- Add comprehensive test coverage for all new components/hooks
- Add keyboard shortcuts documentation to README
- Wrap KubernetesPage with ErrorBoundary for crash recovery
- Install react-window for virtual scrolling support

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-06-09 13:28:30 -05:00

116 lines
3.5 KiB
TypeScript

import React, { useCallback, useEffect, useState } from "react";
import { RefreshCw } from "lucide-react";
import { listCustomResourcesCmd } from "@/lib/tauriCommands";
import type { CustomResourceInfo, PrinterColumn } from "@/lib/tauriCommands";
interface CustomResourceListProps {
clusterId: string;
namespace: string;
group: string;
version: string;
resource: string;
kind: string;
printerColumns?: PrinterColumn[];
}
export function CustomResourceList({
clusterId,
namespace,
group,
version,
resource,
kind,
printerColumns = [],
}: CustomResourceListProps) {
const [items, setItems] = useState<CustomResourceInfo[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const loadItems = useCallback(async () => {
setLoading(true);
setError(null);
try {
const data = await listCustomResourcesCmd(clusterId, group, version, resource, namespace);
setItems(data);
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
} finally {
setLoading(false);
}
}, [clusterId, group, version, resource, namespace]);
useEffect(() => {
void loadItems();
}, [loadItems]);
if (loading) {
return (
<div className="flex items-center gap-2 text-muted-foreground text-sm py-2">
<RefreshCw className="h-4 w-4 animate-spin" />
Loading {kind} instances
</div>
);
}
if (error) {
return (
<div className="rounded-md border border-destructive/50 bg-destructive/10 px-3 py-2 text-sm text-destructive">
{error}
</div>
);
}
if (items.length === 0) {
return (
<p className="text-sm text-muted-foreground py-2">
No {kind} instances found.
</p>
);
}
const showNamespace = items.some((item) => item.namespace !== "");
// Filter printer columns by priority (0 = always show, higher = less important)
const visibleColumns = printerColumns.filter((col) => col.priority === 0);
return (
<div className="rounded-md border overflow-hidden">
<table className="w-full text-sm">
<thead>
<tr className="border-b text-muted-foreground bg-muted/30">
<th className="text-left px-4 py-2 font-medium">Name</th>
{showNamespace && (
<th className="text-left px-4 py-2 font-medium">Namespace</th>
)}
{visibleColumns.map((col) => (
<th key={col.name} className="text-left px-4 py-2 font-medium" title={col.description}>
{col.name}
</th>
))}
<th className="text-left px-4 py-2 font-medium">Age</th>
</tr>
</thead>
<tbody>
{items.map((item) => (
<tr
key={`${item.namespace}/${item.name}`}
className="border-b last:border-0 hover:bg-muted/20 transition-colors"
>
<td className="px-4 py-2 font-mono text-xs font-medium">{item.name}</td>
{showNamespace && (
<td className="px-4 py-2 text-muted-foreground">{item.namespace || "—"}</td>
)}
{visibleColumns.map((col) => (
<td key={col.name} className="px-4 py-2 text-muted-foreground">
{item.additional_columns[col.name] || "—"}
</td>
))}
<td className="px-4 py-2 text-muted-foreground">{item.age}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}