diff --git a/src/app/applications/page.tsx b/src/app/applications/page.tsx index 041c584..ced7cbb 100644 --- a/src/app/applications/page.tsx +++ b/src/app/applications/page.tsx @@ -20,9 +20,10 @@ import { useAsyncInterval } from "@/hooks/use-async"; export default function ApplicationsPage() { const { value: apps, setValue: setApps } = useAsyncInterval( + "apps", [], getApps, - 3000 + 3000, ); const remove = async (appId: string) => { diff --git a/src/app/deployments/page.tsx b/src/app/deployments/page.tsx index 93ea2d5..4bbb912 100644 --- a/src/app/deployments/page.tsx +++ b/src/app/deployments/page.tsx @@ -25,9 +25,14 @@ const statusDot = { } as const; export default function DeploymentsPage() { - const { value: apps } = useAsyncInterval([], getApps, 5000); + const { value: apps } = useAsyncInterval("apps", [], getApps, 5000); - const { value: deployments, loading } = useAsync([], getDeployments, [apps]); + const { value: deployments, loading } = useAsync( + "deployments", + [], + getDeployments, + [apps], + ); const appNameByContainer = useMemo(() => { const map = new Map(); diff --git a/src/app/environment/page.tsx b/src/app/environment/page.tsx index 458b577..6403dcc 100644 --- a/src/app/environment/page.tsx +++ b/src/app/environment/page.tsx @@ -21,11 +21,16 @@ import { useAsync } from "@/hooks/use-async"; export default function EnvironmentPage() { const [isSaving, setIsSaving] = useState(false); const [showSecrets, setShowSecrets] = useState>({}); - const { value: envGroups, setValue: setEnvGroups } = useAsync({}, getEnv); + const { value: envGroups, setValue: setEnvGroups } = useAsync( + "env", + {}, + getEnv, + ); const { value: selectedApp, setValue: setSelectedApp } = useAsync( + "selectedApp", "", async () => Object.keys(envGroups)[0] || "", - [envGroups] + [envGroups], ); const handleSave = async () => { diff --git a/src/app/monitoring/page.tsx b/src/app/monitoring/page.tsx index f290113..f67a656 100644 --- a/src/app/monitoring/page.tsx +++ b/src/app/monitoring/page.tsx @@ -31,10 +31,17 @@ export default function MonitoringPage() { const isMobile = useIsMobile(); // Metrics polling every 30 seconds - const { value: metrics } = useAsyncInterval([], getMetrics, 30000, []); + const { value: metrics } = useAsyncInterval( + "metrics", + [], + getMetrics, + 30000, + [], + ); // Live nodes polling every 3 seconds const { value: liveData } = useAsyncInterval( + "liveNodes", { cpuUsage: 0, ramUsage: 0 }, async () => { const nodes = await getLiveNodes(); @@ -44,11 +51,12 @@ export default function MonitoringPage() { }; }, 3000, - [] + [], ); // Container stats const { value: containerStats } = useAsync( + "containerStats", [], async () => { const nodes = await getNodes(); @@ -61,18 +69,18 @@ export default function MonitoringPage() { const stats = await getContainerStats(c.Id); if (!stats) return undefined; return { ...stats, name: c.Names[0] }; - }) - ) + }), + ), ) ).filter((s): s is SimpleStats & { name: string } => s !== undefined); }, - [] + [], ); const containerCPU = containerStats.reduce((acc, val) => acc + val.cpu, 0); const containerMemory = containerStats.reduce( (acc, val) => acc + val.memory, - 0 + 0, ); return ( diff --git a/src/app/nodes/page.tsx b/src/app/nodes/page.tsx index 70df860..30a4d15 100644 --- a/src/app/nodes/page.tsx +++ b/src/app/nodes/page.tsx @@ -35,14 +35,18 @@ function DiskUsage({ liveData }: NodeLiveData) { export default function NodesPage() { const { value: nodes, setValue: setNodes } = useAsyncInterval( + "liveNodes", [], getLiveNodes, - 3000 + 3000, ); - const { value: dockerNodes } = useAsync([], async () => getDocker(nodes), [ - nodes, - ]); + const { value: dockerNodes } = useAsync( + "docker", + [], + async () => getDocker(nodes), + [nodes], + ); return ( diff --git a/src/app/overview/page.tsx b/src/app/overview/page.tsx index 82d4666..285cb43 100644 --- a/src/app/overview/page.tsx +++ b/src/app/overview/page.tsx @@ -17,12 +17,21 @@ import { getDeployments } from "@/lib/dockship/deploy"; import { useAsync, useAsyncInterval } from "@/hooks/use-async"; export default function OverviewPage() { - const { value: nodes } = useAsyncInterval([], getLiveNodes, 3000); - const { value: dockerNodes } = useAsync([], async () => getDocker(nodes), [ - nodes, - ]); - const { value: apps } = useAsyncInterval([], getApps, 3000); + const { value: nodes } = useAsyncInterval( + "liveNodes", + [], + getLiveNodes, + 3000, + ); + const { value: dockerNodes } = useAsync( + "docker", + [], + async () => getDocker(nodes), + [nodes], + ); + const { value: apps } = useAsyncInterval("apps", [], getApps, 3000); const { value: deployments } = useAsyncInterval( + "deployments", [], async () => (await getDeployments()).filter((d) => d !== null), 3000, diff --git a/src/components/dialogs/DeployApp.tsx b/src/components/dialogs/DeployApp.tsx index 56351ab..5c48506 100644 --- a/src/components/dialogs/DeployApp.tsx +++ b/src/components/dialogs/DeployApp.tsx @@ -28,8 +28,8 @@ export default function DeployAppDialog({ defaultApp?: string; children: ReactNode; }) { - const { value: nodes } = useAsync([], getLiveNodes); - const { value: apps } = useAsync([], getApps); + const { value: nodes } = useAsync("liveNodes", [], getLiveNodes); + const { value: apps } = useAsync("apps", [], getApps); const [name, setName] = useState(""); const [app, setApp] = useState(defaultApp || ""); const [node, setNode] = useState(""); diff --git a/src/components/dialogs/RegisterApp.tsx b/src/components/dialogs/RegisterApp.tsx index 955914e..2cecf83 100644 --- a/src/components/dialogs/RegisterApp.tsx +++ b/src/components/dialogs/RegisterApp.tsx @@ -13,10 +13,7 @@ import { } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { getLiveNodes } from "@/lib/dockship/node"; -import { Combobox } from "../ui/combobox"; import { registerApp } from "@/lib/dockship/application"; -import { useAsync } from "@/hooks/use-async"; export default function RegisterAppDialog({ children, diff --git a/src/hooks/use-async.ts b/src/hooks/use-async.ts index 260b37c..973d976 100644 --- a/src/hooks/use-async.ts +++ b/src/hooks/use-async.ts @@ -1,20 +1,31 @@ import { useState, useEffect } from "react"; export function useAsync( + cacheKey: string, defaultValue: T, asyncFunction: () => Promise, - dependencies: any[] = [] + dependencies: any[] = [], ) { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [value, setValue] = useState(defaultValue); + const [value, setValue] = useState(() => { + try { + const cached = localStorage.getItem(cacheKey); + return cached ? (JSON.parse(cached) as T) : defaultValue; + } catch { + return defaultValue; + } + }); useEffect(() => { let isMounted = true; setLoading(true); + asyncFunction() .then((result) => { - if (isMounted) setValue(result); + if (!isMounted) return; + setValue(result); + localStorage.setItem(cacheKey, JSON.stringify(result)); }) .catch((err) => { if (isMounted) setError(err); @@ -32,35 +43,42 @@ export function useAsync( } export function useAsyncInterval( + cacheKey: string, defaultValue: T, asyncFunction: () => Promise, interval: number, - dependencies: any[] = [] + dependencies: any[] = [], ) { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [value, setValue] = useState(defaultValue); + const [value, setValue] = useState(() => { + try { + const cached = localStorage.getItem(cacheKey); + return cached ? (JSON.parse(cached) as T) : defaultValue; + } catch { + return defaultValue; + } + }); useEffect(() => { let isMounted = true; - const run = () => { + const run = async () => { setLoading(true); - asyncFunction() - .then((result) => { - if (isMounted) setValue(result); - }) - .catch((err) => { - if (isMounted) setError(err); - }) - .finally(() => { - if (isMounted) setLoading(false); - }); + try { + const result = await asyncFunction(); + if (!isMounted) return; + setValue(result); + localStorage.setItem(cacheKey, JSON.stringify(result)); + } catch (err) { + if (isMounted) setError(err as Error); + } finally { + if (isMounted) setLoading(false); + } }; - const iid = setInterval(run, interval); - run(); + const iid = setInterval(run, interval); return () => { isMounted = false;