Implemented useAsync in the app structure for cleaner code
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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="
|
||||
|
||||
@@ -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..."
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
Reference in New Issue
Block a user