+
+ {hasContainers && (
+
+ }
+ onClick={() => changeView('cards')}
+ >
+ Cards
+
+ }
+ onClick={() => changeView('table')}
+ >
+ Table
+
+
+ )}
-
+
}>
+
Nodes
+
-
}>
- New container
+
}>
+
New container
@@ -117,7 +316,47 @@ export function ContainersListPage() {
)}
- {data && data.length > 0 && (
+ {hasContainers && view === 'cards' && (
+
+ {data.map((c: Container) => (
+
+
+
+ {c.hostname}
+
+ {c.status}
+
+
+
+
+
+
+
+
+
+
+ {templateTitle(c.template)}
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+ )}
+
+ {hasContainers && view === 'table' && (
@@ -138,106 +377,19 @@ export function ContainersListPage() {
{c.status}
- {c.nodeApiUrl ? (
-
- {c.nodeName || c.nodeApiUrl}
-
- ) : (
- c.nodeName || '—'
- )}
+
-
- {c.template || '—'}
+
+ {templateTitle(c.template)}
- {c.httpEntries.length === 0 ? (
- '—'
- ) : (
-
- )}
+
-
- {c.sshHost && c.sshPort ? (
-
-
{c.sshHost}:{c.sshPort}
- {sessionUser && (
- <>
-
-
-
-
-
-
- >
- )}
-
- ) : (
- '—'
- )}
+
+
- {c.creationJobId && (
-
-
-
- )}
-
- }>
- Edit
-
-
- }
- onClick={() => {
- if (confirm(`Delete container "${c.hostname}"?`)) del.mutate(c.id);
- }}
- disabled={del.isPending}
- >
- Delete
-
+
))}
diff --git a/create-a-container/client/src/pages/nodes/NodesListPage.tsx b/create-a-container/client/src/pages/nodes/NodesListPage.tsx
index d2628c06..004b146e 100644
--- a/create-a-container/client/src/pages/nodes/NodesListPage.tsx
+++ b/create-a-container/client/src/pages/nodes/NodesListPage.tsx
@@ -1,3 +1,4 @@
+import { useState } from 'react';
import { Link, useParams } from 'react-router';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import {
@@ -5,6 +6,8 @@ import {
AlertDescription,
Badge,
Button,
+ Card,
+ CardTitle,
PageHeader,
Spinner,
Table,
@@ -15,11 +18,71 @@ import {
TableRow,
useToast,
} from '@mieweb/ui';
-import { Download, Pencil, Plus, Server, Trash2 } from 'lucide-react';
+import { Download, LayoutGrid, Pencil, Plus, Rows3, Server, Trash2 } from 'lucide-react';
import { api, ApiError } from '@/lib/api';
import { keys, queries } from '@/lib/queries';
import type { Node } from '@/lib/types';
+type ViewMode = 'cards' | 'table';
+const VIEW_STORAGE_KEY = 'nodes:view';
+
+function Meta({ label, children }: { label: string; children: React.ReactNode }) {
+ return (
+
+
+ {label}
+
+ {children}
+
+ );
+}
+
+function NvidiaBadge({ n }: { n: Node }) {
+ return n.nvidiaAvailable ? (
+ Available
+ ) : (
+ No
+ );
+}
+
+function CredentialsBadge({ n }: { n: Node }) {
+ return n.hasSecret ? Set : Missing;
+}
+
+function RowActions({
+ n,
+ siteId,
+ onDelete,
+ deleting,
+}: {
+ n: Node;
+ siteId?: string;
+ onDelete: (id: number) => void;
+ deleting: boolean;
+}) {
+ return (
+ <>
+
+ }>
+ Edit
+
+
+ }
+ onClick={() => {
+ if (confirm(`Delete node "${n.name}"?`)) onDelete(n.id);
+ }}
+ disabled={deleting}
+ >
+ Delete
+
+ >
+ );
+}
+
export function NodesListPage() {
const { siteId } = useParams<{ siteId: string }>();
const qc = useQueryClient();
@@ -31,6 +94,19 @@ export function NodesListPage() {
enabled: !!siteId,
});
+ const [view, setView] = useState(() => {
+ const stored = typeof window !== 'undefined' ? window.localStorage.getItem(VIEW_STORAGE_KEY) : null;
+ return stored === 'table' ? 'table' : 'cards';
+ });
+ const changeView = (next: ViewMode) => {
+ setView(next);
+ try {
+ window.localStorage.setItem(VIEW_STORAGE_KEY, next);
+ } catch {
+ /* ignore storage failures */
+ }
+ };
+
const del = useMutation({
mutationFn: (id: number) => api.delete(`/api/v1/sites/${siteId}/nodes/${id}`),
onSuccess: () => {
@@ -40,6 +116,8 @@ export function NodesListPage() {
onError: (err: ApiError) => toast.error(err.message),
});
+ const hasNodes = !!data && data.length > 0;
+
return (
}
actions={
-
+
+ {hasNodes && (
+
+ }
+ onClick={() => changeView('cards')}
+ >
+ Cards
+
+ }
+ onClick={() => changeView('table')}
+ >
+ Table
+
+
+ )}
-
}>Import from Proxmox
+
}>
+
Import from Proxmox
+
-
}>New node
+
}>
+
New node
+
}
/>
{error &&
{(error as ApiError).message}}
{isLoading &&
}
- {data && (
+ {data && data.length === 0 && (
+
+ No nodes yet. Add one with the button above.
+
+ )}
+
+ {hasNodes && view === 'cards' && (
+
+ {data.map((n: Node) => (
+
+
+
+ {n.name}
+
+
+
+
+
+
+
+
+ {n.ipv4Address || '—'}
+
+
+
+ {n.apiUrl || '—'}
+
+
+
+
+
+
+
+ ))}
+
+ )}
+
+ {hasNodes && view === 'table' && (
@@ -77,21 +230,14 @@ export function NodesListPage() {
{n.name}
{n.ipv4Address || '—'}
{n.apiUrl || '—'}
- {n.nvidiaAvailable ? Available : No}
- {n.hasSecret ? Set : Missing}
+
+
+
+
+
+
-
- }>Edit
-
- }
- onClick={() => { if (confirm(`Delete node "${n.name}"?`)) del.mutate(n.id); }}
- disabled={del.isPending}
- >
- Delete
-
+
))}