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