Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,26 @@
import { formatMb } from "@dokploy/server/monitoring/units";
import { useEffect, useState } from "react";
import { FolderKanban } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { api } from "@/utils/api";
import { DockerBlockChart } from "./docker-block-chart";
import { DockerCpuChart } from "./docker-cpu-chart";
Expand All @@ -10,6 +29,8 @@ import { DockerDiskUsageChart } from "./docker-disk-usage-chart";
import { DockerMemoryChart } from "./docker-memory-chart";
import { DockerNetworkChart } from "./docker-network-chart";

const ALL_SERVER = "all";

const defaultData = {
cpu: {
value: "0%",
Expand Down Expand Up @@ -53,8 +74,8 @@ export interface DockerStats {
};
memory: {
value: {
used: number;
total: number;
used: number | string;
total: number | string;
};
time: string;
};
Expand Down Expand Up @@ -116,39 +137,61 @@ export const convertMemoryToBytes = (
}
};

const resetAccumulativeData = (): DockerStatsJSON => ({
cpu: [],
memory: [],
block: [],
network: [],
disk: [],
});

export const ContainerFreeMonitoring = ({
appName,
appType = "application",
}: Props) => {
const isServerView = appName === "dokploy";
const [selectedProjectId, setSelectedProjectId] = useState(ALL_SERVER);
const isProjectView = isServerView && selectedProjectId !== ALL_SERVER;

const { data: projects } = api.project.all.useQuery(undefined, {
enabled: isServerView,
});

const { data } = api.application.readAppMonitoring.useQuery(
{ appName },
{
refetchOnWindowFocus: false,
enabled: !isProjectView,
},
);
const [accumulativeData, setAccumulativeData] = useState<DockerStatsJSON>({
cpu: [],
memory: [],
block: [],
network: [],
disk: [],
});

const { data: projectStats } = api.project.resourceStats.useQuery(
{ projectId: selectedProjectId },
{
enabled: isProjectView,
refetchInterval: 2000,
refetchOnWindowFocus: false,
},
);

const [accumulativeData, setAccumulativeData] = useState<DockerStatsJSON>(
resetAccumulativeData,
);
const [currentData, setCurrentData] = useState<DockerStats>(defaultData);
const lastProjectSampleRef = useRef<string>("");

const selectedProjectName =
projects?.find((project) => project.projectId === selectedProjectId)
?.name ?? projectStats?.projectName;

useEffect(() => {
setCurrentData(defaultData);

setAccumulativeData({
cpu: [],
memory: [],
block: [],
network: [],
disk: [],
});
}, [appName]);
setAccumulativeData(resetAccumulativeData());
lastProjectSampleRef.current = "";
}, [appName, selectedProjectId]);

useEffect(() => {
if (!data) return;
if (isProjectView || !data) return;

setCurrentData({
cpu: data.cpu[data.cpu.length - 1] ?? currentData.cpu,
Expand All @@ -164,9 +207,38 @@ export const ContainerFreeMonitoring = ({
memory: data?.memory || [],
network: data?.network || [],
});
}, [data]);
}, [data, isProjectView]);

useEffect(() => {
if (!isProjectView || !projectStats?.aggregated) return;

const sampleTime = projectStats.aggregated.cpu.time;
if (lastProjectSampleRef.current === sampleTime) return;
lastProjectSampleRef.current = sampleTime;

const nextData: DockerStats = {
cpu: projectStats.aggregated.cpu,
memory: projectStats.aggregated.memory,
block: projectStats.aggregated.block,
network: projectStats.aggregated.network,
disk: defaultData.disk,
};

setCurrentData(nextData);

const MAX_DATA_POINTS = 300;
setAccumulativeData((prevData) => ({
cpu: [...prevData.cpu, nextData.cpu].slice(-MAX_DATA_POINTS),
memory: [...prevData.memory, nextData.memory].slice(-MAX_DATA_POINTS),
block: [...prevData.block, nextData.block].slice(-MAX_DATA_POINTS),
network: [...prevData.network, nextData.network].slice(-MAX_DATA_POINTS),
disk: prevData.disk,
}));
}, [projectStats, isProjectView]);

useEffect(() => {
if (isProjectView) return;

const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const wsUrl = `${protocol}//${window.location.host}/listen-docker-stats-monitoring?appName=${appName}&appType=${appType}`;
const ws = new WebSocket(wsUrl);
Expand All @@ -175,23 +247,25 @@ export const ContainerFreeMonitoring = ({
const value = JSON.parse(e.data);
if (!value) return;

const data = {
const nextData = {
cpu: value.data.cpu ?? currentData.cpu,
memory: value.data.memory ?? currentData.memory,
block: value.data.block ?? currentData.block,
disk: value.data.disk ?? currentData.disk,
network: value.data.network ?? currentData.network,
};

setCurrentData(data);
setCurrentData(nextData);

const MAX_DATA_POINTS = 300;
setAccumulativeData((prevData) => ({
cpu: [...prevData.cpu, data.cpu].slice(-MAX_DATA_POINTS),
memory: [...prevData.memory, data.memory].slice(-MAX_DATA_POINTS),
block: [...prevData.block, data.block].slice(-MAX_DATA_POINTS),
network: [...prevData.network, data.network].slice(-MAX_DATA_POINTS),
disk: [...prevData.disk, data.disk].slice(-MAX_DATA_POINTS),
cpu: [...prevData.cpu, nextData.cpu].slice(-MAX_DATA_POINTS),
memory: [...prevData.memory, nextData.memory].slice(-MAX_DATA_POINTS),
block: [...prevData.block, nextData.block].slice(-MAX_DATA_POINTS),
network: [...prevData.network, nextData.network].slice(
-MAX_DATA_POINTS,
),
disk: [...prevData.disk, nextData.disk].slice(-MAX_DATA_POINTS),
}));
};

Expand All @@ -200,17 +274,55 @@ export const ContainerFreeMonitoring = ({
};

return () => ws.close();
}, [appName]);
}, [appName, appType, isProjectView]);

const memoryUsedLabel = String(currentData.memory.value.used ?? "0");
const memoryTotalLabel = String(currentData.memory.value.total ?? "0");
const memoryProgress =
(convertMemoryToBytes(memoryUsedLabel) /
(convertMemoryToBytes(memoryTotalLabel) || 1)) *
100;

return (
<div className="rounded-xl bg-background flex flex-col gap-4">
<header className="flex items-center justify-between">
<header className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div className="space-y-1">
<h1 className="text-2xl font-semibold tracking-tight">Monitoring</h1>
<p className="text-sm text-muted-foreground">
Watch the usage of your server in the current app
{isProjectView
? `Resource usage for ${selectedProjectName || "selected project"}`
: "Watch the usage of your server in the current app"}
</p>
</div>
{isServerView && (
<div className="w-full sm:w-[260px]">
<Select
value={selectedProjectId}
onValueChange={setSelectedProjectId}
>
<SelectTrigger>
<div className="flex items-center gap-2 truncate">
<FolderKanban className="size-4 shrink-0 text-muted-foreground" />
<SelectValue placeholder="Filter by project" />
</div>
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Scope</SelectLabel>
<SelectItem value={ALL_SERVER}>All Server</SelectItem>
{projects?.map((project) => (
<SelectItem
key={project.projectId}
value={project.projectId}
>
{project.name}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</div>
)}
</header>

<div className="grid gap-6 lg:grid-cols-2">
Expand All @@ -224,9 +336,11 @@ export const ContainerFreeMonitoring = ({
Used: {String(currentData.cpu.value ?? "0%")}
</span>
<Progress
value={Number.parseInt(
String(currentData.cpu.value ?? "0%").replace("%", ""),
10,
value={Math.min(
Number.parseFloat(
String(currentData.cpu.value ?? "0%").replace("%", ""),
) || 0,
100,
)}
className="w-full"
/>
Expand All @@ -241,30 +355,19 @@ export const ContainerFreeMonitoring = ({
<CardContent>
<div className="flex flex-col gap-2 w-full">
<span className="text-sm text-muted-foreground">
{`Used: ${currentData.memory.value.used} / Limit: ${currentData.memory.value.total} `}
{`Used: ${memoryUsedLabel} / Limit: ${memoryTotalLabel} `}
</span>
<Progress
value={
// @ts-ignore
(convertMemoryToBytes(currentData.memory.value.used) /
// @ts-ignore
convertMemoryToBytes(currentData.memory.value.total)) *
100
}
className="w-full"
/>
<Progress value={memoryProgress} className="w-full" />
<DockerMemoryChart
accumulativeData={accumulativeData.memory}
memoryLimitGB={
// @ts-ignore
convertMemoryToBytes(currentData.memory.value.total) /
1024 ** 3
convertMemoryToBytes(memoryTotalLabel) / 1024 ** 3
}
/>
</div>
</CardContent>
</Card>
{appName === "dokploy" && (
{!isProjectView && appName === "dokploy" && (
<Card className="bg-background">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Disk Space</CardTitle>
Expand All @@ -286,7 +389,7 @@ export const ContainerFreeMonitoring = ({
</CardContent>
</Card>
)}
{appName === "dokploy" && (
{!isProjectView && appName === "dokploy" && (
<Card className="bg-background">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Expand Down Expand Up @@ -326,6 +429,64 @@ export const ContainerFreeMonitoring = ({
</CardContent>
</Card>
</div>

{isProjectView && (
<Card className="bg-background">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<div className="space-y-1">
<CardTitle className="text-sm font-medium">
Services breakdown
</CardTitle>
<p className="text-xs text-muted-foreground">
Live CPU and memory usage by service in this project
</p>
</div>
</CardHeader>
<CardContent>
{!projectStats?.services?.length ? (
<p className="text-sm text-muted-foreground py-6 text-center">
No services found in this project.
</p>
) : (
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Service</TableHead>
<TableHead>Type</TableHead>
<TableHead>Containers</TableHead>
<TableHead>CPU</TableHead>
<TableHead>Memory</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{projectStats.services.map((service) => (
<TableRow key={service.id}>
<TableCell className="font-medium">
{service.name}
</TableCell>
<TableCell>
<Badge variant="outline" className="capitalize">
{service.type}
</Badge>
</TableCell>
<TableCell>{service.containerCount}</TableCell>
<TableCell>{service.cpuPerc.toFixed(2)}%</TableCell>
<TableCell>
{service.memUsed}
{service.containerCount > 0
? ` / ${service.memLimit}`
: ""}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
</CardContent>
</Card>
)}
</div>
);
};
Loading