This commit is contained in:
2026-02-14 14:28:41 +01:00
parent b088f19b18
commit e1e717a54c
9 changed files with 90 additions and 43 deletions
+2 -1
View File
@@ -20,9 +20,10 @@ import { useAsyncInterval } from "@/hooks/use-async";
export default function ApplicationsPage() { export default function ApplicationsPage() {
const { value: apps, setValue: setApps } = useAsyncInterval( const { value: apps, setValue: setApps } = useAsyncInterval(
"apps",
[], [],
getApps, getApps,
3000 3000,
); );
const remove = async (appId: string) => { const remove = async (appId: string) => {
+7 -2
View File
@@ -25,9 +25,14 @@ const statusDot = {
} as const; } as const;
export default function DeploymentsPage() { 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 appNameByContainer = useMemo(() => {
const map = new Map<string, string>(); const map = new Map<string, string>();
+7 -2
View File
@@ -21,11 +21,16 @@ import { useAsync } from "@/hooks/use-async";
export default function EnvironmentPage() { export default function EnvironmentPage() {
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [showSecrets, setShowSecrets] = useState<Record<string, boolean>>({}); const [showSecrets, setShowSecrets] = useState<Record<string, boolean>>({});
const { value: envGroups, setValue: setEnvGroups } = useAsync({}, getEnv); const { value: envGroups, setValue: setEnvGroups } = useAsync(
"env",
{},
getEnv,
);
const { value: selectedApp, setValue: setSelectedApp } = useAsync( const { value: selectedApp, setValue: setSelectedApp } = useAsync(
"selectedApp",
"", "",
async () => Object.keys(envGroups)[0] || "", async () => Object.keys(envGroups)[0] || "",
[envGroups] [envGroups],
); );
const handleSave = async () => { const handleSave = async () => {
+14 -6
View File
@@ -31,10 +31,17 @@ export default function MonitoringPage() {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
// Metrics polling every 30 seconds // Metrics polling every 30 seconds
const { value: metrics } = useAsyncInterval([], getMetrics, 30000, []); const { value: metrics } = useAsyncInterval(
"metrics",
[],
getMetrics,
30000,
[],
);
// Live nodes polling every 3 seconds // Live nodes polling every 3 seconds
const { value: liveData } = useAsyncInterval( const { value: liveData } = useAsyncInterval(
"liveNodes",
{ cpuUsage: 0, ramUsage: 0 }, { cpuUsage: 0, ramUsage: 0 },
async () => { async () => {
const nodes = await getLiveNodes(); const nodes = await getLiveNodes();
@@ -44,11 +51,12 @@ export default function MonitoringPage() {
}; };
}, },
3000, 3000,
[] [],
); );
// Container stats // Container stats
const { value: containerStats } = useAsync( const { value: containerStats } = useAsync(
"containerStats",
[], [],
async () => { async () => {
const nodes = await getNodes(); const nodes = await getNodes();
@@ -61,18 +69,18 @@ export default function MonitoringPage() {
const stats = await getContainerStats(c.Id); const stats = await getContainerStats(c.Id);
if (!stats) return undefined; if (!stats) return undefined;
return { ...stats, name: c.Names[0] }; return { ...stats, name: c.Names[0] };
}) }),
) ),
) )
).filter((s): s is SimpleStats & { name: string } => s !== undefined); ).filter((s): s is SimpleStats & { name: string } => s !== undefined);
}, },
[] [],
); );
const containerCPU = containerStats.reduce((acc, val) => acc + val.cpu, 0); const containerCPU = containerStats.reduce((acc, val) => acc + val.cpu, 0);
const containerMemory = containerStats.reduce( const containerMemory = containerStats.reduce(
(acc, val) => acc + val.memory, (acc, val) => acc + val.memory,
0 0,
); );
return ( return (
+8 -4
View File
@@ -35,14 +35,18 @@ function DiskUsage({ liveData }: NodeLiveData) {
export default function NodesPage() { export default function NodesPage() {
const { value: nodes, setValue: setNodes } = useAsyncInterval( const { value: nodes, setValue: setNodes } = useAsyncInterval(
"liveNodes",
[], [],
getLiveNodes, getLiveNodes,
3000 3000,
); );
const { value: dockerNodes } = useAsync([], async () => getDocker(nodes), [ const { value: dockerNodes } = useAsync(
nodes, "docker",
]); [],
async () => getDocker(nodes),
[nodes],
);
return ( return (
<DashboardLayout title="Nodes" subtitle="Cluster node management"> <DashboardLayout title="Nodes" subtitle="Cluster node management">
+14 -5
View File
@@ -17,12 +17,21 @@ import { getDeployments } from "@/lib/dockship/deploy";
import { useAsync, useAsyncInterval } from "@/hooks/use-async"; import { useAsync, useAsyncInterval } from "@/hooks/use-async";
export default function OverviewPage() { export default function OverviewPage() {
const { value: nodes } = useAsyncInterval([], getLiveNodes, 3000); const { value: nodes } = useAsyncInterval(
const { value: dockerNodes } = useAsync([], async () => getDocker(nodes), [ "liveNodes",
nodes, [],
]); getLiveNodes,
const { value: apps } = useAsyncInterval([], getApps, 3000); 3000,
);
const { value: dockerNodes } = useAsync(
"docker",
[],
async () => getDocker(nodes),
[nodes],
);
const { value: apps } = useAsyncInterval("apps", [], getApps, 3000);
const { value: deployments } = useAsyncInterval( const { value: deployments } = useAsyncInterval(
"deployments",
[], [],
async () => (await getDeployments()).filter((d) => d !== null), async () => (await getDeployments()).filter((d) => d !== null),
3000, 3000,
+2 -2
View File
@@ -28,8 +28,8 @@ export default function DeployAppDialog({
defaultApp?: string; defaultApp?: string;
children: ReactNode; children: ReactNode;
}) { }) {
const { value: nodes } = useAsync([], getLiveNodes); const { value: nodes } = useAsync("liveNodes", [], getLiveNodes);
const { value: apps } = useAsync<Application[]>([], getApps); const { value: apps } = useAsync<Application[]>("apps", [], getApps);
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("");
-3
View File
@@ -13,10 +13,7 @@ import {
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input"; 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 { Combobox } from "../ui/combobox";
import { registerApp } from "@/lib/dockship/application"; import { registerApp } from "@/lib/dockship/application";
import { useAsync } from "@/hooks/use-async";
export default function RegisterAppDialog({ export default function RegisterAppDialog({
children, children,
+35 -17
View File
@@ -1,20 +1,31 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
export function useAsync<T>( export function useAsync<T>(
cacheKey: string,
defaultValue: T, defaultValue: T,
asyncFunction: () => Promise<T>, asyncFunction: () => Promise<T>,
dependencies: any[] = [] dependencies: any[] = [],
) { ) {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<null | Error>(null); const [error, setError] = useState<null | Error>(null);
const [value, setValue] = useState<T>(defaultValue); const [value, setValue] = useState<T>(() => {
try {
const cached = localStorage.getItem(cacheKey);
return cached ? (JSON.parse(cached) as T) : defaultValue;
} catch {
return defaultValue;
}
});
useEffect(() => { useEffect(() => {
let isMounted = true; let isMounted = true;
setLoading(true); setLoading(true);
asyncFunction() asyncFunction()
.then((result) => { .then((result) => {
if (isMounted) setValue(result); if (!isMounted) return;
setValue(result);
localStorage.setItem(cacheKey, JSON.stringify(result));
}) })
.catch((err) => { .catch((err) => {
if (isMounted) setError(err); if (isMounted) setError(err);
@@ -32,35 +43,42 @@ export function useAsync<T>(
} }
export function useAsyncInterval<T>( export function useAsyncInterval<T>(
cacheKey: string,
defaultValue: T, defaultValue: T,
asyncFunction: () => Promise<T>, asyncFunction: () => Promise<T>,
interval: number, interval: number,
dependencies: any[] = [] dependencies: any[] = [],
) { ) {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<null | Error>(null); const [error, setError] = useState<null | Error>(null);
const [value, setValue] = useState<T>(defaultValue); const [value, setValue] = useState<T>(() => {
try {
const cached = localStorage.getItem(cacheKey);
return cached ? (JSON.parse(cached) as T) : defaultValue;
} catch {
return defaultValue;
}
});
useEffect(() => { useEffect(() => {
let isMounted = true; let isMounted = true;
const run = () => { const run = async () => {
setLoading(true); setLoading(true);
asyncFunction() try {
.then((result) => { const result = await asyncFunction();
if (isMounted) setValue(result); if (!isMounted) return;
}) setValue(result);
.catch((err) => { localStorage.setItem(cacheKey, JSON.stringify(result));
if (isMounted) setError(err); } catch (err) {
}) if (isMounted) setError(err as Error);
.finally(() => { } finally {
if (isMounted) setLoading(false); if (isMounted) setLoading(false);
}); }
}; };
const iid = setInterval(run, interval);
run(); run();
const iid = setInterval(run, interval);
return () => { return () => {
isMounted = false; isMounted = false;