Feat/monitoring (#1267) Cloud Version

* feat: add start monitoring remote servers

* reafctor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor:

* refactor: add metrics

* feat: add disk monitoring

* refactor: translate to english

* refacotor: add stats

* refactor: remove color

* feat: add log server metrics

* refactor: remove unused deps

* refactor: add origin

* refactor: add logs

* refactor: update

* feat: add series monitoring

* refactor: add system monitoring

* feat: add benchmark to optimize data

* refactor: update fn

* refactor: remove comments

* refactor: update

* refactor: exclude items

* feat: add refresh rate

* feat: add monitoring remote servers

* refactor: update

* refactor: remove unsued volumes

* refactor: update monitoring

* refactor: add more presets

* feat: add container metrics

* feat: add docker monitoring

* refactor: update conversion

* refactor: remove unused code

* refactor: update

* refactor: add docker compose logs

* refactor: add docker cli

* refactor: add install curl

* refactor: add get update

* refactor: add monitoring remote servers

* refactor: add containers config

* feat: add container specification

* refactor: update path

* refactor: add server filter

* refactor: simplify logic

* fix: verify if file exist before get stats

* refactor: update

* refactor: remove unused deps

* test: add test for containers

* refactor: update

* refactor add memory collector

* refactor: update

* refactor: update

* refactor: update

* refactor: remove

* refactor: add memory

* refactor: add server memory usage

* refactor: change memory

* refactor: update

* refactor: update

* refactor: add container metrics

* refactor: comment code

* refactor: mount proc bind

* refactor: change interval with node cron

* refactor: remove opening file

* refactor: use streams

* refactor: remove unused ws

* refactor: disable live when is all

* refactor: add sqlite

* refactor: update

* feat: add golang benchmark

* refactor: update go

* refactor: update dockerfile

* refactor: update db

* refactor: add env

* refactor: separate logic

* refactor: split logic

* refactor: update logs

* refactor: update dockerfile

* refactor: hide .env

* refactor: update

* chore: hide ,.ebnv

* refactor: add end angle

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update

* refactor: update monitoring

* refactor: add mount db

* refactor: add metrics and url callback

* refactor: add middleware

* refactor: add threshold property

* feat: add memory and cpu threshold notification

* feat: send notifications to the server

* feat: add metrics for dokploy server

* refactor: add dokploy server to monitoring

* refactor: update methods

* refactor: add admin to useeffect

* refactor: stop monitoring containers if elements are 0

* refactor: cancel request if appName is empty

* refactor: reuse methods

* chore; add feat monitoring

* refactor: set base url

* refactor: adjust monitoring

* refactor: delete migrations

* feat: add columns

* fix: add missing flag

* refactor: add free metrics

* refactor: add paid monitoring

* refactor: update methods

* feat: improve ui

* feat: add container stats

* refactor: add all container metrics

* refactor: add color primary

* refactor: change default rate limiting refresher

* refactor: update retention days

* refactor: use json instead of individual properties

* refactor: lint

* refactor: pass json env

* refactor: update

* refactor: delete

* refactor: update

* refactor: fix types

* refactor: add retention days

* chore: add license

* refactor: create db

* refactor: update path

* refactor: update setup

* refactor: update

* refactor: create files

* refactor: update

* refactor: delete

* refactor: update

* refactor: update token metrics

* fix: typechecks

* refactor: setup web server

* refactor: update error handling and add monitoring

* refactor: add local storage save

* refactor: add spacing

* refactor: update

* refactor: upgrade drizzle

* refactor: delete

* refactor: uppgrade drizzle kit

* refactor: update search with jsonB

* chore: upgrade drizzle

* chore: update packages

* refactor: add missing type

* refactor: add serverType

* refactor: update url

* refactor: update

* refactor: update

* refactor: hide monitoring on self hosted

* refactor: update server

* refactor: update

* refactor: update

* refactor: pin node version
This commit is contained in:
Mauricio Siu
2025-02-02 14:08:06 -06:00
committed by GitHub
parent 8c69d2a085
commit 74a0f5e992
150 changed files with 36173 additions and 11538 deletions

View File

@@ -8,7 +8,7 @@ import {
Tooltip,
YAxis,
} from "recharts";
import type { DockerStatsJSON } from "./show";
import type { DockerStatsJSON } from "./show-free-container-monitoring";
interface Props {
acummulativeData: DockerStatsJSON["block"];

View File

@@ -8,7 +8,7 @@ import {
Tooltip,
YAxis,
} from "recharts";
import type { DockerStatsJSON } from "./show";
import type { DockerStatsJSON } from "./show-free-container-monitoring";
interface Props {
acummulativeData: DockerStatsJSON["cpu"];

View File

@@ -8,7 +8,7 @@ import {
Tooltip,
YAxis,
} from "recharts";
import type { DockerStatsJSON } from "./show";
import type { DockerStatsJSON } from "./show-free-container-monitoring";
interface Props {
acummulativeData: DockerStatsJSON["disk"];

View File

@@ -8,9 +8,8 @@ import {
Tooltip,
YAxis,
} from "recharts";
import type { DockerStatsJSON } from "./show";
import { convertMemoryToBytes } from "./show";
import type { DockerStatsJSON } from "./show-free-container-monitoring";
import { convertMemoryToBytes } from "./show-free-container-monitoring";
interface Props {
acummulativeData: DockerStatsJSON["memory"];
memoryLimitGB: number;

View File

@@ -8,8 +8,7 @@ import {
Tooltip,
YAxis,
} from "recharts";
import type { DockerStatsJSON } from "./show";
1;
import type { DockerStatsJSON } from "./show-free-container-monitoring";
interface Props {
acummulativeData: DockerStatsJSON["network"];
}

View File

@@ -0,0 +1,126 @@
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { api } from "@/utils/api";
import { Loader2 } from "lucide-react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { ContainerFreeMonitoring } from "./show-free-container-monitoring";
interface Props {
appName: string;
serverId?: string;
appType: "stack" | "docker-compose";
}
export const ComposeFreeMonitoring = ({
appName,
appType = "stack",
serverId,
}: Props) => {
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
{
appName: appName,
appType,
serverId,
},
{
enabled: !!appName,
},
);
const [containerAppName, setContainerAppName] = useState<
string | undefined
>();
const [containerId, setContainerId] = useState<string | undefined>();
const { mutateAsync: restart, isLoading: isRestarting } =
api.docker.restartContainer.useMutation();
useEffect(() => {
if (data && data?.length > 0) {
setContainerAppName(data[0]?.name);
setContainerId(data[0]?.containerId);
}
}, [data]);
return (
<>
<CardHeader>
<CardTitle className="text-xl">Monitoring</CardTitle>
<CardDescription>Watch the usage of your compose</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<Label>Select a container to watch the monitoring</Label>
<div className="flex flex-row gap-4">
<Select
onValueChange={(value) => {
setContainerAppName(value);
setContainerId(
data?.find((container) => container.name === value)
?.containerId,
);
}}
value={containerAppName}
>
<SelectTrigger>
{isLoading ? (
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground">
<span>Loading...</span>
<Loader2 className="animate-spin size-4" />
</div>
) : (
<SelectValue placeholder="Select a container" />
)}
</SelectTrigger>
<SelectContent>
<SelectGroup>
{data?.map((container) => (
<SelectItem
key={container.containerId}
value={container.name}
>
{container.name} ({container.containerId}) {container.state}
</SelectItem>
))}
<SelectLabel>Containers ({data?.length})</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
<Button
isLoading={isRestarting}
onClick={async () => {
if (!containerId) return;
toast.success(`Restarting container ${containerAppName}`);
await restart({ containerId }).then(() => {
toast.success("Container restarted");
});
}}
>
Restart
</Button>
</div>
<ContainerFreeMonitoring
appName={containerAppName || ""}
appType={appType}
/>
</CardContent>
</>
);
};

View File

@@ -120,7 +120,7 @@ export const convertMemoryToBytes = (
}
};
export const DockerMonitoring = ({
export const ContainerFreeMonitoring = ({
appName,
appType = "application",
}: Props) => {
@@ -206,128 +206,111 @@ export const DockerMonitoring = ({
}, [appName]);
return (
<div>
<Card className="h-full bg-sidebar p-2.5 rounded-xl mx-auto w-full">
<div className="rounded-xl bg-background shadow-md p-6 flex flex-col gap-4">
<header className="flex items-center 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
</p>
</div>
</header>
<div className="grid gap-6 lg:grid-cols-2">
<Card className="bg-background">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">CPU Usage</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-col gap-2 w-full">
<span className="text-sm text-muted-foreground">
Used: {currentData.cpu.value}%
</span>
<Progress
value={currentData.cpu.value}
className="w-[100%]"
/>
<DockerCpuChart acummulativeData={acummulativeData.cpu} />
</div>
</CardContent>
</Card>
<Card className="bg-background">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Memory Usage
</CardTitle>
</CardHeader>
<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} `}
</span>
<Progress
value={
// @ts-ignore
(convertMemoryToBytes(currentData.memory.value.used) /
// @ts-ignore
convertMemoryToBytes(currentData.memory.value.total)) *
100
}
className="w-[100%]"
/>
<DockerMemoryChart
acummulativeData={acummulativeData.memory}
memoryLimitGB={
// @ts-ignore
convertMemoryToBytes(currentData.memory.value.total) /
1024 ** 3
}
/>
</div>
</CardContent>
</Card>
{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>
</CardHeader>
<CardContent>
<div className="flex flex-col gap-2 w-full">
<span className="text-sm text-muted-foreground">
{`Used: ${currentData.disk.value.diskUsage} GB / Limit: ${currentData.disk.value.diskTotal} GB`}
</span>
<Progress
value={currentData.disk.value.diskUsedPercentage}
className="w-[100%]"
/>
<DockerDiskChart
acummulativeData={acummulativeData.disk}
diskTotal={currentData.disk.value.diskTotal}
/>
</div>
</CardContent>
</Card>
)}
<Card className="bg-background">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Block I/O</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-col gap-2 w-full">
<span className="text-sm text-muted-foreground">
{`Read: ${currentData.block.value.readMb} / Write: ${currentData.block.value.writeMb} `}
</span>
<DockerBlockChart acummulativeData={acummulativeData.block} />
</div>
</CardContent>
</Card>
<Card className="bg-background">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Network I/O
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-col gap-2 w-full">
<span className="text-sm text-muted-foreground">
{`In MB: ${currentData.network.value.inputMb} / Out MB: ${currentData.network.value.outputMb} `}
</span>
<DockerNetworkChart
acummulativeData={acummulativeData.network}
/>
</div>
</CardContent>
</Card>
</div>
<div className="rounded-xl bg-background shadow-md flex flex-col gap-4">
<header className="flex items-center 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
</p>
</div>
</Card>
</header>
<div className="grid gap-6 lg:grid-cols-2">
<Card className="bg-background">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">CPU Usage</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-col gap-2 w-full">
<span className="text-sm text-muted-foreground">
Used: {currentData.cpu.value}%
</span>
<Progress value={currentData.cpu.value} className="w-[100%]" />
<DockerCpuChart acummulativeData={acummulativeData.cpu} />
</div>
</CardContent>
</Card>
<Card className="bg-background">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Memory Usage</CardTitle>
</CardHeader>
<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} `}
</span>
<Progress
value={
// @ts-ignore
(convertMemoryToBytes(currentData.memory.value.used) /
// @ts-ignore
convertMemoryToBytes(currentData.memory.value.total)) *
100
}
className="w-[100%]"
/>
<DockerMemoryChart
acummulativeData={acummulativeData.memory}
memoryLimitGB={
// @ts-ignore
convertMemoryToBytes(currentData.memory.value.total) /
1024 ** 3
}
/>
</div>
</CardContent>
</Card>
{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>
</CardHeader>
<CardContent>
<div className="flex flex-col gap-2 w-full">
<span className="text-sm text-muted-foreground">
{`Used: ${currentData.disk.value.diskUsage} GB / Limit: ${currentData.disk.value.diskTotal} GB`}
</span>
<Progress
value={currentData.disk.value.diskUsedPercentage}
className="w-[100%]"
/>
<DockerDiskChart
acummulativeData={acummulativeData.disk}
diskTotal={currentData.disk.value.diskTotal}
/>
</div>
</CardContent>
</Card>
)}
<Card className="bg-background">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Block I/O</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-col gap-2 w-full">
<span className="text-sm text-muted-foreground">
{`Read: ${currentData.block.value.readMb} / Write: ${currentData.block.value.writeMb} `}
</span>
<DockerBlockChart acummulativeData={acummulativeData.block} />
</div>
</CardContent>
</Card>
<Card className="bg-background">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Network I/O</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-col gap-2 w-full">
<span className="text-sm text-muted-foreground">
{`In MB: ${currentData.network.value.inputMb} / Out MB: ${currentData.network.value.outputMb} `}
</span>
<DockerNetworkChart acummulativeData={acummulativeData.network} />
</div>
</CardContent>
</Card>
</div>
</div>
);
};

View File

@@ -0,0 +1,189 @@
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
type ChartConfig,
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
} from "@/components/ui/chart";
import { formatTimestamp } from "@/lib/utils";
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
interface ContainerMetric {
timestamp: string;
BlockIO: {
read: number;
write: number;
readUnit: string;
writeUnit: string;
};
}
interface Props {
data: ContainerMetric[];
}
interface FormattedMetric {
timestamp: string;
read: number;
write: number;
readUnit: string;
writeUnit: string;
}
const chartConfig = {
read: {
label: "Read",
color: "hsl(217, 91%, 60%)", // Azul brillante
},
write: {
label: "Write",
color: "hsl(142, 71%, 45%)", // Verde brillante
},
} satisfies ChartConfig;
export const ContainerBlockChart = ({ data }: Props) => {
const formattedData = data.map((metric) => ({
timestamp: metric.timestamp,
read: metric.BlockIO.read,
write: metric.BlockIO.write,
readUnit: metric.BlockIO.readUnit,
writeUnit: metric.BlockIO.writeUnit,
}));
const latestData = formattedData[formattedData.length - 1] || {
timestamp: "",
read: 0,
write: 0,
readUnit: "B",
writeUnit: "B",
};
return (
<Card className="bg-transparent">
<CardHeader className="border-b py-5">
<CardTitle>Block I/O</CardTitle>
<CardDescription>
Read: {latestData.read}
{latestData.readUnit} / Write: {latestData.write}
{latestData.writeUnit}
</CardDescription>
</CardHeader>
<CardContent className="px-2 pt-4 sm:px-6 sm:pt-6">
<ChartContainer
config={chartConfig}
className="aspect-auto h-[250px] w-full"
>
<AreaChart data={formattedData}>
<defs>
<linearGradient id="fillRead" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="hsl(217, 91%, 60%)"
stopOpacity={0.3}
/>
<stop
offset="95%"
stopColor="hsl(217, 91%, 60%)"
stopOpacity={0.1}
/>
</linearGradient>
<linearGradient id="fillWrite" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="hsl(142, 71%, 45%)"
stopOpacity={0.3}
/>
<stop
offset="95%"
stopColor="hsl(142, 71%, 45%)"
stopOpacity={0.1}
/>
</linearGradient>
</defs>
<CartesianGrid vertical={false} />
<XAxis
dataKey="timestamp"
tickLine={false}
axisLine={false}
tickMargin={8}
minTickGap={32}
tickFormatter={(value) => formatTimestamp(value)}
/>
<YAxis />
<ChartTooltip
cursor={false}
content={({ active, payload, label }) => {
if (active && payload && payload.length) {
const data = payload?.[0]?.payload;
return (
<div className="rounded-lg border bg-background p-2 shadow-sm">
<div className="grid grid-cols-2 gap-2">
<div className="flex flex-col">
<span className="text-[0.70rem] uppercase text-muted-foreground">
Time
</span>
<span className="font-bold">
{formatTimestamp(label)}
</span>
</div>
<div className="flex flex-col">
<span className="text-[0.70rem] uppercase text-muted-foreground">
Read
</span>
<span className="font-bold">
{data.read}
{data.readUnit}
</span>
</div>
<div className="flex flex-col">
<span className="text-[0.70rem] uppercase text-muted-foreground">
Write
</span>
<span className="font-bold">
{data.write}
{data.writeUnit}
</span>
</div>
</div>
</div>
);
}
return null;
}}
/>
<Area
name="Write"
dataKey="write"
type="monotone"
fill="url(#fillWrite)"
stroke="hsl(142, 71%, 45%)"
strokeWidth={2}
fillOpacity={0.3}
/>
<Area
name="Read"
dataKey="read"
type="monotone"
fill="url(#fillRead)"
stroke="hsl(217, 91%, 60%)"
strokeWidth={2}
fillOpacity={0.3}
/>
<ChartLegend
content={<ChartLegendContent />}
verticalAlign="bottom"
align="center"
/>
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
);
};

View File

@@ -0,0 +1,128 @@
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
type ChartConfig,
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
} from "@/components/ui/chart";
import { formatTimestamp } from "@/lib/utils";
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
interface ContainerMetric {
timestamp: string;
CPU: number;
}
interface Props {
data: ContainerMetric[];
}
const chartConfig = {
cpu: {
label: "CPU",
color: "hsl(var(--chart-1))",
},
} satisfies ChartConfig;
export const ContainerCPUChart = ({ data }: Props) => {
const formattedData = data.map((metric) => ({
timestamp: metric.timestamp,
cpu: metric.CPU,
}));
const latestData = formattedData[formattedData.length - 1] || {
timestamp: "",
cpu: 0,
};
return (
<Card className="bg-transparent">
<CardHeader className="border-b py-5">
<CardTitle>CPU</CardTitle>
<CardDescription>CPU Usage: {latestData.cpu}%</CardDescription>
</CardHeader>
<CardContent className="px-2 pt-4 sm:px-6 sm:pt-6">
<ChartContainer
config={chartConfig}
className="aspect-auto h-[250px] w-full"
>
<AreaChart data={formattedData}>
<defs>
<linearGradient id="fillCPU" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="hsl(var(--chart-1))"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="hsl(var(--chart-1))"
stopOpacity={0.1}
/>
</linearGradient>
</defs>
<CartesianGrid vertical={false} />
<XAxis
dataKey="timestamp"
tickLine={false}
axisLine={false}
tickMargin={8}
minTickGap={32}
tickFormatter={(value) => formatTimestamp(value)}
/>
<YAxis tickFormatter={(value) => `${value}%`} domain={[0, 100]} />
<ChartTooltip
cursor={false}
content={({ active, payload, label }) => {
if (active && payload && payload.length) {
const data = payload?.[0]?.payload;
return (
<div className="rounded-lg border bg-background p-2 shadow-sm">
<div className="grid grid-cols-2 gap-2">
<div className="flex flex-col">
<span className="text-[0.70rem] uppercase text-muted-foreground">
Time
</span>
<span className="font-bold">
{formatTimestamp(label)}
</span>
</div>
<div className="flex flex-col">
<span className="text-[0.70rem] uppercase text-muted-foreground">
CPU
</span>
<span className="font-bold">{data.cpu}%</span>
</div>
</div>
</div>
);
}
return null;
}}
/>
<Area
name="CPU"
dataKey="cpu"
type="monotone"
fill="url(#fillCPU)"
stroke="hsl(var(--chart-1))"
strokeWidth={2}
/>
<ChartLegend
content={<ChartLegendContent />}
verticalAlign="bottom"
align="center"
/>
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
);
};

View File

@@ -0,0 +1,149 @@
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
type ChartConfig,
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
} from "@/components/ui/chart";
import { formatTimestamp } from "@/lib/utils";
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
interface ContainerMetric {
timestamp: string;
Memory: {
percentage: number;
used: number;
total: number;
usedUnit: string;
totalUnit: string;
};
}
interface Props {
data: ContainerMetric[];
}
const chartConfig = {
memory: {
label: "Memory",
color: "hsl(var(--chart-2))",
},
} satisfies ChartConfig;
const formatMemoryValue = (value: number) => {
return value.toLocaleString("en-US", {
minimumFractionDigits: 1,
maximumFractionDigits: 2,
});
};
export const ContainerMemoryChart = ({ data }: Props) => {
const formattedData = data.map((metric) => ({
timestamp: metric.timestamp,
memory: metric.Memory.percentage,
usage: `${formatMemoryValue(metric.Memory.used)}${metric.Memory.usedUnit} / ${formatMemoryValue(metric.Memory.total)}${metric.Memory.totalUnit}`,
}));
const latestData = formattedData[formattedData.length - 1] || {
timestamp: "",
memory: 0,
usage: "0 / 0 B",
};
return (
<Card className="bg-transparent">
<CardHeader className="border-b py-5">
<CardTitle>Memory</CardTitle>
<CardDescription>Memory Usage: {latestData.usage}</CardDescription>
</CardHeader>
<CardContent className="px-2 pt-4 sm:px-6 sm:pt-6">
<ChartContainer
config={chartConfig}
className="aspect-auto h-[250px] w-full"
>
<AreaChart data={formattedData}>
<defs>
<linearGradient id="fillMemory" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="hsl(var(--chart-2))"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="hsl(var(--chart-2))"
stopOpacity={0.1}
/>
</linearGradient>
</defs>
<CartesianGrid vertical={false} />
<XAxis
dataKey="timestamp"
tickLine={false}
axisLine={false}
tickMargin={8}
minTickGap={32}
tickFormatter={(value) => formatTimestamp(value)}
/>
<YAxis tickFormatter={(value) => `${value}%`} domain={[0, 100]} />
<ChartTooltip
cursor={false}
content={({ active, payload, label }) => {
if (active && payload && payload.length) {
const data = payload?.[0]?.payload;
return (
<div className="rounded-lg border bg-background p-2 shadow-sm">
<div className="grid grid-cols-2 gap-2">
<div className="flex flex-col">
<span className="text-[0.70rem] uppercase text-muted-foreground">
Time
</span>
<span className="font-bold">
{formatTimestamp(label)}
</span>
</div>
<div className="flex flex-col">
<span className="text-[0.70rem] uppercase text-muted-foreground">
Memory
</span>
<span className="font-bold">{data.memory}%</span>
</div>
<div className="flex flex-col col-span-2">
<span className="text-[0.70rem] uppercase text-muted-foreground">
Usage
</span>
<span className="font-bold">{data.usage}</span>
</div>
</div>
</div>
);
}
return null;
}}
/>
<Area
name="Memory"
dataKey="memory"
type="monotone"
fill="url(#fillMemory)"
stroke="hsl(var(--chart-2))"
strokeWidth={2}
/>
<ChartLegend
content={<ChartLegendContent />}
verticalAlign="bottom"
align="center"
/>
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
);
};

View File

@@ -0,0 +1,186 @@
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
type ChartConfig,
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
} from "@/components/ui/chart";
import { formatTimestamp } from "@/lib/utils";
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
interface ContainerMetric {
timestamp: string;
Network: {
input: number;
output: number;
inputUnit: string;
outputUnit: string;
};
}
interface Props {
data: ContainerMetric[];
}
interface FormattedMetric {
timestamp: string;
input: number;
output: number;
inputUnit: string;
outputUnit: string;
}
const chartConfig = {
input: {
label: "Input",
color: "hsl(var(--chart-3))",
},
output: {
label: "Output",
color: "hsl(var(--chart-4))",
},
} satisfies ChartConfig;
export const ContainerNetworkChart = ({ data }: Props) => {
const formattedData: FormattedMetric[] = data.map((metric) => ({
timestamp: metric.timestamp,
input: metric.Network.input,
output: metric.Network.output,
inputUnit: metric.Network.inputUnit,
outputUnit: metric.Network.outputUnit,
}));
const latestData = formattedData[formattedData.length - 1] || {
input: 0,
output: 0,
inputUnit: "B",
outputUnit: "B",
};
return (
<Card className="bg-transparent">
<CardHeader className="border-b py-5">
<CardTitle>Network I/O</CardTitle>
<CardDescription>
Input: {latestData.input}
{latestData.inputUnit} / Output: {latestData.output}
{latestData.outputUnit}
</CardDescription>
</CardHeader>
<CardContent className="px-2 pt-4 sm:px-6 sm:pt-6">
<ChartContainer
config={chartConfig}
className="aspect-auto h-[250px] w-full"
>
<AreaChart data={formattedData}>
<defs>
<linearGradient id="fillInput" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="hsl(var(--chart-3))"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="hsl(var(--chart-3))"
stopOpacity={0.1}
/>
</linearGradient>
<linearGradient id="fillOutput" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="hsl(var(--chart-4))"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="hsl(var(--chart-4))"
stopOpacity={0.1}
/>
</linearGradient>
</defs>
<CartesianGrid vertical={false} />
<XAxis
dataKey="timestamp"
tickLine={false}
axisLine={false}
tickMargin={8}
minTickGap={32}
tickFormatter={(value) => formatTimestamp(value)}
/>
<YAxis />
<ChartTooltip
cursor={false}
content={({ active, payload, label }) => {
if (active && payload && payload.length) {
const data = payload?.[0]?.payload;
return (
<div className="rounded-lg border bg-background p-2 shadow-sm">
<div className="grid grid-cols-2 gap-2">
<div className="flex flex-col">
<span className="text-[0.70rem] uppercase text-muted-foreground">
Time
</span>
<span className="font-bold">
{formatTimestamp(label)}
</span>
</div>
<div className="flex flex-col">
<span className="text-[0.70rem] uppercase text-muted-foreground">
Input
</span>
<span className="font-bold">
{data.input}
{data.inputUnit}
</span>
</div>
<div className="flex flex-col">
<span className="text-[0.70rem] uppercase text-muted-foreground">
Output
</span>
<span className="font-bold">
{data.output}
{data.outputUnit}
</span>
</div>
</div>
</div>
);
}
return null;
}}
/>
<Area
name="Input"
dataKey="input"
type="monotone"
fill="url(#fillInput)"
stroke="hsl(var(--chart-3))"
strokeWidth={2}
/>
<Area
name="Output"
dataKey="output"
type="monotone"
fill="url(#fillOutput)"
stroke="hsl(var(--chart-4))"
strokeWidth={2}
/>
<ChartLegend
content={<ChartLegendContent />}
verticalAlign="bottom"
align="center"
/>
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
);
};

View File

@@ -20,18 +20,22 @@ import { api } from "@/utils/api";
import { Loader2 } from "lucide-react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { DockerMonitoring } from "../../monitoring/docker/show";
import { ContainerPaidMonitoring } from "./show-paid-container-monitoring";
interface Props {
appName: string;
serverId?: string;
appType: "stack" | "docker-compose";
baseUrl: string;
token: string;
}
export const ShowMonitoringCompose = ({
export const ComposePaidMonitoring = ({
appName,
appType = "stack",
serverId,
baseUrl,
token,
}: Props) => {
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
{
@@ -44,9 +48,9 @@ export const ShowMonitoringCompose = ({
},
);
const [containerAppName, setContainerAppName] = useState<
string | undefined
>();
const [containerAppName, setContainerAppName] = useState<string | undefined>(
"",
);
const [containerId, setContainerId] = useState<string | undefined>();
@@ -62,7 +66,7 @@ export const ShowMonitoringCompose = ({
return (
<div>
<Card className="bg-background">
<Card className="bg-background border-0">
<CardHeader>
<CardTitle className="text-xl">Monitoring</CardTitle>
<CardDescription>Watch the usage of your compose</CardDescription>
@@ -118,10 +122,13 @@ export const ShowMonitoringCompose = ({
Restart
</Button>
</div>
<DockerMonitoring
appName={containerAppName || ""}
appType={appType}
/>
<div className="flex flex-col gap-4">
<ContainerPaidMonitoring
appName={containerAppName || ""}
baseUrl={baseUrl}
token={token}
/>
</div>
</CardContent>
</Card>
</div>

View File

@@ -0,0 +1,298 @@
import { Card } from "@/components/ui/card";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Cpu, HardDrive, Loader2, MemoryStick, Network } from "lucide-react";
import { useEffect, useState } from "react";
import { ContainerBlockChart } from "./container-block-chart";
import { ContainerCPUChart } from "./container-cpu-chart";
import { ContainerMemoryChart } from "./container-memory-chart";
import { ContainerNetworkChart } from "./container-network-chart";
const REFRESH_INTERVALS = {
"5000": "5 Seconds",
"10000": "10 Seconds",
"20000": "20 Seconds",
"30000": "30 Seconds",
} as const;
const DATA_POINTS_OPTIONS = {
"50": "50 points",
"200": "200 points",
"500": "500 points",
"800": "800 points",
"1200": "1200 points",
"1600": "1600 points",
"2000": "2000 points",
all: "All points",
} as const;
interface ContainerMetric {
timestamp: string;
CPU: number;
Memory: {
percentage: number;
used: number;
total: number;
unit: string;
usedUnit: string;
totalUnit: string;
};
Network: {
input: number;
output: number;
inputUnit: string;
outputUnit: string;
};
BlockIO: {
read: number;
write: number;
readUnit: string;
writeUnit: string;
};
Container: string;
ID: string;
Name: string;
}
interface Props {
appName: string;
baseUrl: string;
token: string;
}
export const ContainerPaidMonitoring = ({ appName, baseUrl, token }: Props) => {
const [historicalData, setHistoricalData] = useState<ContainerMetric[]>([]);
const [metrics, setMetrics] = useState<ContainerMetric>(
{} as ContainerMetric,
);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [dataPoints, setDataPoints] =
useState<keyof typeof DATA_POINTS_OPTIONS>("50");
const [refreshInterval, setRefreshInterval] = useState<string>("5000");
const fetchMetrics = async () => {
try {
const url = new URL(`${baseUrl}/metrics/containers`);
// if (dataPoints !== "all") {
url.searchParams.append("limit", dataPoints);
// }
if (!appName) {
throw new Error(
[
"No Application Selected:",
"",
"Make Sure to select an application to monitor.",
].join("\n"),
);
}
url.searchParams.append("appName", appName);
const response = await fetch(url.toString(), {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!response.ok) {
throw new Error(
`Error ${response.status}: ${response.statusText}. Please verify that the application "${appName}" is running and this service is included in the monitoring configuration.`,
);
}
const data = await response.json();
if (!Array.isArray(data) || data.length === 0) {
throw new Error(
[
`No monitoring data available for "${appName}". This could be because:`,
"",
"1. The container was recently started - wait a few minutes for data to be collected",
"2. The container is not running - verify its status",
"3. The service is not included in your monitoring configuration",
].join("\n"),
);
}
setHistoricalData(data);
setMetrics(data[data.length - 1]);
setError(null);
} catch (err) {
setError(
err instanceof Error
? err.message
: "Failed to fetch metrics, Please check your monitoring Instance is Configured correctly.",
);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchMetrics();
if (dataPoints === "all") {
return;
}
const interval = setInterval(() => {
fetchMetrics();
}, Number(refreshInterval));
return () => clearInterval(interval);
}, [dataPoints, appName, token, refreshInterval]);
if (isLoading) {
return (
<div className="flex h-[400px] w-full items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
);
}
if (error) {
return (
<div className="mt-5 flex min-h-[55vh] w-full items-center justify-center p-4">
<div className="max-w-xl text-center">
<p className="mb-2 text-base font-medium leading-none text-muted-foreground">
Error fetching metrics for{" "}
<strong className="text-primary">{appName}</strong>
</p>
<p className="whitespace-pre-line text-sm text-destructive">
{error}
</p>
<p className=" text-sm text-muted-foreground">URL: {baseUrl}</p>
</div>
</div>
);
}
return (
<>
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold tracking-tight">
Container Monitoring
</h2>
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">Data points:</span>
<Select
value={dataPoints}
onValueChange={(value: keyof typeof DATA_POINTS_OPTIONS) =>
setDataPoints(value)
}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Select points" />
</SelectTrigger>
<SelectContent>
{Object.entries(DATA_POINTS_OPTIONS).map(([value, label]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
<span className="text-sm text-muted-foreground">
Refresh interval:
</span>
<Select
value={refreshInterval}
onValueChange={(value: keyof typeof REFRESH_INTERVALS) =>
setRefreshInterval(value)
}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Select interval" />
</SelectTrigger>
<SelectContent>
{Object.entries(REFRESH_INTERVALS).map(([value, label]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* Stats Cards */}
<div className="grid gap-4 md:grid-cols-4">
<Card className="p-6 bg-transparent">
<div className="flex items-center gap-2">
<Cpu className="h-4 w-4 text-muted-foreground" />
<h3 className="text-sm font-medium">CPU Usage</h3>
</div>
<p className="mt-2 text-2xl font-bold">{metrics.CPU}%</p>
</Card>
<Card className="p-6 bg-transparent">
<div className="flex items-center gap-2">
<MemoryStick className="h-4 w-4 text-muted-foreground" />
<h3 className="text-sm font-medium">Memory Usage</h3>
</div>
<p className="mt-2 text-2xl font-bold">
{metrics.Memory.percentage}%
</p>
<p className="mt-1 text-sm text-muted-foreground">
{metrics.Memory.used} {metrics.Memory.unit} / {metrics.Memory.total}{" "}
{metrics.Memory.unit}
</p>
</Card>
<Card className="p-6 bg-transparent">
<div className="flex items-center gap-2">
<Network className="h-4 w-4 text-muted-foreground" />
<h3 className="text-sm font-medium">Network I/O</h3>
</div>
<p className="mt-2 text-2xl font-bold">
{metrics.Network.input} {metrics.Network.inputUnit} /{" "}
{metrics.Network.output} {metrics.Network.outputUnit}
</p>
</Card>
<Card className="p-6 bg-transparent">
<div className="flex items-center gap-2">
<HardDrive className="h-4 w-4 text-muted-foreground" />
<h3 className="text-sm font-medium">Block I/O</h3>
</div>
<p className="mt-2 text-2xl font-bold">
{metrics.BlockIO.read} {metrics.BlockIO.readUnit} /{" "}
{metrics.BlockIO.write} {metrics.BlockIO.writeUnit}
</p>
</Card>
</div>
{/* Container Information */}
<Card className="p-6 bg-transparent">
<h3 className="text-lg font-medium mb-4">Container Information</h3>
<div className="grid gap-4 md:grid-cols-2">
<div>
<h4 className="text-sm font-medium text-muted-foreground">
Container ID
</h4>
<p className="mt-1">{metrics.ID}</p>
</div>
<div>
<h4 className="text-sm font-medium text-muted-foreground">Name</h4>
<p className="mt-1">{metrics.Name}</p>
</div>
</div>
</Card>
{/* Charts Grid */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-2">
<ContainerCPUChart data={historicalData} />
<ContainerMemoryChart data={historicalData} />
<ContainerBlockChart data={historicalData} />
<ContainerNetworkChart data={historicalData} />
</div>
</>
);
};

View File

@@ -0,0 +1,115 @@
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
type ChartConfig,
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
} from "@/components/ui/chart";
import { formatTimestamp } from "@/lib/utils";
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
interface CPUChartProps {
data: any[];
}
const chartConfig = {
cpu: {
label: "CPU",
color: "hsl(var(--chart-1))",
},
} satisfies ChartConfig;
export function CPUChart({ data }: CPUChartProps) {
const latestData = data[data.length - 1] || {};
return (
<Card className="bg-transparent">
<CardHeader className="border-b py-5">
<CardTitle>CPU</CardTitle>
<CardDescription>CPU Usage: {latestData.cpu}%</CardDescription>
</CardHeader>
<CardContent className="px-2 pt-4 sm:px-6 sm:pt-6">
<ChartContainer
config={chartConfig}
className="aspect-auto h-[250px] w-full"
>
<AreaChart data={data}>
<defs>
<linearGradient id="fillCPU" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="hsl(var(--chart-1))"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="hsl(var(--chart-1))"
stopOpacity={0.1}
/>
</linearGradient>
</defs>
<CartesianGrid vertical={false} />
<XAxis
dataKey="timestamp"
tickLine={false}
axisLine={false}
tickMargin={8}
minTickGap={32}
tickFormatter={(value) => formatTimestamp(value)}
/>
<YAxis tickFormatter={(value) => `${value}%`} domain={[0, 100]} />
<ChartTooltip
cursor={false}
content={({ active, payload, label }) => {
if (active && payload && payload.length) {
const data = payload?.[0]?.payload;
return (
<div className="rounded-lg border bg-background p-2 shadow-sm">
<div className="grid grid-cols-2 gap-2">
<div className="flex flex-col">
<span className="text-[0.70rem] uppercase text-muted-foreground">
Time
</span>
<span className="font-bold">
{formatTimestamp(label)}
</span>
</div>
<div className="flex flex-col">
<span className="text-[0.70rem] uppercase text-muted-foreground">
CPU
</span>
<span className="font-bold">{data.cpu}%</span>
</div>
</div>
</div>
);
}
return null;
}}
/>
<Area
name="CPU"
dataKey="cpu"
type="monotone"
fill="url(#fillCPU)"
stroke="hsl(var(--chart-1))"
strokeWidth={2}
/>
<ChartLegend
content={<ChartLegendContent />}
verticalAlign="bottom"
align="center"
/>
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,120 @@
import { HardDrive } from "lucide-react";
import {
Label,
PolarGrid,
PolarRadiusAxis,
RadialBar,
RadialBarChart,
} from "recharts";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { type ChartConfig, ChartContainer } from "@/components/ui/chart";
interface RadialChartProps {
data: any;
}
export function DiskChart({ data }: RadialChartProps) {
const diskUsed = Number.parseFloat(data.diskUsed || 0);
const totalDiskGB = Number.parseFloat(data.totalDisk || 0);
const usedDiskGB = (totalDiskGB * diskUsed) / 100;
const chartData = [
{
disk: 25,
fill: "hsl(var(--chart-2))",
},
];
const chartConfig = {
disk: {
label: "Disk",
color: "hsl(var(--chart-2))",
},
} satisfies ChartConfig;
const endAngle = (diskUsed * 360) / 100;
return (
<Card className="flex flex-col bg-transparent">
<CardHeader className="items-center border-b pb-5">
<CardTitle>Disk</CardTitle>
<CardDescription>Storage Space</CardDescription>
</CardHeader>
<CardContent className="flex-1 pb-0">
<ChartContainer
config={chartConfig}
className="mx-auto aspect-square max-h-[250px]"
>
<RadialBarChart
data={chartData}
startAngle={0}
endAngle={endAngle}
innerRadius={80}
outerRadius={110}
>
<PolarGrid
gridType="circle"
radialLines={false}
stroke="none"
className="first:fill-muted last:fill-background"
polarRadius={[86, 74]}
/>
<RadialBar
dataKey="disk"
background
cornerRadius={10}
fill="hsl(var(--chart-2))"
/>
<PolarRadiusAxis tick={false} tickLine={false} axisLine={false}>
<Label
content={({ viewBox }) => {
if (viewBox && "cx" in viewBox && "cy" in viewBox) {
return (
<text
x={viewBox.cx}
y={viewBox.cy}
textAnchor="middle"
dominantBaseline="middle"
>
<tspan
x={viewBox.cx}
y={viewBox.cy}
className="fill-foreground text-4xl font-bold"
>
{diskUsed.toFixed(1)}%
</tspan>
<tspan
x={viewBox.cx}
y={(viewBox.cy || 0) + 24}
className="fill-muted-foreground text-sm"
>
Used
</tspan>
</text>
);
}
}}
/>
</PolarRadiusAxis>
</RadialBarChart>
</ChartContainer>
</CardContent>
<CardFooter className="flex-col gap-2 text-sm">
<div className="flex items-center gap-2 font-medium leading-none">
<HardDrive className="h-4 w-4" /> {usedDiskGB.toFixed(1)} GB used
</div>
<div className="leading-none text-muted-foreground">
Of {totalDiskGB.toFixed(1)} GB total
</div>
</CardFooter>
</Card>
);
}

View File

@@ -0,0 +1,128 @@
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
type ChartConfig,
ChartContainer,
ChartTooltip,
} from "@/components/ui/chart";
import { formatTimestamp } from "@/lib/utils";
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
interface MemoryChartProps {
data: any[];
}
const chartConfig = {
Memory: {
label: "Memory",
color: "hsl(var(--chart-2))",
},
} satisfies ChartConfig;
export function MemoryChart({ data }: MemoryChartProps) {
const latestData = data[data.length - 1] || {};
return (
<Card className="bg-transparent">
<CardHeader className="border-b py-5">
<CardTitle>Memory</CardTitle>
<CardDescription>
Memory Usage: {latestData.memUsedGB} GB of {latestData.memTotal} GB (
{latestData.memUsed}%)
</CardDescription>
</CardHeader>
<CardContent className="px-2 pt-4 sm:px-6 sm:pt-6">
<ChartContainer
config={chartConfig}
className="aspect-auto h-[250px] w-full"
>
<AreaChart data={data}>
<defs>
<linearGradient id="fillMemory" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="hsl(var(--chart-2))"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="hsl(var(--chart-2))"
stopOpacity={0.1}
/>
</linearGradient>
</defs>
<CartesianGrid vertical={false} />
<XAxis
dataKey="timestamp"
tickLine={false}
axisLine={false}
tickMargin={8}
minTickGap={32}
tickFormatter={(value) => formatTimestamp(value)}
/>
<YAxis
yAxisId="left"
orientation="left"
tickFormatter={(value) => `${value}%`}
domain={[0, 100]}
/>
<YAxis
yAxisId="right"
orientation="right"
tickFormatter={(value) => `${value.toFixed(1)} GB`}
domain={[
0,
Math.ceil(Number.parseFloat(latestData.memTotal || "0")),
]}
/>
<ChartTooltip
cursor={false}
content={({ active, payload, label }) => {
if (active && payload && payload.length) {
const data = payload?.[0]?.payload;
return (
<div className="rounded-lg border bg-background p-2 shadow-sm">
<div className="grid grid-cols-2 gap-2">
<div className="flex flex-col">
<span className="text-[0.70rem] uppercase text-muted-foreground">
Time
</span>
<span className="font-bold">
{formatTimestamp(label)}
</span>
</div>
<div className="flex flex-col">
<span className="text-[0.70rem] uppercase text-muted-foreground">
Memory
</span>
<span className="font-bold">
{data.memUsed}% ({data.memUsedGB} GB)
</span>
</div>
</div>
</div>
);
}
return null;
}}
/>
<Area
yAxisId="left"
dataKey="memUsed"
type="monotone"
fill="url(#fillMemory)"
stroke="hsl(var(--chart-2))"
strokeWidth={2}
name="Memory"
/>
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,145 @@
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
type ChartConfig,
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
} from "@/components/ui/chart";
import { formatTimestamp } from "@/lib/utils";
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
interface NetworkChartProps {
data: any[];
}
const chartConfig = {
networkIn: {
label: "Network In",
color: "hsl(var(--chart-3))",
},
networkOut: {
label: "Network Out",
color: "hsl(var(--chart-4))",
},
} satisfies ChartConfig;
export function NetworkChart({ data }: NetworkChartProps) {
const latestData = data[data.length - 1] || {};
return (
<Card className="bg-transparent">
<CardHeader className="border-b py-5">
<CardTitle>Network</CardTitle>
<CardDescription>
Network Traffic: {latestData.networkOut} KB/s {" "}
{latestData.networkIn} KB/s
</CardDescription>
</CardHeader>
<CardContent className="px-2 pt-4 sm:px-6 sm:pt-6">
<ChartContainer
config={chartConfig}
className="aspect-auto h-[250px] w-full"
>
<AreaChart data={data}>
<defs>
<linearGradient id="fillNetworkIn" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="hsl(var(--chart-3))"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="hsl(var(--chart-3))"
stopOpacity={0.1}
/>
</linearGradient>
<linearGradient id="fillNetworkOut" x1="0" y1="0" x2="0" y2="1">
<stop
offset="5%"
stopColor="hsl(var(--chart-4))"
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor="hsl(var(--chart-4))"
stopOpacity={0.1}
/>
</linearGradient>
</defs>
<CartesianGrid vertical={false} />
<XAxis
dataKey="timestamp"
tickLine={false}
axisLine={false}
tickMargin={8}
minTickGap={32}
tickFormatter={(value) => formatTimestamp(value)}
/>
<YAxis tickFormatter={(value) => `${value} KB/s`} />
<ChartTooltip
cursor={false}
content={({ active, payload, label }) => {
if (active && payload && payload.length) {
const data = payload?.[0]?.payload;
return (
<div className="rounded-lg border bg-background p-2 shadow-sm">
<div className="grid grid-cols-2 gap-2">
<div className="flex flex-col">
<span className="text-[0.70rem] uppercase text-muted-foreground">
Time
</span>
<span className="font-bold">
{formatTimestamp(label)}
</span>
</div>
<div className="flex flex-col">
<span className="text-[0.70rem] uppercase text-muted-foreground">
Network
</span>
<span className="font-bold">
{data.networkOut} KB/s
<br /> {data.networkIn} KB/s
</span>
</div>
</div>
</div>
);
}
return null;
}}
/>
<Area
name="Network In"
dataKey="networkIn"
type="monotone"
fill="url(#fillNetworkIn)"
stroke="hsl(var(--chart-3))"
strokeWidth={2}
/>
<Area
name="Network Out"
dataKey="networkOut"
type="monotone"
fill="url(#fillNetworkOut)"
stroke="hsl(var(--chart-4))"
strokeWidth={2}
/>
<ChartLegend
content={<ChartLegendContent />}
verticalAlign="bottom"
align="center"
/>
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,302 @@
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Clock, Cpu, HardDrive, Loader2, MemoryStick } from "lucide-react";
import type React from "react";
import { useEffect, useState } from "react";
import { CPUChart } from "./cpu-chart";
import { DiskChart } from "./disk-chart";
import { MemoryChart } from "./memory-chart";
import { NetworkChart } from "./network-chart";
const REFRESH_INTERVALS = {
"5000": "5 Seconds",
"10000": "10 Seconds",
"20000": "20 Seconds",
"30000": "30 Seconds",
} as const;
const DATA_POINTS_OPTIONS = {
"50": "50 points",
"200": "200 points",
"500": "500 points",
"800": "800 points",
"1200": "1200 points",
"1600": "1600 points",
"2000": "2000 points",
all: "All points",
} as const;
interface SystemMetrics {
cpu: string;
cpuModel: string;
cpuCores: number;
cpuPhysicalCores: number;
cpuSpeed: number;
os: string;
distro: string;
kernel: string;
arch: string;
memUsed: string;
memUsedGB: string;
memTotal: string;
uptime: number;
diskUsed: string;
totalDisk: string;
networkIn: string;
networkOut: string;
timestamp: string;
}
interface Props {
BASE_URL?: string;
token?: string;
}
export const ShowPaidMonitoring = ({
BASE_URL = process.env.NEXT_PUBLIC_METRICS_URL ||
"http://localhost:3001/metrics",
token = process.env.NEXT_PUBLIC_METRICS_TOKEN || "my-token",
}: Props) => {
const [historicalData, setHistoricalData] = useState<SystemMetrics[]>([]);
const [metrics, setMetrics] = useState<SystemMetrics>({} as SystemMetrics);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [dataPoints, setDataPoints] =
useState<keyof typeof DATA_POINTS_OPTIONS>("50");
const [refreshInterval, setRefreshInterval] = useState<string>("5000");
const fetchMetrics = async () => {
try {
const url = new URL(BASE_URL);
url.searchParams.append("limit", dataPoints);
const response = await fetch(url.toString(), {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!response.ok) {
throw new Error(
`Error ${response.status}: ${response.statusText}. Ensure the container is running and this service is included in the monitoring configuration.`,
);
}
const data = await response.json();
if (!Array.isArray(data) || data.length === 0) {
throw new Error(
[
"No monitoring data available. This could be because:",
"",
"1. You don't have setup the monitoring service, you can do in web server section.",
"2. If you already have setup the monitoring service, wait a few minutes and refresh the page.",
].join("\n"),
);
}
const formattedData = data.map((metric: SystemMetrics) => ({
timestamp: metric.timestamp,
cpu: Number.parseFloat(metric.cpu),
cpuModel: metric.cpuModel,
cpuCores: metric.cpuCores,
cpuPhysicalCores: metric.cpuPhysicalCores,
cpuSpeed: metric.cpuSpeed,
os: metric.os,
distro: metric.distro,
kernel: metric.kernel,
arch: metric.arch,
memUsed: Number.parseFloat(metric.memUsed),
memUsedGB: Number.parseFloat(metric.memUsedGB),
memTotal: Number.parseFloat(metric.memTotal),
networkIn: Number.parseFloat(metric.networkIn),
networkOut: Number.parseFloat(metric.networkOut),
diskUsed: Number.parseFloat(metric.diskUsed),
totalDisk: Number.parseFloat(metric.totalDisk),
uptime: metric.uptime,
}));
// @ts-ignore
setHistoricalData(formattedData);
// @ts-ignore
setMetrics(formattedData[formattedData.length - 1] || {});
setError(null);
} catch (err) {
setError(
err instanceof Error
? err.message
: "Failed to fetch metrics, Please check your monitoring Instance is Configured correctly.",
);
} finally {
setIsLoading(false);
}
};
const formatUptime = (seconds: number): string => {
const days = Math.floor(seconds / (24 * 60 * 60));
const hours = Math.floor((seconds % (24 * 60 * 60)) / (60 * 60));
const minutes = Math.floor((seconds % (60 * 60)) / 60);
return `${days}d ${hours}h ${minutes}m`;
};
useEffect(() => {
fetchMetrics();
if (dataPoints === "all") {
return;
}
const interval = setInterval(() => {
fetchMetrics();
}, Number(refreshInterval));
return () => clearInterval(interval);
}, [dataPoints, token, refreshInterval]);
if (isLoading) {
return (
<div className="flex h-[400px] w-full items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
);
}
if (error) {
return (
<div className="flex min-h-[55vh] w-full items-center justify-center p-4">
<div className="max-w-xl text-center">
<p className="mb-2 text-base font-medium leading-none text-muted-foreground">
Error fetching metrics{" "}
</p>
<p className="whitespace-pre-line text-sm text-destructive">
{error}
</p>
<p className=" text-sm text-muted-foreground">URL: {BASE_URL}</p>
</div>
</div>
);
}
return (
<div className="space-y-4 pt-5 pb-10 w-full px-4">
<div className="flex justify-between items-center">
<h2 className="text-2xl font-bold tracking-tight">System Monitoring</h2>
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">Data points:</span>
<Select
value={dataPoints}
onValueChange={(value: keyof typeof DATA_POINTS_OPTIONS) =>
setDataPoints(value)
}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Select points" />
</SelectTrigger>
<SelectContent>
{Object.entries(DATA_POINTS_OPTIONS).map(([value, label]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
<span className="text-sm text-muted-foreground">
Refresh interval:
</span>
<Select
value={refreshInterval}
onValueChange={(value: keyof typeof REFRESH_INTERVALS) =>
setRefreshInterval(value)
}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Select interval" />
</SelectTrigger>
<SelectContent>
{Object.entries(REFRESH_INTERVALS).map(([value, label]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* Stats Cards */}
<div className="grid gap-4 md:grid-cols-4">
<div className="rounded-lg border text-card-foreground shadow-sm p-6">
<div className="flex items-center gap-2">
<Clock className="h-4 w-4 text-muted-foreground" />
<h3 className="text-sm font-medium">Uptime</h3>
</div>
<p className="mt-2 text-2xl font-bold">
{formatUptime(metrics.uptime || 0)}
</p>
</div>
<div className="rounded-lg border text-card-foreground shadow-sm p-6">
<div className="flex items-center gap-2">
<Cpu className="h-4 w-4 text-muted-foreground" />
<h3 className="text-sm font-medium">CPU Usage</h3>
</div>
<p className="mt-2 text-2xl font-bold">{metrics.cpu}%</p>
</div>
<div className="rounded-lg border text-card-foreground bg-transparent shadow-sm p-6">
<div className="flex items-center gap-2">
<MemoryStick className="h-4 w-4 text-muted-foreground" />
<h3 className="text-sm font-medium">Memory Usage</h3>
</div>
<p className="mt-2 text-2xl font-bold">
{metrics.memUsedGB} GB / {metrics.memTotal} GB
</p>
</div>
<div className="rounded-lg border text-card-foreground shadow-sm p-6">
<div className="flex items-center gap-2">
<HardDrive className="h-4 w-4 text-muted-foreground" />
<h3 className="text-sm font-medium">Disk Usage</h3>
</div>
<p className="mt-2 text-2xl font-bold">{metrics.diskUsed}%</p>
</div>
</div>
{/* System Information */}
<div className="rounded-lg border text-card-foreground shadow-sm p-6">
<h3 className="text-lg font-medium mb-4">System Information</h3>
<div className="grid gap-4 md:grid-cols-2">
<div>
<h4 className="text-sm font-medium text-muted-foreground">CPU</h4>
<p className="mt-1">{metrics.cpuModel}</p>
<p className="text-sm text-muted-foreground mt-1">
{metrics.cpuPhysicalCores} Physical Cores ({metrics.cpuCores}{" "}
Threads) @ {metrics.cpuSpeed}GHz
</p>
</div>
<div>
<h4 className="text-sm font-medium text-muted-foreground">
Operating System
</h4>
<p className="mt-1">{metrics.distro}</p>
<p className="text-sm text-muted-foreground mt-1">
Kernel: {metrics.kernel} ({metrics.arch})
</p>
</div>
</div>
</div>
{/* Charts Grid */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-2">
<CPUChart data={historicalData} />
<MemoryChart data={historicalData} />
<DiskChart data={metrics} />
<NetworkChart data={historicalData} />
</div>
</div>
);
};

View File

@@ -1,10 +0,0 @@
import React from "react";
import { DockerMonitoring } from "../docker/show";
export const ShowMonitoring = () => {
return (
<div className="w-full">
<DockerMonitoring appName="dokploy" />
</div>
);
};

View File

@@ -168,6 +168,7 @@ export const ShowGeneralRedis = ({ redisId }: Props) => {
setIsDrawerOpen(false);
setFilteredLogs([]);
setIsDeploying(false);
refetch();
}}
filteredLogs={filteredLogs}
/>

View File

@@ -49,6 +49,7 @@ const notificationBaseSchema = z.object({
databaseBackup: z.boolean().default(false),
dokployRestart: z.boolean().default(false),
dockerCleanup: z.boolean().default(false),
serverThreshold: z.boolean().default(false),
});
export const notificationSchema = z.discriminatedUnion("type", [
@@ -204,6 +205,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
channel: notification.slack?.channel || "",
name: notification.name,
type: notification.notificationType,
serverThreshold: notification.serverThreshold,
});
} else if (notification.notificationType === "telegram") {
form.reset({
@@ -216,6 +218,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
type: notification.notificationType,
name: notification.name,
dockerCleanup: notification.dockerCleanup,
serverThreshold: notification.serverThreshold,
});
} else if (notification.notificationType === "discord") {
form.reset({
@@ -228,6 +231,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
decoration: notification.discord?.decoration || undefined,
name: notification.name,
dockerCleanup: notification.dockerCleanup,
serverThreshold: notification.serverThreshold,
});
} else if (notification.notificationType === "email") {
form.reset({
@@ -244,6 +248,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
fromAddress: notification.email?.fromAddress,
name: notification.name,
dockerCleanup: notification.dockerCleanup,
serverThreshold: notification.serverThreshold,
});
} else if (notification.notificationType === "gotify") {
form.reset({
@@ -280,6 +285,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
dokployRestart,
databaseBackup,
dockerCleanup,
serverThreshold,
} = data;
let promise: Promise<unknown> | null = null;
if (data.type === "slack") {
@@ -294,6 +300,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
dockerCleanup: dockerCleanup,
slackId: notification?.slackId || "",
notificationId: notificationId || "",
serverThreshold: serverThreshold,
});
} else if (data.type === "telegram") {
promise = telegramMutation.mutateAsync({
@@ -307,6 +314,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
dockerCleanup: dockerCleanup,
notificationId: notificationId || "",
telegramId: notification?.telegramId || "",
serverThreshold: serverThreshold,
});
} else if (data.type === "discord") {
promise = discordMutation.mutateAsync({
@@ -320,6 +328,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
dockerCleanup: dockerCleanup,
notificationId: notificationId || "",
discordId: notification?.discordId || "",
serverThreshold: serverThreshold,
});
} else if (data.type === "email") {
promise = emailMutation.mutateAsync({
@@ -337,6 +346,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
dockerCleanup: dockerCleanup,
notificationId: notificationId || "",
emailId: notification?.emailId || "",
serverThreshold: serverThreshold,
});
} else if (data.type === "gotify") {
promise = gotifyMutation.mutateAsync({
@@ -955,6 +965,30 @@ export const HandleNotifications = ({ notificationId }: Props) => {
)}
/>
)}
{isCloud && (
<FormField
control={form.control}
name="serverThreshold"
render={({ field }) => (
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
<div className="space-y-0.5">
<FormLabel>Server Threshold</FormLabel>
<FormDescription>
Trigger the action when the server threshold is
reached.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
)}
</div>
</div>
</form>

View File

@@ -0,0 +1,636 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
} from "@/components/ui/command";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input, NumberInput } from "@/components/ui/input";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { ScrollArea } from "@/components/ui/scroll-area";
import { extractServices } from "@/pages/dashboard/project/[projectId]";
import { api } from "@/utils/api";
import { useUrl } from "@/utils/hooks/use-url";
import { zodResolver } from "@hookform/resolvers/zod";
import { Eye, EyeOff, LayoutDashboardIcon, RefreshCw } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
interface Props {
serverId?: string;
}
const Schema = z.object({
metricsConfig: z.object({
server: z.object({
refreshRate: z.number().min(2, {
message: "Server Refresh Rate is required",
}),
port: z.number().min(1, {
message: "Port is required",
}),
token: z.string(),
urlCallback: z.string(),
retentionDays: z.number().min(1, {
message: "Retention days must be at least 1",
}),
thresholds: z.object({
cpu: z.number().min(0),
memory: z.number().min(0),
}),
cronJob: z.string().min(1, {
message: "Cron Job is required",
}),
}),
containers: z.object({
refreshRate: z.number().min(2, {
message: "Container Refresh Rate is required",
}),
services: z.object({
include: z.array(z.string()).optional(),
exclude: z.array(z.string()).optional(),
}),
}),
}),
});
type Schema = z.infer<typeof Schema>;
export const SetupMonitoring = ({ serverId }: Props) => {
const { data, isLoading } = serverId
? api.server.one.useQuery(
{
serverId: serverId || "",
},
{
enabled: !!serverId,
},
)
: api.admin.one.useQuery();
const url = useUrl();
const { data: projects } = api.project.all.useQuery();
const extractServicesFromProjects = (projects: any[] | undefined) => {
if (!projects) return [];
const allServices = projects.flatMap((project) => {
const services = extractServices(project);
return serverId
? services
.filter((service) => service.serverId === serverId)
.map((service) => service.appName)
: services.map((service) => service.appName);
});
return [...new Set(allServices)];
};
const services = extractServicesFromProjects(projects);
const form = useForm<Schema>({
resolver: zodResolver(Schema),
defaultValues: {
metricsConfig: {
server: {
refreshRate: 20,
port: 4500,
token: "",
urlCallback: `${url}/api/trpc/notification.receiveNotification`,
retentionDays: 7,
thresholds: {
cpu: 0,
memory: 0,
},
cronJob: "",
},
containers: {
refreshRate: 20,
services: {
include: [],
exclude: [],
},
},
},
},
});
useEffect(() => {
if (data) {
form.reset({
metricsConfig: {
server: {
refreshRate: data?.metricsConfig?.server?.refreshRate,
port: data?.metricsConfig?.server?.port,
token: data?.metricsConfig?.server?.token || generateToken(),
urlCallback:
data?.metricsConfig?.server?.urlCallback ||
`${url}/api/trpc/notification.receiveNotification`,
retentionDays: data?.metricsConfig?.server?.retentionDays || 5,
thresholds: {
cpu: data?.metricsConfig?.server?.thresholds?.cpu,
memory: data?.metricsConfig?.server?.thresholds?.memory,
},
cronJob: data?.metricsConfig?.server?.cronJob || "0 0 * * *",
},
containers: {
refreshRate: data?.metricsConfig?.containers?.refreshRate,
services: {
include: data?.metricsConfig?.containers?.services?.include,
exclude: data?.metricsConfig?.containers?.services?.exclude,
},
},
},
});
}
}, [data, url]);
const [search, setSearch] = useState("");
const [searchExclude, setSearchExclude] = useState("");
const [showToken, setShowToken] = useState(false);
const availableServices = services?.filter(
(service) =>
!form
.watch("metricsConfig.containers.services.include")
?.some((s) => s === service) &&
!form
.watch("metricsConfig.containers.services.exclude")
?.includes(service) &&
service.toLowerCase().includes(search.toLowerCase()),
);
const availableServicesToExclude = [
...(services?.filter(
(service) =>
!form
.watch("metricsConfig.containers.services.exclude")
?.includes(service) &&
!form
.watch("metricsConfig.containers.services.include")
?.some((s) => s === service) &&
service.toLowerCase().includes(searchExclude.toLowerCase()),
) ?? []),
...(!form.watch("metricsConfig.containers.services.exclude")?.includes("*")
? ["*"]
: []),
];
const { mutateAsync } = serverId
? api.server.setupMonitoring.useMutation()
: api.admin.setupMonitoring.useMutation();
const generateToken = () => {
const array = new Uint8Array(64);
crypto.getRandomValues(array);
return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join(
"",
);
};
const onSubmit = async (values: Schema) => {
await mutateAsync({
serverId: serverId || "",
metricsConfig: values.metricsConfig,
})
.then(() => {
toast.success("Server updated successfully");
})
.catch(() => {
toast.error("Error updating the server");
});
};
return (
<>
<CardHeader className="">
<CardTitle className="text-xl flex flex-row gap-2">
<LayoutDashboardIcon className="size-6 text-muted-foreground self-center" />
Monitoring
</CardTitle>
<CardDescription>
Monitor your servers and containers in realtime with notifications
when they reach their thresholds.
</CardDescription>
</CardHeader>
<CardContent className="space-y-6 py-6 border-t">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex w-full flex-col gap-4"
>
<AlertBlock>
Using a lower refresh rate will make your CPU and memory usage
higher, we recommend 30-60 seconds
</AlertBlock>
<div className="flex flex-col gap-4">
<FormField
control={form.control}
name="metricsConfig.server.refreshRate"
render={({ field }) => (
<FormItem className="flex flex-col justify-center max-sm:items-center">
<FormLabel>Server Refresh Rate</FormLabel>
<FormControl>
<NumberInput placeholder="10" {...field} />
</FormControl>
<FormDescription>
Please set the refresh rate for the server in seconds
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="metricsConfig.containers.refreshRate"
render={({ field }) => (
<FormItem className="flex flex-col justify-center max-sm:items-center">
<FormLabel>Container Refresh Rate</FormLabel>
<FormControl>
<NumberInput placeholder="10" {...field} />
</FormControl>
<FormDescription>
Please set the refresh rate for the containers in seconds
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="metricsConfig.server.cronJob"
render={({ field }) => (
<FormItem>
<FormLabel>Cron Job</FormLabel>
<FormControl>
<Input {...field} placeholder="0 0 * * *" />
</FormControl>
<FormDescription>
Cron job for cleaning up metrics
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="metricsConfig.server.retentionDays"
render={({ field }) => (
<FormItem>
<FormLabel>Server Retention Days</FormLabel>
<FormControl>
<NumberInput {...field} />
</FormControl>
<FormDescription>
Number of days to retain server metrics data
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="metricsConfig.server.port"
render={({ field }) => (
<FormItem className="flex flex-col justify-center max-sm:items-center">
<FormLabel>Port</FormLabel>
<FormControl>
<NumberInput placeholder="4500" {...field} />
</FormControl>
<FormDescription>
Please set the port for the metrics server
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="metricsConfig.containers.services.include"
render={({ field }) => (
<FormItem>
<FormLabel>Include Services</FormLabel>
<FormControl>
<div className="flex flex-col gap-4">
<div className="flex gap-2">
<Popover>
<PopoverTrigger asChild>
<Button variant="outline">Add Service</Button>
</PopoverTrigger>
<PopoverContent
className="w-[300px] p-0"
align="start"
>
<Command>
<CommandInput
placeholder="Search service..."
value={search}
onValueChange={setSearch}
/>
{availableServices?.length === 0 ? (
<div className="p-4 text-sm text-muted-foreground">
No services available.
</div>
) : (
<>
<CommandEmpty>
No service found.
</CommandEmpty>
<CommandGroup>
{availableServices?.map((service) => (
<CommandItem
key={service}
value={service}
onSelect={() => {
field.onChange([
...(field.value ?? []),
service,
]);
setSearch("");
}}
>
{service}
</CommandItem>
))}
</CommandGroup>
</>
)}
</Command>
</PopoverContent>
</Popover>
</div>
<div className="flex flex-wrap gap-2">
{field.value?.map((service) => (
<Badge
key={service}
variant="secondary"
className="flex items-center gap-2"
>
{service}
<Button
type="button"
variant="ghost"
size="icon"
className="h-4 w-4 p-0"
onClick={() => {
field.onChange(
field.value?.filter((s) => s !== service),
);
}}
>
×
</Button>
</Badge>
))}
<FormDescription>
Services to monitor.
</FormDescription>
</div>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="metricsConfig.containers.services.exclude"
render={({ field }) => (
<FormItem>
<FormLabel>Exclude Services</FormLabel>
<FormControl>
<div className="flex flex-col gap-4">
<div className="flex gap-2">
<Popover>
<PopoverTrigger asChild>
<Button variant="outline">Add Service</Button>
</PopoverTrigger>
<PopoverContent
className="w-[300px] p-0"
align="start"
>
<Command>
<CommandInput
placeholder="Search service..."
value={searchExclude}
onValueChange={setSearchExclude}
/>
{availableServicesToExclude?.length === 0 ? (
<div className="p-4 text-sm text-muted-foreground">
No services available.
</div>
) : (
<>
<CommandEmpty>
No service found.
</CommandEmpty>
<CommandGroup>
{availableServicesToExclude.map(
(service) => (
<CommandItem
key={service}
value={service}
onSelect={() => {
field.onChange([
...(field.value ?? []),
service,
]);
setSearchExclude("");
}}
>
{service}
</CommandItem>
),
)}
</CommandGroup>
</>
)}
</Command>
</PopoverContent>
</Popover>
</div>
<div className="flex flex-wrap gap-2">
{field.value?.map((service, index) => (
<Badge
key={service}
variant="secondary"
className="flex items-center gap-2"
>
{service}
<Button
type="button"
variant="ghost"
size="icon"
className="h-4 w-4 p-0"
onClick={() => {
field.onChange(
field.value?.filter((_, i) => i !== index),
);
}}
>
×
</Button>
</Badge>
))}
<FormDescription>
Services to exclude from monitoring
</FormDescription>
</div>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="metricsConfig.server.thresholds.cpu"
render={({ field }) => (
<FormItem>
<FormLabel>CPU Threshold (%)</FormLabel>
<FormControl>
<NumberInput {...field} />
</FormControl>
<FormDescription>
Alert when CPU usage exceeds this percentage
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="metricsConfig.server.thresholds.memory"
render={({ field }) => (
<FormItem>
<FormLabel>Memory Threshold (%)</FormLabel>
<FormControl>
<NumberInput {...field} />
</FormControl>
<FormDescription>
Alert when memory usage exceeds this percentage
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="metricsConfig.server.token"
render={({ field }) => (
<FormItem>
<FormLabel>Metrics Token</FormLabel>
<FormControl>
<div className="flex gap-2">
<div className="relative flex-1">
<Input
type={showToken ? "text" : "password"}
placeholder="Enter your metrics token"
{...field}
/>
<Button
type="button"
variant="secondary"
size="icon"
className="absolute right-0 top-1/2 -translate-y-1/2"
onClick={() => setShowToken(!showToken)}
title={showToken ? "Hide token" : "Show token"}
>
{showToken ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</Button>
</div>
<Button
type="button"
variant="outline"
size="icon"
onClick={() => {
const newToken = generateToken();
form.setValue(
"metricsConfig.server.token",
newToken,
);
toast.success("Token generated successfully");
}}
title="Generate new token"
>
<RefreshCw className="h-4 w-4" />
</Button>
</div>
</FormControl>
<FormDescription>
Token for authenticating metrics requests
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="metricsConfig.server.urlCallback"
render={({ field }) => (
<FormItem>
<FormLabel>Metrics Callback URL</FormLabel>
<FormControl>
<Input
placeholder="https://your-callback-url.com"
{...field}
/>
</FormControl>
<FormDescription>
URL where metrics will be sent
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex items-center justify-end gap-2">
<Button type="submit" isLoading={form.formState.isSubmitting}>
Save changes
</Button>
</div>
</form>
</Form>
</CardContent>
</>
);
};

View File

@@ -19,6 +19,7 @@ import {
} from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import copy from "copy-to-clipboard";
import { CopyIcon, ExternalLinkIcon, ServerIcon } from "lucide-react";
@@ -30,6 +31,7 @@ import { type LogLine, parseLogs } from "../../docker/logs/utils";
import { EditScript } from "./edit-script";
import { GPUSupport } from "./gpu-support";
import { SecurityAudit } from "./security-audit";
import { SetupMonitoring } from "./setup-monitoring";
import { ValidateServer } from "./validate-server";
interface Props {
@@ -48,7 +50,7 @@ export const SetupServer = ({ serverId }: Props) => {
);
const [activeLog, setActiveLog] = useState<string | null>(null);
const { data: isCloud } = api.settings.isCloud.useQuery();
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
const [isDeploying, setIsDeploying] = useState(false);
@@ -112,11 +114,19 @@ export const SetupServer = ({ serverId }: Props) => {
</AlertBlock>
<Tabs defaultValue="ssh-keys">
<TabsList className="grid grid-cols-5 w-[700px]">
<TabsList
className={cn(
"grid w-[700px]",
isCloud ? "grid-cols-6" : "grid-cols-5",
)}
>
<TabsTrigger value="ssh-keys">SSH Keys</TabsTrigger>
<TabsTrigger value="deployments">Deployments</TabsTrigger>
<TabsTrigger value="validate">Validate</TabsTrigger>
<TabsTrigger value="audit">Security</TabsTrigger>
{isCloud && (
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
)}
<TabsTrigger value="gpu-setup">GPU Setup</TabsTrigger>
</TabsList>
<TabsContent
@@ -309,6 +319,16 @@ export const SetupServer = ({ serverId }: Props) => {
<SecurityAudit serverId={serverId} />
</div>
</TabsContent>
<TabsContent
value="monitoring"
className="outline-none ring-0 focus-visible:ring-0 focus-visible:ring-offset-0"
>
<div className="flex flex-col gap-2 text-sm pt-3">
<div className="rounded-xl bg-background shadow-md border">
<SetupMonitoring serverId={serverId} />
</div>
</div>
</TabsContent>
<TabsContent
value="gpu-setup"
className="outline-none ring-0 focus-visible:ring-0 focus-visible:ring-offset-0"

View File

@@ -0,0 +1,31 @@
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { useState } from "react";
import { ShowPaidMonitoring } from "../../monitoring/paid/servers/show-paid-monitoring";
interface Props {
url: string;
token: string;
}
export const ShowMonitoringModal = ({ url, token }: Props) => {
const [isOpen, setIsOpen] = useState(false);
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<DropdownMenuItem
className="w-full cursor-pointer "
onSelect={(e) => e.preventDefault()}
>
Show Monitoring
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="sm:max-w-7xl overflow-y-auto max-h-screen ">
<div className="flex gap-4 py-4 w-full">
<ShowPaidMonitoring BASE_URL={url} token={token} />
</div>
</DialogContent>
</Dialog>
);
};

View File

@@ -38,6 +38,7 @@ import { ShowServerActions } from "./actions/show-server-actions";
import { HandleServers } from "./handle-servers";
import { SetupServer } from "./setup-server";
import { ShowDockerContainersModal } from "./show-docker-containers-modal";
import { ShowMonitoringModal } from "./show-monitoring-modal";
import { ShowSwarmOverviewModal } from "./show-swarm-overview-modal";
import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal";
import { WelcomeSuscription } from "./welcome-stripe/welcome-suscription";
@@ -314,6 +315,16 @@ export const ShowServers = () => {
<ShowDockerContainersModal
serverId={server.serverId}
/>
{isCloud && (
<ShowMonitoringModal
url={`http://${server.ipAddress}:${server?.metricsConfig?.server?.port}/metrics`}
token={
server?.metricsConfig?.server
?.token
}
/>
)}
<ShowSwarmOverviewModal
serverId={server.serverId}
/>

View File

@@ -15,7 +15,7 @@ export const OnboardingLayout = ({ children }: Props) => {
<div className="absolute inset-0 bg-muted" />
<Link
href="https://dokploy.com"
className="relative z-20 flex items-center text-lg font-medium gap-4"
className="relative z-20 flex items-center text-lg font-medium gap-4 text-primary"
>
<Logo className="size-10" />
Dokploy

View File

@@ -67,7 +67,7 @@ ChartContainer.displayName = "Chart";
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(
([_, config]) => config.theme || config.color,
([, config]) => config.theme || config.color,
);
if (!colorConfig.length) {