"use client"; import ConfirmActionDialog from "@/components/ConfirmActionDialog"; import DashboardLayout from "@/components/DashboardLayout"; import ToastViewport from "@/components/ToastViewport"; import { useState, useEffect, useCallback } from "react"; import { containers as containersApi, servers as serversApi, Server, } from "@/lib/api"; import { readCachedPageData, readStoredServerSelection, storeServerSelection, writeCachedPageData, } from "@/lib/page-state"; import { useToastManager } from "@/lib/use-toast-manager"; import AddDatabaseModal from "./components/AddDatabaseModal"; import DatabaseServerFilter from "./components/DatabaseServerFilter"; import DatabaseStatePanel from "./components/DatabaseStatePanel"; import DatabaseSummary from "./components/DatabaseSummary"; import DatabaseToolbar from "./components/DatabaseToolbar"; import DatabaseGrid from "./components/DatabaseGrid"; import { toDatabaseContainer } from "./components/database-utils"; import type { DatabaseContainer } from "./components/database-types"; const PAGE_KEY = "databases"; type PendingConfirmAction = { title: string; description: string; confirmLabel: string; tone?: "danger" | "warning" | "info"; note?: string; onConfirm: () => void; }; export default function DatabasesPage() { const { toasts, pushToast, dismissToast } = useToastManager(); const [data, setData] = useState([]); const [serverList, setServerList] = useState([]); const [selectedServerId, setSelectedServerId] = useState(""); const [loading, setLoading] = useState(true); const [search, setSearch] = useState(""); const [showAdd, setShowAdd] = useState(false); const [actioning, setActioning] = useState(null); const [removing, setRemoving] = useState(null); const [syncing, setSyncing] = useState(false); const [confirmDialog, setConfirmDialog] = useState(null); const applyCachedState = useCallback((serverId: string) => { const cached = readCachedPageData<{ items: DatabaseContainer[]; serverList: Server[]; }>(PAGE_KEY, serverId); if (!cached) return false; setData(cached.items); setServerList(cached.serverList); setLoading(false); return true; }, []); const load = useCallback( async (options?: { sync?: boolean; serverId?: string }) => { setLoading(true); try { const resolvedServerId = options?.serverId ?? ""; const sr = await serversApi.list(); const nextServers = sr.data ?? []; let ir; try { ir = options?.sync ? await containersApi.sync( resolvedServerId ? { serverId: resolvedServerId } : undefined, ) : await containersApi.list( resolvedServerId ? { serverId: resolvedServerId } : undefined, ); } catch (error) { if (!options?.sync) { throw error; } ir = await containersApi.list( resolvedServerId ? { serverId: resolvedServerId } : undefined, ); } const dbs = (ir.data ?? []) .map(toDatabaseContainer) .filter((item): item is DatabaseContainer => item !== null); setData(dbs); setServerList(nextServers); setSelectedServerId(resolvedServerId); storeServerSelection(PAGE_KEY, resolvedServerId); writeCachedPageData( PAGE_KEY, { items: dbs, serverList: nextServers, }, resolvedServerId, ); } catch { /* silently fail */ } finally { setLoading(false); } }, [], ); useEffect(() => { const storedServerId = readStoredServerSelection(PAGE_KEY); setSelectedServerId(storedServerId); const hasCache = applyCachedState(storedServerId); if (!hasCache) { void load({ sync: false, serverId: storedServerId }); } }, [applyCachedState, load]); const handleAction = async ( container: DatabaseContainer, action: "start" | "stop", ) => { setActioning(container.id); try { await containersApi.action(container.id, action); setData((current) => current.map((item) => item.id === container.id ? { ...item, status: action === "start" ? "RUNNING" : "STOPPED" } : item, ), ); pushToast({ tone: "success", title: "Database Action", message: `${container.name} ${action === "start" ? "started" : "stopped"} successfully`, showProgress: true, }); } catch (err: unknown) { pushToast({ tone: "error", title: "Database Action", message: err instanceof Error ? err.message : "Action failed", showProgress: true, }); } finally { setActioning(null); } }; const removeDatabase = async (id: string, name: string) => { setRemoving(id); try { await containersApi.action(id, "rm"); setData((prev) => prev.filter((d) => d.id !== id)); pushToast({ tone: "success", title: "Database Removed", message: `${name} removed successfully`, showProgress: true, }); } catch (err: unknown) { pushToast({ tone: "error", title: "Database Removed", message: err instanceof Error ? err.message : "Remove failed", showProgress: true, }); } finally { setRemoving(null); } }; const handleRemove = async (id: string, name: string) => { setConfirmDialog({ title: "Remove Database", description: `Remove database "${name}"?`, confirmLabel: "Remove Database", tone: "danger", note: "The database container will be removed from the selected server.", onConfirm: () => { void removeDatabase(id, name); }, }); }; const handleSync = async () => { setSyncing(true); try { await load({ sync: true, serverId: selectedServerId }); pushToast({ tone: "success", title: "Sync Complete", message: "Database inventory refreshed successfully", showProgress: true, }); } finally { setSyncing(false); } }; const handleServerChange = async (serverId: string) => { if (serverId === selectedServerId) return; storeServerSelection(PAGE_KEY, serverId); setSelectedServerId(serverId); const hasCache = applyCachedState(serverId); if (!hasCache) { await load({ sync: false, serverId }); } }; const query = search.trim().toLowerCase(); const filteredData = data.filter((db) => { if (!query) return true; return ( db.name.toLowerCase().includes(query) || db.image.toLowerCase().includes(query) || db.databaseLabel.toLowerCase().includes(query) || db.databaseType.toLowerCase().includes(query) || (db.server?.name ?? "").toLowerCase().includes(query) || (db.server?.ip ?? "").toLowerCase().includes(query) ); }); const running = data.filter((d) => d.status === "RUNNING").length; return ( setConfirmDialog(null)} onConfirm={() => { const current = confirmDialog; setConfirmDialog(null); current?.onConfirm(); }} /> {showAdd && ( setShowAdd(false)} onAdded={() => load({ sync: true, serverId: selectedServerId })} /> )}
d.serverId)).size} typeCount={new Set(data.map((d) => d.databaseType)).size} /> setShowAdd(true)} /> setShowAdd(true)} /> {/* Cards */} {!loading && filteredData.length > 0 && ( )}
); }