Implemented useAsync in the app structure for cleaner code

This commit is contained in:
2026-01-03 03:58:32 +01:00
parent 533e832e44
commit 2cc324c517
10 changed files with 177 additions and 182 deletions
+6 -15
View File
@@ -13,26 +13,17 @@ import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { MoreVertical, Plus, Rocket } from "lucide-react"; import { MoreVertical, Plus, Rocket } from "lucide-react";
import InstallApplicationDialog from "@/components/dialogs/InstallApplication"; import InstallApplicationDialog from "@/components/dialogs/InstallApplication";
import { useEffect, useState } from "react";
import { getApplications, removeApp } from "@/lib/dockship/application"; import { getApplications, removeApp } from "@/lib/dockship/application";
import RemoveDialog from "@/components/dialogs/Remove"; import RemoveDialog from "@/components/dialogs/Remove";
import { Application } from "@/lib/types";
import DeployAppDialog from "@/components/dialogs/DeployApp"; import DeployAppDialog from "@/components/dialogs/DeployApp";
import { useAsyncInterval } from "@/hooks/use-async";
export default function ApplicationsPage() { export default function ApplicationsPage() {
const [applications, setApplications] = useState<Application[]>([]); const { value: applications, setValue: setApplications } = useAsyncInterval(
[],
useEffect(() => { getApplications,
const fetch = async () => { 3000
const apps = await getApplications(); );
setApplications(apps);
};
fetch();
const iid = setInterval(fetch, 3000);
return () => clearInterval(iid);
}, []);
const remove = async (appId: string) => { const remove = async (appId: string) => {
await removeApp(appId); await removeApp(appId);
+7 -29
View File
@@ -11,6 +11,7 @@ import { getDeployments } from "@/lib/dockship/deploy";
import { getApplications } from "@/lib/dockship/application"; import { getApplications } from "@/lib/dockship/application";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import DeployAppDialog from "@/components/dialogs/DeployApp"; import DeployAppDialog from "@/components/dialogs/DeployApp";
import { useAsync } from "@/hooks/use-async";
const statusDot = { const statusDot = {
running: "bg-chart-2", running: "bg-chart-2",
@@ -19,36 +20,13 @@ const statusDot = {
} as const; } as const;
export default function DeploymentsPage() { export default function DeploymentsPage() {
const [applications, setApplications] = useState<Application[]>([]); const { value: applications } = useAsync([], getApplications);
const [deployments, setDeployments] = useState<Deployment[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => { const { value: deployments, loading } = useAsync(
let mounted = true; [],
async () => (await getDeployments()).filter((d) => d !== null),
async function fetchData() { [applications]
try { );
const [apps, deps] = await Promise.all([
getApplications(),
getDeployments(),
]);
if (!mounted) return;
setApplications(apps);
setDeployments(deps.filter((d) => d !== null));
} catch (err) {
console.error("Failed to load deployments:", err);
} finally {
if (mounted) setLoading(false);
}
}
fetchData();
return () => {
mounted = false;
};
}, []);
const appNameByContainer = useMemo(() => { const appNameByContainer = useMemo(() => {
const map = new Map<string, string>(); const map = new Map<string, string>();
+7 -13
View File
@@ -16,22 +16,16 @@ import {
DropdownMenuItem, DropdownMenuItem,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { ChevronDown } from "lucide-react"; import { ChevronDown } from "lucide-react";
import { useAsync } from "@/hooks/use-async";
export default function EnvironmentPage() { export default function EnvironmentPage() {
const [selectedApp, setSelectedApp] = useState("");
const [showSecrets, setShowSecrets] = useState<Record<string, boolean>>({});
const [envGroups, setEnvGroups] = useState<Record<string, Env>>({});
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [showSecrets, setShowSecrets] = useState<Record<string, boolean>>({});
useEffect(() => { const { value: envGroups, setValue: setEnvGroups } = useAsync({}, getEnv);
const fetch = async () => { const { value: selectedApp, setValue: setSelectedApp } = useAsync(
const env = await getEnv(); "",
setEnvGroups(env); async () => Object.keys(envGroups)[0] || ""
const vars = Object.keys(env); );
if (vars.length > 0) setSelectedApp(vars[0]);
};
fetch();
}, []);
const handleSave = async () => { const handleSave = async () => {
setIsSaving(true); setIsSaving(true);
+7 -14
View File
@@ -1,6 +1,6 @@
"use client"; "use client";
import { useEffect, useState } from "react"; import { useState } from "react";
import { DashboardLayout } from "@/components/dashboard/DashboardLayout"; import { DashboardLayout } from "@/components/dashboard/DashboardLayout";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -9,25 +9,18 @@ import { Pause, Play, Download, Filter, RefreshCw } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Log } from "@/lib/types"; import { Log } from "@/lib/types";
import { getAllContainerLogs } from "@/lib/dockship/docker"; import { getAllContainerLogs } from "@/lib/dockship/docker";
import { useAsyncInterval } from "@/hooks/use-async";
export default function LogsPage() { export default function LogsPage() {
const [isPaused, setIsPaused] = useState(false); const [isPaused, setIsPaused] = useState(false);
const [filter, setFilter] = useState<"all" | "info" | "warn" | "error">( const [filter, setFilter] = useState<"all" | "info" | "warn" | "error">(
"all" "all"
); );
const [logs, setLogs] = useState<Log[]>([]); const { value: logs, setValue: setLogs } = useAsyncInterval(
[],
useEffect(() => { async () => (isPaused ? ([] as Log[]) : getAllContainerLogs()),
const fetch = async () => { 100
if (!isPaused) setLogs(await getAllContainerLogs()); );
};
const iid = setInterval(fetch, 1000);
fetch();
return () => clearInterval(iid);
}, []);
const filteredLogs = logs.filter((log) => const filteredLogs = logs.filter((log) =>
filter === "all" ? true : log.level === filter filter === "all" ? true : log.level === filter
+47 -45
View File
@@ -13,6 +13,7 @@ import { getLiveNodes, getNodes } from "@/lib/dockship/node";
import { average } from "@/lib/format"; import { average } from "@/lib/format";
import { getContainerStats, getDocker } from "@/lib/dockship/docker"; import { getContainerStats, getDocker } from "@/lib/dockship/docker";
import { SimpleStats } from "@/lib/types"; import { SimpleStats } from "@/lib/types";
import { useAsync, useAsyncInterval } from "@/hooks/use-async";
const cpuChartConfig = { const cpuChartConfig = {
cpu: { label: "CPU Usage", color: "var(--chart-1)" }, cpu: { label: "CPU Usage", color: "var(--chart-1)" },
@@ -29,73 +30,74 @@ const networkChartConfig = {
export default function MonitoringPage() { export default function MonitoringPage() {
const [data, setData] = useState< const [data, setData] = useState<
{ { time: string; cpu: number; memory: number; in: number; out: number }[]
time: string;
cpu: number;
memory: number;
in: number;
out: number;
}[]
>([]); >([]);
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const [liveData, setLiveData] = useState<{ const [liveData, setLiveData] = useState<{
cpuUsage: number; cpuUsage: number;
ramUsage: number; ramUsage: number;
}>(); }>();
const [containerStats, SetContainerStats] = useState< const [containerStats, setContainerStats] = useState<
(SimpleStats & { name: string })[] (SimpleStats & { name: string })[]
>([]); >([]);
useEffect(() => { // Live nodes polling every 3 seconds
const fetchNodes = async () => { const { value: liveNodes } = useAsyncInterval(
{ cpuUsage: 0, ramUsage: 0 },
async () => {
const nodes = await getLiveNodes(); const nodes = await getLiveNodes();
setLiveData({ return {
cpuUsage: average(nodes, (n) => n.liveData?.cpu.usage || 0), cpuUsage: average(nodes, (n) => n.liveData?.cpu.usage || 0),
ramUsage: average(nodes, (n) => n.liveData?.memory.usage || 0), ramUsage: average(nodes, (n) => n.liveData?.memory.usage || 0),
}); };
}; },
fetchNodes(); 3000,
const interval = setInterval(fetchNodes, 3000); []
return () => clearInterval(interval); );
}, []);
// Update liveData state
useEffect(() => { useEffect(() => {
async function fetchMetrics() { setLiveData(liveNodes);
setData(await getMetrics()); }, [liveNodes]);
}
fetchMetrics(); // Metrics polling every 30 seconds
const { value: metrics } = useAsyncInterval(
const i = setInterval(fetchMetrics, 30000); [] as typeof data,
getMetrics,
return () => clearInterval(i); 30000,
}, []); []
);
// Update data state
useEffect(() => { useEffect(() => {
async function fetchContainerMetrics() { setData(metrics);
}, [metrics]);
// Container stats fetched once
const { value: containers } = useAsync(
[] as typeof containerStats,
async () => {
const nodes = await getNodes(); const nodes = await getNodes();
const dockerNodes = await getDocker(nodes); const dockerNodes = await getDocker(nodes);
SetContainerStats( return (
( await Promise.all(
await Promise.all( dockerNodes.flatMap((node) =>
dockerNodes.flatMap((node) => node.containers.map(async (c) => {
node.containers.map(async (c) => { const stats = await getContainerStats(c.Id);
const stats = await getContainerStats(c.Id); if (!stats) return undefined;
return { ...stats, name: c.Names[0] };
if (stats === undefined) return undefined; })
return { ...stats, name: c.Names[0] };
})
)
) )
).filter((s): s is SimpleStats & { name: string } => s !== undefined) )
); ).filter((s): s is SimpleStats & { name: string } => s !== undefined);
},
[]
);
console.log(containerStats); useEffect(() => {
} setContainerStats(containers);
fetchContainerMetrics(); }, [containers]);
}, []);
return ( return (
<DashboardLayout <DashboardLayout
+9 -18
View File
@@ -14,6 +14,7 @@ import { setNodes as updateNodes } from "@/lib/dockship/node";
import AddNode from "@/components/dialogs/AddNode"; import AddNode from "@/components/dialogs/AddNode";
import { getDocker } from "@/lib/dockship/docker"; import { getDocker } from "@/lib/dockship/docker";
import RemoveDialog from "@/components/dialogs/Remove"; import RemoveDialog from "@/components/dialogs/Remove";
import { useAsync, useAsyncInterval } from "@/hooks/use-async";
function DiskUsage({ liveData }: NodeLiveData) { function DiskUsage({ liveData }: NodeLiveData) {
if (!liveData) return null; if (!liveData) return null;
@@ -33,25 +34,15 @@ function DiskUsage({ liveData }: NodeLiveData) {
} }
export default function NodesPage() { export default function NodesPage() {
const [nodes, setNodes] = useState<(NodeLiveData & Node)[]>([]); const { value: nodes, setValue: setNodes } = useAsyncInterval(
const [dockerNodes, setDockerNodes] = useState<Docker[]>([]); [],
getLiveNodes,
3000
);
useEffect(() => { const { value: dockerNodes } = useAsync([], async () => getDocker(nodes), [
const fetchNodes = async () => { nodes,
const nodes = await getLiveNodes(); ]);
setNodes(nodes);
};
fetchNodes();
const interval = setInterval(fetchNodes, 3000);
return () => clearInterval(interval);
}, []);
useEffect(() => {
const fetchDocker = async () => {
setDockerNodes(await getDocker(nodes));
};
fetchDocker();
}, [nodes]);
return ( return (
<DashboardLayout title="Nodes" subtitle="Cluster node management"> <DashboardLayout title="Nodes" subtitle="Cluster node management">
+17 -24
View File
@@ -29,36 +29,29 @@ import { getDocker } from "@/lib/dockship/docker";
import { getApplications } from "@/lib/dockship/application"; import { getApplications } from "@/lib/dockship/application";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { getDeployments } from "@/lib/dockship/deploy"; import { getDeployments } from "@/lib/dockship/deploy";
import { useAsync, useAsyncInterval } from "@/hooks/use-async";
export default function OverviewPage() { export default function OverviewPage() {
const [nodes, setNodes] = useState<(NodeLiveData & Node)[]>([]); const { value: nodes } = useAsyncInterval([], getLiveNodes, 3000);
const [dockerNodes, setDockerNodes] = useState<Docker[]>([]); const { value: dockerNodes } = useAsync([], async () => getDocker(nodes), [
const [apps, setApplications] = useState<Application[]>([]); nodes,
const [deployments, setDeployments] = useState<Deployment[]>([]); ]);
const { value: applications } = useAsyncInterval([], getApplications, 3000);
useEffect(() => { const { value: deployments } = useAsyncInterval(
const fetchNodes = async () => { [],
const nodes = await getLiveNodes(); async () => (await getDeployments()).filter((d) => d !== null),
setNodes(nodes); 3000
setDockerNodes(await getDocker(nodes)); );
setApplications(await getApplications());
setDeployments((await getDeployments()).filter((d) => d !== null));
};
fetchNodes();
const interval = setInterval(fetchNodes, 3000);
return () => clearInterval(interval);
}, []);
const appNameByContainer = useMemo(() => { const appNameByContainer = useMemo(() => {
const map = new Map<string, string>(); const map = new Map<string, string>();
for (const app of apps) { for (const app of applications) {
for (const container of app.deployments) { for (const container of app.deployments) {
map.set(container, app.name); map.set(container, app.name);
} }
} }
return map; return map;
}, [apps]); }, [applications]);
return ( return (
<DashboardLayout title="Overview" subtitle="System status and metrics"> <DashboardLayout title="Overview" subtitle="System status and metrics">
@@ -66,9 +59,9 @@ export default function OverviewPage() {
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 mb-6"> <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<StatCard <StatCard
title="Applications" title="Applications"
value={apps.length} value={applications.length}
subtitle={`${ subtitle={`${
apps.filter((app) => app.deployments.length > 0).length applications.filter((app) => app.deployments.length > 0).length
} Deployed`} } Deployed`}
icon={<Box className="w-4 h-4" />} icon={<Box className="w-4 h-4" />}
/> />
@@ -118,12 +111,12 @@ export default function OverviewPage() {
</div> </div>
{/* Rows */} {/* Rows */}
{apps.slice(-5).length === 0 ? ( {applications.slice(-5).length === 0 ? (
<div className="py-6 text-sm text-muted-foreground text-center"> <div className="py-6 text-sm text-muted-foreground text-center">
No applications yet No applications yet
</div> </div>
) : ( ) : (
apps.slice(-5).map((app) => ( applications.slice(-5).map((app) => (
<div <div
key={app.id} key={app.id}
className=" className="
+6 -15
View File
@@ -1,6 +1,6 @@
"use client"; "use client";
import { useState, ReactNode, useEffect } from "react"; import { useState, ReactNode } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Dialog, Dialog,
@@ -15,9 +15,9 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { getLiveNodes } from "@/lib/dockship/node"; import { getLiveNodes } from "@/lib/dockship/node";
import { Combobox } from "../ui/combobox"; import { Combobox } from "../ui/combobox";
import { Application, Node, NodeLiveData } from "@/lib/types";
import { getApplications } from "@/core/application"; import { getApplications } from "@/core/application";
import { deployApp } from "@/lib/dockship/deploy"; import { deployApp } from "@/lib/dockship/deploy";
import { useAsync } from "@/hooks/use-async";
export default function DeployAppDialog({ export default function DeployAppDialog({
defaultApp, defaultApp,
@@ -26,21 +26,12 @@ export default function DeployAppDialog({
defaultApp?: string; defaultApp?: string;
children: ReactNode; children: ReactNode;
}) { }) {
const [nodeList, setNodeList] = useState<(NodeLiveData & Node)[]>([]); const { value: nodes } = useAsync([], getLiveNodes);
const [apps, setApps] = useState<Application[]>([]); const { value: applications } = useAsync([], getApplications);
const [name, setName] = useState(""); const [name, setName] = useState("");
const [app, setApp] = useState(defaultApp || ""); const [app, setApp] = useState(defaultApp || "");
const [node, setNode] = useState(""); const [node, setNode] = useState("");
useEffect(() => {
const fetchNodes = async () => {
setNodeList(await getLiveNodes());
setApps(await getApplications());
};
fetchNodes();
}, []);
const deploy = async () => { const deploy = async () => {
console.log(name, app, node); console.log(name, app, node);
await deployApp(name, app, node); await deployApp(name, app, node);
@@ -75,7 +66,7 @@ export default function DeployAppDialog({
<Combobox <Combobox
value={app} value={app}
setValue={setApp} setValue={setApp}
data={apps.map((app) => ({ data={applications.map((app) => ({
label: app.name, label: app.name,
value: app.id, value: app.id,
}))} }))}
@@ -91,7 +82,7 @@ export default function DeployAppDialog({
<Combobox <Combobox
value={node} value={node}
setValue={setNode} setValue={setNode}
data={nodeList data={nodes
.filter((node) => node.liveData) .filter((node) => node.liveData)
.map((node) => ({ label: node.name, value: node.node_id }))} .map((node) => ({ label: node.name, value: node.node_id }))}
placeholder="Select Node..." placeholder="Select Node..."
@@ -17,24 +17,18 @@ import { getLiveNodes } from "@/lib/dockship/node";
import { Combobox } from "../ui/combobox"; import { Combobox } from "../ui/combobox";
import { Node, NodeLiveData } from "@/lib/types"; import { Node, NodeLiveData } from "@/lib/types";
import { installApp } from "@/lib/dockship/application"; import { installApp } from "@/lib/dockship/application";
import { useAsync } from "@/hooks/use-async";
export default function InstallApplicationDialog({ export default function InstallApplicationDialog({
children, children,
}: { }: {
children: ReactNode; children: ReactNode;
}) { }) {
const [nodeList, setNodeList] = useState<(NodeLiveData & Node)[]>([]); const { value: nodes } = useAsync([], getLiveNodes);
const [name, setName] = useState(""); const [name, setName] = useState("");
const [repo, setRepo] = useState(""); const [repo, setRepo] = useState("");
const [node, setNode] = useState(""); const [node, setNode] = useState("");
useEffect(() => {
const fetchNodes = async () => {
setNodeList(await getLiveNodes());
};
fetchNodes();
}, []);
const deploy = async () => { const deploy = async () => {
console.log(await installApp(name, repo, node)); console.log(await installApp(name, repo, node));
}; };
@@ -80,7 +74,7 @@ export default function InstallApplicationDialog({
<Combobox <Combobox
value={node} value={node}
setValue={setNode} setValue={setNode}
data={nodeList data={nodes
.filter((node) => node.liveData) .filter((node) => node.liveData)
.map((node) => ({ label: node.name, value: node.node_id }))} .map((node) => ({ label: node.name, value: node.node_id }))}
placeholder="Select Node..." placeholder="Select Node..."
+68
View File
@@ -0,0 +1,68 @@
import { useState, useEffect } from "react";
export function useAsync<T>(
defaultValue: T,
asyncFunction: () => Promise<T>,
dependencies: any[] = []
) {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<null | Error>(null);
const [value, setValue] = useState<T>(defaultValue);
useEffect(() => {
let isMounted = true;
setLoading(true);
asyncFunction()
.then((result) => {
if (isMounted) setValue(result);
})
.catch((err) => {
if (isMounted) setError(err);
})
.finally(() => {
if (isMounted) setLoading(false);
});
return () => {
isMounted = false;
};
}, dependencies);
return { loading, error, value, setValue };
}
export function useAsyncInterval<T>(
defaultValue: T,
asyncFunction: () => Promise<T>,
interval: number,
dependencies: any[] = []
) {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<null | Error>(null);
const [value, setValue] = useState<T>(defaultValue);
useEffect(() => {
let isMounted = true;
const iid = setInterval(() => {
setLoading(true);
asyncFunction()
.then((result) => {
if (isMounted) setValue(result);
})
.catch((err) => {
if (isMounted) setError(err);
})
.finally(() => {
if (isMounted) setLoading(false);
});
}, interval);
return () => {
isMounted = false;
clearInterval(iid);
};
}, dependencies);
return { loading, error, value, setValue };
}