mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-15 20:25:23 +02:00
Merge pull request #4142 from Dokploy/2267-add-a-disk-space-pie-chart
feat(dashboard): enhance monitoring charts with new Docker disk usage…
This commit is contained in:
@@ -1,103 +1,103 @@
|
|||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
|
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts";
|
||||||
import {
|
import {
|
||||||
Area,
|
type ChartConfig,
|
||||||
AreaChart,
|
ChartContainer,
|
||||||
CartesianGrid,
|
ChartLegend,
|
||||||
Legend,
|
ChartLegendContent,
|
||||||
ResponsiveContainer,
|
ChartTooltip,
|
||||||
Tooltip,
|
ChartTooltipContent,
|
||||||
YAxis,
|
} from "@/components/ui/chart";
|
||||||
} from "recharts";
|
|
||||||
import type { DockerStatsJSON } from "./show-free-container-monitoring";
|
import type { DockerStatsJSON } from "./show-free-container-monitoring";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
accumulativeData: DockerStatsJSON["block"];
|
accumulativeData: DockerStatsJSON["block"];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const chartConfig = {
|
||||||
|
readMb: {
|
||||||
|
label: "Read (MB)",
|
||||||
|
color: "hsl(var(--chart-1))",
|
||||||
|
},
|
||||||
|
writeMb: {
|
||||||
|
label: "Write (MB)",
|
||||||
|
color: "hsl(var(--chart-2))",
|
||||||
|
},
|
||||||
|
} satisfies ChartConfig;
|
||||||
|
|
||||||
export const DockerBlockChart = ({ accumulativeData }: Props) => {
|
export const DockerBlockChart = ({ accumulativeData }: Props) => {
|
||||||
const transformedData = accumulativeData.map((item, index) => {
|
const transformedData = accumulativeData.map((item, index) => ({
|
||||||
return {
|
time: item.time,
|
||||||
time: item.time,
|
name: `Point ${index + 1}`,
|
||||||
name: `Point ${index + 1}`,
|
readMb: item.value.readMb,
|
||||||
readMb: item.value.readMb,
|
writeMb: item.value.writeMb,
|
||||||
writeMb: item.value.writeMb,
|
}));
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-6 w-full h-[10rem]">
|
<ChartContainer config={chartConfig} className="mt-4 h-[10rem] w-full">
|
||||||
<ResponsiveContainer>
|
<AreaChart
|
||||||
<AreaChart
|
data={transformedData}
|
||||||
data={transformedData}
|
margin={{ top: 10, right: 10, left: 0, bottom: 0 }}
|
||||||
margin={{
|
>
|
||||||
top: 10,
|
<defs>
|
||||||
right: 30,
|
<linearGradient id="fillBlockRead" x1="0" y1="0" x2="0" y2="1">
|
||||||
left: 0,
|
<stop
|
||||||
bottom: 0,
|
offset="5%"
|
||||||
}}
|
stopColor="var(--color-readMb)"
|
||||||
>
|
stopOpacity={0.8}
|
||||||
<defs>
|
/>
|
||||||
<linearGradient id="colorUv" x1="0" y1="0" x2="0" y2="1">
|
<stop
|
||||||
<stop offset="5%" stopColor="#27272A" stopOpacity={0.8} />
|
offset="95%"
|
||||||
<stop offset="95%" stopColor="#8884d8" stopOpacity={0} />
|
stopColor="var(--color-readMb)"
|
||||||
</linearGradient>
|
stopOpacity={0.1}
|
||||||
<linearGradient id="colorWrite" x1="0" y1="0" x2="0" y2="1">
|
/>
|
||||||
<stop offset="5%" stopColor="#82ca9d" stopOpacity={0.8} />
|
</linearGradient>
|
||||||
<stop offset="95%" stopColor="#82ca9d" stopOpacity={0} />
|
<linearGradient id="fillBlockWrite" x1="0" y1="0" x2="0" y2="1">
|
||||||
</linearGradient>
|
<stop
|
||||||
</defs>
|
offset="5%"
|
||||||
<YAxis stroke="#A1A1AA" />
|
stopColor="var(--color-writeMb)"
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="#27272A" />
|
stopOpacity={0.8}
|
||||||
{/* @ts-ignore */}
|
/>
|
||||||
<Tooltip content={<CustomTooltip />} />
|
<stop
|
||||||
<Legend />
|
offset="95%"
|
||||||
<Area
|
stopColor="var(--color-writeMb)"
|
||||||
type="monotone"
|
stopOpacity={0.1}
|
||||||
dataKey="readMb"
|
/>
|
||||||
stroke="#27272A"
|
</linearGradient>
|
||||||
fillOpacity={1}
|
</defs>
|
||||||
fill="url(#colorUv)"
|
<CartesianGrid vertical={false} />
|
||||||
name="Read Mb"
|
<YAxis tickLine={false} axisLine={false} />
|
||||||
/>
|
<ChartTooltip
|
||||||
<Area
|
cursor={false}
|
||||||
type="monotone"
|
content={
|
||||||
dataKey="writeMb"
|
<ChartTooltipContent
|
||||||
stroke="#82ca9d"
|
labelFormatter={(_, payload) => {
|
||||||
fillOpacity={1}
|
const time = payload?.[0]?.payload?.time;
|
||||||
fill="url(#colorWrite)"
|
return time ? format(new Date(time), "PPpp") : "";
|
||||||
name="Write Mb"
|
}}
|
||||||
/>
|
formatter={(value, name) => {
|
||||||
</AreaChart>
|
const label = name === "readMb" ? "Read" : "Write";
|
||||||
</ResponsiveContainer>
|
return [`${value} MB`, label];
|
||||||
</div>
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Area
|
||||||
|
type="monotone"
|
||||||
|
dataKey="readMb"
|
||||||
|
stroke="var(--color-readMb)"
|
||||||
|
fill="url(#fillBlockRead)"
|
||||||
|
strokeWidth={2}
|
||||||
|
/>
|
||||||
|
<Area
|
||||||
|
type="monotone"
|
||||||
|
dataKey="writeMb"
|
||||||
|
stroke="var(--color-writeMb)"
|
||||||
|
fill="url(#fillBlockWrite)"
|
||||||
|
strokeWidth={2}
|
||||||
|
/>
|
||||||
|
<ChartLegend content={<ChartLegendContent />} />
|
||||||
|
</AreaChart>
|
||||||
|
</ChartContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
interface CustomTooltipProps {
|
|
||||||
active: boolean;
|
|
||||||
payload?: {
|
|
||||||
color?: string;
|
|
||||||
dataKey?: string;
|
|
||||||
value?: number;
|
|
||||||
payload: {
|
|
||||||
time: string;
|
|
||||||
readMb: number;
|
|
||||||
writeMb: number;
|
|
||||||
};
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const CustomTooltip = ({ active, payload }: CustomTooltipProps) => {
|
|
||||||
if (active && payload && payload.length && payload[0]) {
|
|
||||||
return (
|
|
||||||
<div className="custom-tooltip bg-background p-2 shadow-lg rounded-md text-primary border">
|
|
||||||
{payload[0].payload.time && (
|
|
||||||
<p>{`Date: ${format(new Date(payload[0].payload.time), "PPpp")}`}</p>
|
|
||||||
)}
|
|
||||||
<p>{`Read ${payload[0].payload.readMb} `}</p>
|
|
||||||
<p>{`Write: ${payload[0].payload.writeMb} `}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,87 +1,81 @@
|
|||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
|
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts";
|
||||||
import {
|
import {
|
||||||
Area,
|
type ChartConfig,
|
||||||
AreaChart,
|
ChartContainer,
|
||||||
CartesianGrid,
|
ChartLegend,
|
||||||
Legend,
|
ChartLegendContent,
|
||||||
ResponsiveContainer,
|
ChartTooltip,
|
||||||
Tooltip,
|
ChartTooltipContent,
|
||||||
YAxis,
|
} from "@/components/ui/chart";
|
||||||
} from "recharts";
|
|
||||||
import type { DockerStatsJSON } from "./show-free-container-monitoring";
|
import type { DockerStatsJSON } from "./show-free-container-monitoring";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
accumulativeData: DockerStatsJSON["cpu"];
|
accumulativeData: DockerStatsJSON["cpu"];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const chartConfig = {
|
||||||
|
usage: {
|
||||||
|
label: "CPU Usage",
|
||||||
|
color: "hsl(var(--chart-1))",
|
||||||
|
},
|
||||||
|
} satisfies ChartConfig;
|
||||||
|
|
||||||
export const DockerCpuChart = ({ accumulativeData }: Props) => {
|
export const DockerCpuChart = ({ accumulativeData }: Props) => {
|
||||||
const transformedData = accumulativeData.map((item, index) => {
|
const transformedData = accumulativeData.map((item, index) => ({
|
||||||
return {
|
name: `Point ${index + 1}`,
|
||||||
name: `Point ${index + 1}`,
|
time: item.time,
|
||||||
time: item.time,
|
usage: item.value.toString().split("%")[0],
|
||||||
usage: item.value.toString().split("%")[0],
|
}));
|
||||||
};
|
|
||||||
});
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-6 w-full h-[10rem]">
|
<ChartContainer config={chartConfig} className="mt-4 h-[10rem] w-full">
|
||||||
<ResponsiveContainer>
|
<AreaChart
|
||||||
<AreaChart
|
data={transformedData}
|
||||||
data={transformedData}
|
margin={{ top: 10, right: 10, left: 0, bottom: 0 }}
|
||||||
margin={{
|
>
|
||||||
top: 10,
|
<defs>
|
||||||
right: 30,
|
<linearGradient id="fillCpu" x1="0" y1="0" x2="0" y2="1">
|
||||||
left: 0,
|
<stop
|
||||||
bottom: 0,
|
offset="5%"
|
||||||
}}
|
stopColor="var(--color-usage)"
|
||||||
>
|
stopOpacity={0.8}
|
||||||
<defs>
|
/>
|
||||||
<linearGradient id="colorUv" x1="0" y1="0" x2="0" y2="1">
|
<stop
|
||||||
<stop offset="5%" stopColor="#27272A" stopOpacity={0.8} />
|
offset="95%"
|
||||||
<stop offset="95%" stopColor="white" stopOpacity={0} />
|
stopColor="var(--color-usage)"
|
||||||
</linearGradient>
|
stopOpacity={0.1}
|
||||||
</defs>
|
/>
|
||||||
<YAxis stroke="#A1A1AA" domain={[0, 100]} />
|
</linearGradient>
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="#27272A" />
|
</defs>
|
||||||
{/* @ts-ignore */}
|
<CartesianGrid vertical={false} />
|
||||||
<Tooltip content={<CustomTooltip />} />
|
<YAxis
|
||||||
<Legend />
|
tickFormatter={(value) => `${value}%`}
|
||||||
<Area
|
domain={[0, 100]}
|
||||||
type="monotone"
|
tickLine={false}
|
||||||
dataKey="usage"
|
axisLine={false}
|
||||||
stroke="#27272A"
|
/>
|
||||||
fillOpacity={1}
|
<ChartTooltip
|
||||||
fill="url(#colorUv)"
|
cursor={false}
|
||||||
/>
|
content={
|
||||||
</AreaChart>
|
<ChartTooltipContent
|
||||||
</ResponsiveContainer>
|
labelFormatter={(_, payload) => {
|
||||||
</div>
|
const time = payload?.[0]?.payload?.time;
|
||||||
|
return time ? format(new Date(time), "PPpp") : "";
|
||||||
|
}}
|
||||||
|
formatter={(value) => [`${value}%`, "CPU Usage"]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Area
|
||||||
|
type="monotone"
|
||||||
|
dataKey="usage"
|
||||||
|
stroke="var(--color-usage)"
|
||||||
|
fill="url(#fillCpu)"
|
||||||
|
strokeWidth={2}
|
||||||
|
/>
|
||||||
|
<ChartLegend content={<ChartLegendContent />} />
|
||||||
|
</AreaChart>
|
||||||
|
</ChartContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface CustomTooltipProps {
|
|
||||||
active: boolean;
|
|
||||||
payload?: {
|
|
||||||
color?: string;
|
|
||||||
dataKey?: string;
|
|
||||||
value?: number;
|
|
||||||
payload: {
|
|
||||||
time: string;
|
|
||||||
usage: number;
|
|
||||||
};
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const CustomTooltip = ({ active, payload }: CustomTooltipProps) => {
|
|
||||||
if (active && payload && payload.length && payload[0]) {
|
|
||||||
return (
|
|
||||||
<div className="custom-tooltip bg-background p-2 shadow-lg rounded-md text-primary border">
|
|
||||||
{payload[0].payload.time && (
|
|
||||||
<p>{`Date: ${format(new Date(payload[0].payload.time), "PPpp")}`}</p>
|
|
||||||
)}
|
|
||||||
<p>{`CPU Usage: ${payload[0].payload.usage}%`}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
|
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts";
|
||||||
import {
|
import {
|
||||||
Area,
|
type ChartConfig,
|
||||||
AreaChart,
|
ChartContainer,
|
||||||
CartesianGrid,
|
ChartLegend,
|
||||||
Legend,
|
ChartLegendContent,
|
||||||
ResponsiveContainer,
|
ChartTooltip,
|
||||||
Tooltip,
|
ChartTooltipContent,
|
||||||
YAxis,
|
} from "@/components/ui/chart";
|
||||||
} from "recharts";
|
|
||||||
import type { DockerStatsJSON } from "./show-free-container-monitoring";
|
import type { DockerStatsJSON } from "./show-free-container-monitoring";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -15,91 +15,96 @@ interface Props {
|
|||||||
diskTotal: number;
|
diskTotal: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const chartConfig = {
|
||||||
|
usedGb: {
|
||||||
|
label: "Used (GB)",
|
||||||
|
color: "hsl(var(--chart-3))",
|
||||||
|
},
|
||||||
|
freeGb: {
|
||||||
|
label: "Free (GB)",
|
||||||
|
color: "hsl(var(--chart-4))",
|
||||||
|
},
|
||||||
|
} satisfies ChartConfig;
|
||||||
|
|
||||||
export const DockerDiskChart = ({ accumulativeData, diskTotal }: Props) => {
|
export const DockerDiskChart = ({ accumulativeData, diskTotal }: Props) => {
|
||||||
const transformedData = accumulativeData.map((item, index) => {
|
const transformedData = accumulativeData.map((item, index) => ({
|
||||||
return {
|
time: item.time,
|
||||||
time: item.time,
|
name: `Point ${index + 1}`,
|
||||||
name: `Point ${index + 1}`,
|
usedGb: +item.value.diskUsage,
|
||||||
usedGb: +item.value.diskUsage,
|
totalGb: +item.value.diskTotal,
|
||||||
totalGb: +item.value.diskTotal,
|
freeGb: item.value.diskFree,
|
||||||
freeGb: item.value.diskFree,
|
}));
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-6 w-full h-[10rem]">
|
<ChartContainer config={chartConfig} className="mt-4 h-[10rem] w-full">
|
||||||
<ResponsiveContainer>
|
<AreaChart
|
||||||
<AreaChart
|
data={transformedData}
|
||||||
data={transformedData}
|
margin={{ top: 10, right: 10, left: 0, bottom: 0 }}
|
||||||
margin={{
|
>
|
||||||
top: 10,
|
<defs>
|
||||||
right: 30,
|
<linearGradient id="fillDiskUsed" x1="0" y1="0" x2="0" y2="1">
|
||||||
left: 0,
|
<stop
|
||||||
bottom: 0,
|
offset="5%"
|
||||||
}}
|
stopColor="var(--color-usedGb)"
|
||||||
>
|
stopOpacity={0.8}
|
||||||
<defs>
|
/>
|
||||||
<linearGradient id="colorUsed" x1="0" y1="0" x2="0" y2="1">
|
<stop
|
||||||
<stop offset="5%" stopColor="#6C28D9" stopOpacity={0.8} />
|
offset="95%"
|
||||||
<stop offset="95%" stopColor="#6C28D9" stopOpacity={0} />
|
stopColor="var(--color-usedGb)"
|
||||||
</linearGradient>
|
stopOpacity={0.1}
|
||||||
<linearGradient id="colorFree" x1="0" y1="0" x2="0" y2="1">
|
/>
|
||||||
<stop offset="5%" stopColor="#6C28D9" stopOpacity={0.2} />
|
</linearGradient>
|
||||||
<stop offset="95%" stopColor="#6C28D9" stopOpacity={0} />
|
<linearGradient id="fillDiskFree" x1="0" y1="0" x2="0" y2="1">
|
||||||
</linearGradient>
|
<stop
|
||||||
</defs>
|
offset="5%"
|
||||||
<YAxis stroke="#A1A1AA" domain={[0, diskTotal]} />
|
stopColor="var(--color-freeGb)"
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="#27272A" />
|
stopOpacity={0.4}
|
||||||
{/* @ts-ignore */}
|
/>
|
||||||
<Tooltip content={<CustomTooltip />} />
|
<stop
|
||||||
<Legend />
|
offset="95%"
|
||||||
<Area
|
stopColor="var(--color-freeGb)"
|
||||||
type="monotone"
|
stopOpacity={0.1}
|
||||||
dataKey="usedGb"
|
/>
|
||||||
stroke="#6C28D9"
|
</linearGradient>
|
||||||
fillOpacity={1}
|
</defs>
|
||||||
fill="url(#colorUsed)"
|
<CartesianGrid vertical={false} />
|
||||||
name="Used GB"
|
<YAxis
|
||||||
/>
|
domain={[0, diskTotal]}
|
||||||
<Area
|
tickLine={false}
|
||||||
type="monotone"
|
axisLine={false}
|
||||||
dataKey="freeGb"
|
tickFormatter={(value) => `${value} GB`}
|
||||||
stroke="#8884d8"
|
/>
|
||||||
fillOpacity={1}
|
<ChartTooltip
|
||||||
fill="url(#colorFree)"
|
cursor={false}
|
||||||
name="Free GB"
|
content={
|
||||||
/>
|
<ChartTooltipContent
|
||||||
</AreaChart>
|
labelFormatter={(_, payload) => {
|
||||||
</ResponsiveContainer>
|
const time = payload?.[0]?.payload?.time;
|
||||||
</div>
|
return time ? format(new Date(time), "PPpp") : "";
|
||||||
|
}}
|
||||||
|
formatter={(value, name) => {
|
||||||
|
const label = name === "usedGb" ? "Used" : "Free";
|
||||||
|
return [`${value} GB`, label];
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Area
|
||||||
|
type="monotone"
|
||||||
|
dataKey="usedGb"
|
||||||
|
stroke="var(--color-usedGb)"
|
||||||
|
fill="url(#fillDiskUsed)"
|
||||||
|
strokeWidth={2}
|
||||||
|
/>
|
||||||
|
<Area
|
||||||
|
type="monotone"
|
||||||
|
dataKey="freeGb"
|
||||||
|
stroke="var(--color-freeGb)"
|
||||||
|
fill="url(#fillDiskFree)"
|
||||||
|
strokeWidth={2}
|
||||||
|
/>
|
||||||
|
<ChartLegend content={<ChartLegendContent />} />
|
||||||
|
</AreaChart>
|
||||||
|
</ChartContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
interface CustomTooltipProps {
|
|
||||||
active: boolean;
|
|
||||||
payload?: {
|
|
||||||
color?: string;
|
|
||||||
dataKey?: string;
|
|
||||||
value?: number;
|
|
||||||
payload: {
|
|
||||||
time: string;
|
|
||||||
usedGb: number;
|
|
||||||
freeGb: number;
|
|
||||||
totalGb: number;
|
|
||||||
};
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const CustomTooltip = ({ active, payload }: CustomTooltipProps) => {
|
|
||||||
if (active && payload && payload.length && payload[0]) {
|
|
||||||
return (
|
|
||||||
<div className="custom-tooltip bg-background p-2 shadow-lg rounded-md text-primary border">
|
|
||||||
<p>{`Date: ${format(new Date(payload[0].payload.time), "PPpp")}`}</p>
|
|
||||||
<p>{`Disk usage: ${payload[0].payload.usedGb} GB`}</p>
|
|
||||||
<p>{`Disk free: ${payload[0].payload.freeGb} GB`}</p>
|
|
||||||
<p>{`Total disk: ${payload[0].payload.totalGb} GB`}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
import { Loader2, RefreshCw } from "lucide-react";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { Cell, Label, Pie, PieChart } from "recharts";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
type ChartConfig,
|
||||||
|
ChartContainer,
|
||||||
|
ChartLegend,
|
||||||
|
ChartLegendContent,
|
||||||
|
ChartTooltip,
|
||||||
|
ChartTooltipContent,
|
||||||
|
} from "@/components/ui/chart";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
|
const TYPE_TO_KEY: Record<string, string> = {
|
||||||
|
Images: "images",
|
||||||
|
Containers: "containers",
|
||||||
|
"Local Volumes": "volumes",
|
||||||
|
"Build Cache": "buildCache",
|
||||||
|
};
|
||||||
|
|
||||||
|
const chartConfig = {
|
||||||
|
value: {
|
||||||
|
label: "Size",
|
||||||
|
},
|
||||||
|
images: {
|
||||||
|
label: "Images",
|
||||||
|
color: "hsl(var(--chart-1))",
|
||||||
|
},
|
||||||
|
containers: {
|
||||||
|
label: "Containers",
|
||||||
|
color: "hsl(var(--chart-2))",
|
||||||
|
},
|
||||||
|
volumes: {
|
||||||
|
label: "Volumes",
|
||||||
|
color: "hsl(var(--chart-3))",
|
||||||
|
},
|
||||||
|
buildCache: {
|
||||||
|
label: "Build Cache",
|
||||||
|
color: "hsl(var(--chart-4))",
|
||||||
|
},
|
||||||
|
} satisfies ChartConfig;
|
||||||
|
|
||||||
|
const formatSize = (bytes: number): string => {
|
||||||
|
if (bytes >= 1024 ** 3) return `${(bytes / 1024 ** 3).toFixed(2)} GB`;
|
||||||
|
if (bytes >= 1024 ** 2) return `${(bytes / 1024 ** 2).toFixed(1)} MB`;
|
||||||
|
if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||||
|
return `${bytes} B`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DockerDiskUsageChart = () => {
|
||||||
|
const { data, isLoading, refetch, isRefetching } =
|
||||||
|
api.settings.getDockerDiskUsage.useQuery(undefined, {
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { chartData, totalBytes } = useMemo(() => {
|
||||||
|
const items =
|
||||||
|
data
|
||||||
|
?.filter((item) => item.sizeBytes > 0)
|
||||||
|
.map((item) => {
|
||||||
|
const key = TYPE_TO_KEY[item.type] ?? item.type;
|
||||||
|
return {
|
||||||
|
name: key,
|
||||||
|
value: item.sizeBytes,
|
||||||
|
size: item.size,
|
||||||
|
active: item.active,
|
||||||
|
totalCount: item.totalCount,
|
||||||
|
reclaimable: item.reclaimable,
|
||||||
|
fill: `var(--color-${key})`,
|
||||||
|
};
|
||||||
|
}) ?? [];
|
||||||
|
return {
|
||||||
|
chartData: items,
|
||||||
|
totalBytes: items.reduce((sum, item) => sum + item.value, 0),
|
||||||
|
};
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-[16rem]">
|
||||||
|
<Loader2 className="size-5 animate-spin text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chartData.length === 0) {
|
||||||
|
return (
|
||||||
|
<p className="text-xs text-muted-foreground mt-4">
|
||||||
|
No Docker disk usage data available.
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2 w-full">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
Total: {formatSize(totalBytes)}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-7 w-7"
|
||||||
|
onClick={() => refetch()}
|
||||||
|
disabled={isRefetching}
|
||||||
|
>
|
||||||
|
<RefreshCw
|
||||||
|
className={`size-3.5 ${isRefetching ? "animate-spin" : ""}`}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<ChartContainer
|
||||||
|
config={chartConfig}
|
||||||
|
className="mx-auto w-full max-h-[250px] [&_.recharts-pie-label-text]:fill-foreground"
|
||||||
|
>
|
||||||
|
<PieChart>
|
||||||
|
<ChartTooltip
|
||||||
|
content={
|
||||||
|
<ChartTooltipContent
|
||||||
|
nameKey="name"
|
||||||
|
formatter={(value, name) => {
|
||||||
|
const item = chartData.find((d) => d.name === name);
|
||||||
|
if (!item) return [formatSize(value as number), name];
|
||||||
|
return [
|
||||||
|
`${item.size} — ${item.active} active / ${item.totalCount} total — Reclaimable: ${item.reclaimable}`,
|
||||||
|
chartConfig[name as keyof typeof chartConfig]?.label ??
|
||||||
|
name,
|
||||||
|
];
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Pie
|
||||||
|
data={chartData}
|
||||||
|
dataKey="value"
|
||||||
|
nameKey="name"
|
||||||
|
innerRadius={60}
|
||||||
|
outerRadius={85}
|
||||||
|
strokeWidth={3}
|
||||||
|
stroke="hsl(var(--background))"
|
||||||
|
minAngle={15}
|
||||||
|
>
|
||||||
|
{chartData.map((entry) => (
|
||||||
|
<Cell key={entry.name} fill={entry.fill} />
|
||||||
|
))}
|
||||||
|
<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 || 0) - 8}
|
||||||
|
className="fill-foreground text-2xl font-bold"
|
||||||
|
>
|
||||||
|
{formatSize(totalBytes)}
|
||||||
|
</tspan>
|
||||||
|
<tspan
|
||||||
|
x={viewBox.cx}
|
||||||
|
y={(viewBox.cy || 0) + 14}
|
||||||
|
className="fill-muted-foreground text-xs"
|
||||||
|
>
|
||||||
|
Docker Usage
|
||||||
|
</tspan>
|
||||||
|
</text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Pie>
|
||||||
|
<ChartLegend content={<ChartLegendContent nameKey="name" />} />
|
||||||
|
</PieChart>
|
||||||
|
</ChartContainer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
|
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts";
|
||||||
import {
|
import {
|
||||||
Area,
|
type ChartConfig,
|
||||||
AreaChart,
|
ChartContainer,
|
||||||
CartesianGrid,
|
ChartLegend,
|
||||||
Legend,
|
ChartLegendContent,
|
||||||
ResponsiveContainer,
|
ChartTooltip,
|
||||||
Tooltip,
|
ChartTooltipContent,
|
||||||
YAxis,
|
} from "@/components/ui/chart";
|
||||||
} from "recharts";
|
|
||||||
import type { DockerStatsJSON } from "./show-free-container-monitoring";
|
import type { DockerStatsJSON } from "./show-free-container-monitoring";
|
||||||
import { convertMemoryToBytes } from "./show-free-container-monitoring";
|
import { convertMemoryToBytes } from "./show-free-container-monitoring";
|
||||||
|
|
||||||
@@ -16,78 +16,72 @@ interface Props {
|
|||||||
memoryLimitGB: number;
|
memoryLimitGB: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const chartConfig = {
|
||||||
|
usage: {
|
||||||
|
label: "Memory (GB)",
|
||||||
|
color: "hsl(var(--chart-2))",
|
||||||
|
},
|
||||||
|
} satisfies ChartConfig;
|
||||||
|
|
||||||
export const DockerMemoryChart = ({
|
export const DockerMemoryChart = ({
|
||||||
accumulativeData,
|
accumulativeData,
|
||||||
memoryLimitGB,
|
memoryLimitGB,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const transformedData = accumulativeData.map((item, index) => {
|
const transformedData = accumulativeData.map((item, index) => ({
|
||||||
return {
|
time: item.time,
|
||||||
time: item.time,
|
name: `Point ${index + 1}`,
|
||||||
name: `Point ${index + 1}`,
|
// @ts-ignore
|
||||||
// @ts-ignore
|
usage: (convertMemoryToBytes(item.value.used) / 1024 ** 3).toFixed(2),
|
||||||
usage: (convertMemoryToBytes(item.value.used) / 1024 ** 3).toFixed(2),
|
}));
|
||||||
};
|
|
||||||
});
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-6 w-full h-[10rem]">
|
<ChartContainer config={chartConfig} className="mt-4 h-[10rem] w-full">
|
||||||
<ResponsiveContainer>
|
<AreaChart
|
||||||
<AreaChart
|
data={transformedData}
|
||||||
data={transformedData}
|
margin={{ top: 10, right: 10, left: 0, bottom: 0 }}
|
||||||
margin={{
|
>
|
||||||
top: 10,
|
<defs>
|
||||||
right: 30,
|
<linearGradient id="fillMemory" x1="0" y1="0" x2="0" y2="1">
|
||||||
left: 0,
|
<stop
|
||||||
bottom: 0,
|
offset="5%"
|
||||||
}}
|
stopColor="var(--color-usage)"
|
||||||
>
|
stopOpacity={0.8}
|
||||||
<defs>
|
/>
|
||||||
<linearGradient id="colorUv" x1="0" y1="0" x2="0" y2="1">
|
<stop
|
||||||
<stop offset="5%" stopColor="#27272A" stopOpacity={0.8} />
|
offset="95%"
|
||||||
<stop offset="95%" stopColor="white" stopOpacity={0} />
|
stopColor="var(--color-usage)"
|
||||||
</linearGradient>
|
stopOpacity={0.1}
|
||||||
</defs>
|
/>
|
||||||
<YAxis stroke="#A1A1AA" domain={[0, +memoryLimitGB.toFixed(2)]} />
|
</linearGradient>
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="#27272A" />
|
</defs>
|
||||||
{/* @ts-ignore */}
|
<CartesianGrid vertical={false} />
|
||||||
<Tooltip content={<CustomTooltip />} />
|
<YAxis
|
||||||
<Legend />
|
tickFormatter={(value) => `${value} GB`}
|
||||||
<Area
|
domain={[0, +memoryLimitGB.toFixed(2)]}
|
||||||
type="monotone"
|
tickLine={false}
|
||||||
dataKey="usage"
|
axisLine={false}
|
||||||
stroke="#27272A"
|
/>
|
||||||
fillOpacity={1}
|
<ChartTooltip
|
||||||
fill="url(#colorUv)"
|
cursor={false}
|
||||||
/>
|
content={
|
||||||
</AreaChart>
|
<ChartTooltipContent
|
||||||
</ResponsiveContainer>
|
labelFormatter={(_, payload) => {
|
||||||
</div>
|
const time = payload?.[0]?.payload?.time;
|
||||||
|
return time ? format(new Date(time), "PPpp") : "";
|
||||||
|
}}
|
||||||
|
formatter={(value) => [`${value} GB`, "Memory"]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Area
|
||||||
|
type="monotone"
|
||||||
|
dataKey="usage"
|
||||||
|
stroke="var(--color-usage)"
|
||||||
|
fill="url(#fillMemory)"
|
||||||
|
strokeWidth={2}
|
||||||
|
/>
|
||||||
|
<ChartLegend content={<ChartLegendContent />} />
|
||||||
|
</AreaChart>
|
||||||
|
</ChartContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
interface CustomTooltipProps {
|
|
||||||
active: boolean;
|
|
||||||
payload?: {
|
|
||||||
color?: string;
|
|
||||||
dataKey?: string;
|
|
||||||
value?: number;
|
|
||||||
payload: {
|
|
||||||
time: string;
|
|
||||||
usage: number;
|
|
||||||
};
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const CustomTooltip = ({ active, payload }: CustomTooltipProps) => {
|
|
||||||
if (active && payload && payload.length && payload[0] && payload[0].payload) {
|
|
||||||
return (
|
|
||||||
<div className="custom-tooltip bg-background p-2 shadow-lg rounded-md text-primary border">
|
|
||||||
{payload[0].payload.time && (
|
|
||||||
<p>{`Date: ${format(new Date(payload[0].payload.time), "PPpp")}`}</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<p>{`Memory usage: ${payload[0].payload.usage} GB`}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,99 +1,99 @@
|
|||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
|
import { Area, AreaChart, CartesianGrid, YAxis } from "recharts";
|
||||||
import {
|
import {
|
||||||
Area,
|
type ChartConfig,
|
||||||
AreaChart,
|
ChartContainer,
|
||||||
CartesianGrid,
|
ChartLegend,
|
||||||
Legend,
|
ChartLegendContent,
|
||||||
ResponsiveContainer,
|
ChartTooltip,
|
||||||
Tooltip,
|
ChartTooltipContent,
|
||||||
YAxis,
|
} from "@/components/ui/chart";
|
||||||
} from "recharts";
|
|
||||||
import type { DockerStatsJSON } from "./show-free-container-monitoring";
|
import type { DockerStatsJSON } from "./show-free-container-monitoring";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
accumulativeData: DockerStatsJSON["network"];
|
accumulativeData: DockerStatsJSON["network"];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const chartConfig = {
|
||||||
|
inMB: {
|
||||||
|
label: "In (MB)",
|
||||||
|
color: "hsl(var(--chart-1))",
|
||||||
|
},
|
||||||
|
outMB: {
|
||||||
|
label: "Out (MB)",
|
||||||
|
color: "hsl(var(--chart-2))",
|
||||||
|
},
|
||||||
|
} satisfies ChartConfig;
|
||||||
|
|
||||||
export const DockerNetworkChart = ({ accumulativeData }: Props) => {
|
export const DockerNetworkChart = ({ accumulativeData }: Props) => {
|
||||||
const transformedData = accumulativeData.map((item, index) => {
|
const transformedData = accumulativeData.map((item, index) => ({
|
||||||
return {
|
time: item.time,
|
||||||
time: item.time,
|
name: `Point ${index + 1}`,
|
||||||
name: `Point ${index + 1}`,
|
inMB: item.value.inputMb,
|
||||||
inMB: item.value.inputMb,
|
outMB: item.value.outputMb,
|
||||||
outMB: item.value.outputMb,
|
}));
|
||||||
};
|
|
||||||
});
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-6 w-full h-[10rem]">
|
<ChartContainer config={chartConfig} className="mt-4 h-[10rem] w-full">
|
||||||
<ResponsiveContainer>
|
<AreaChart
|
||||||
<AreaChart
|
data={transformedData}
|
||||||
data={transformedData}
|
margin={{ top: 10, right: 10, left: 0, bottom: 0 }}
|
||||||
margin={{
|
>
|
||||||
top: 10,
|
<defs>
|
||||||
right: 30,
|
<linearGradient id="fillNetIn" x1="0" y1="0" x2="0" y2="1">
|
||||||
left: 0,
|
<stop offset="5%" stopColor="var(--color-inMB)" stopOpacity={0.8} />
|
||||||
bottom: 0,
|
<stop
|
||||||
}}
|
offset="95%"
|
||||||
>
|
stopColor="var(--color-inMB)"
|
||||||
<defs>
|
stopOpacity={0.1}
|
||||||
<linearGradient id="colorUv" x1="0" y1="0" x2="0" y2="1">
|
/>
|
||||||
<stop offset="5%" stopColor="#27272A" stopOpacity={0.8} />
|
</linearGradient>
|
||||||
<stop offset="95%" stopColor="white" stopOpacity={0} />
|
<linearGradient id="fillNetOut" x1="0" y1="0" x2="0" y2="1">
|
||||||
</linearGradient>
|
<stop
|
||||||
</defs>
|
offset="5%"
|
||||||
<YAxis stroke="#A1A1AA" />
|
stopColor="var(--color-outMB)"
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke="#27272A" />
|
stopOpacity={0.8}
|
||||||
{/* @ts-ignore */}
|
/>
|
||||||
<Tooltip content={<CustomTooltip />} />
|
<stop
|
||||||
<Legend />
|
offset="95%"
|
||||||
<Area
|
stopColor="var(--color-outMB)"
|
||||||
type="monotone"
|
stopOpacity={0.1}
|
||||||
dataKey="inMB"
|
/>
|
||||||
stroke="#8884d8"
|
</linearGradient>
|
||||||
fillOpacity={1}
|
</defs>
|
||||||
fill="url(#colorUv)"
|
<CartesianGrid vertical={false} />
|
||||||
name="In MB"
|
<YAxis tickLine={false} axisLine={false} />
|
||||||
/>
|
<ChartTooltip
|
||||||
<Area
|
cursor={false}
|
||||||
type="monotone"
|
content={
|
||||||
dataKey="outMB"
|
<ChartTooltipContent
|
||||||
stroke="#82ca9d"
|
labelFormatter={(_, payload) => {
|
||||||
fillOpacity={1}
|
const time = payload?.[0]?.payload?.time;
|
||||||
fill="url(#colorUv)"
|
return time ? format(new Date(time), "PPpp") : "";
|
||||||
name="Out MB"
|
}}
|
||||||
/>
|
formatter={(value, name) => {
|
||||||
</AreaChart>
|
const label = name === "inMB" ? "In" : "Out";
|
||||||
</ResponsiveContainer>
|
return [`${value} MB`, label];
|
||||||
</div>
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Area
|
||||||
|
type="monotone"
|
||||||
|
dataKey="inMB"
|
||||||
|
stroke="var(--color-inMB)"
|
||||||
|
fill="url(#fillNetIn)"
|
||||||
|
strokeWidth={2}
|
||||||
|
/>
|
||||||
|
<Area
|
||||||
|
type="monotone"
|
||||||
|
dataKey="outMB"
|
||||||
|
stroke="var(--color-outMB)"
|
||||||
|
fill="url(#fillNetOut)"
|
||||||
|
strokeWidth={2}
|
||||||
|
/>
|
||||||
|
<ChartLegend content={<ChartLegendContent />} />
|
||||||
|
</AreaChart>
|
||||||
|
</ChartContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface CustomTooltipProps {
|
|
||||||
active: boolean;
|
|
||||||
payload?: {
|
|
||||||
color?: string;
|
|
||||||
dataKey?: string;
|
|
||||||
value?: number;
|
|
||||||
payload: {
|
|
||||||
time: string;
|
|
||||||
inMB: number;
|
|
||||||
outMB: number;
|
|
||||||
};
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const CustomTooltip = ({ active, payload }: CustomTooltipProps) => {
|
|
||||||
if (active && payload && payload.length && payload[0]) {
|
|
||||||
return (
|
|
||||||
<div className="custom-tooltip bg-background p-2 shadow-lg rounded-md text-primary border">
|
|
||||||
{payload[0].payload.time && (
|
|
||||||
<p>{`Date: ${format(new Date(payload[0].payload.time), "PPpp")}`}</p>
|
|
||||||
)}
|
|
||||||
<p>{`In Usage: ${payload[0].payload.inMB} `}</p>
|
|
||||||
<p>{`Out Usage: ${payload[0].payload.outMB} `}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { api } from "@/utils/api";
|
|||||||
import { DockerBlockChart } from "./docker-block-chart";
|
import { DockerBlockChart } from "./docker-block-chart";
|
||||||
import { DockerCpuChart } from "./docker-cpu-chart";
|
import { DockerCpuChart } from "./docker-cpu-chart";
|
||||||
import { DockerDiskChart } from "./docker-disk-chart";
|
import { DockerDiskChart } from "./docker-disk-chart";
|
||||||
|
import { DockerDiskUsageChart } from "./docker-disk-usage-chart";
|
||||||
import { DockerMemoryChart } from "./docker-memory-chart";
|
import { DockerMemoryChart } from "./docker-memory-chart";
|
||||||
import { DockerNetworkChart } from "./docker-network-chart";
|
import { DockerNetworkChart } from "./docker-network-chart";
|
||||||
|
|
||||||
@@ -284,6 +285,18 @@ export const ContainerFreeMonitoring = ({
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</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">
|
||||||
|
Docker Disk Usage
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<DockerDiskUsageChart />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
DEFAULT_UPDATE_DATA,
|
DEFAULT_UPDATE_DATA,
|
||||||
execAsync,
|
execAsync,
|
||||||
findServerById,
|
findServerById,
|
||||||
|
getDockerDiskUsage,
|
||||||
getDokployImageTag,
|
getDokployImageTag,
|
||||||
getLogCleanupStatus,
|
getLogCleanupStatus,
|
||||||
getUpdateData,
|
getUpdateData,
|
||||||
@@ -291,6 +292,12 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
|
getDockerDiskUsage: adminProcedure.query(async () => {
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return getDockerDiskUsage();
|
||||||
|
}),
|
||||||
saveSSHPrivateKey: adminProcedure
|
saveSSHPrivateKey: adminProcedure
|
||||||
.input(apiSaveSSHKey)
|
.input(apiSaveSSHKey)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
|||||||
@@ -259,6 +259,48 @@ export const cleanupSystem = async (serverId?: string) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface DockerDiskUsageItem {
|
||||||
|
type: string;
|
||||||
|
totalCount: number;
|
||||||
|
active: number;
|
||||||
|
size: string;
|
||||||
|
reclaimable: string;
|
||||||
|
sizeBytes: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseSizeToBytes = (size: string): number => {
|
||||||
|
const match = size.match(/^([\d.]+)\s*([KMGT]?B)$/i);
|
||||||
|
if (!match) return 0;
|
||||||
|
const value = Number.parseFloat(match[1] as string);
|
||||||
|
const unit = match[2].toUpperCase();
|
||||||
|
const multipliers: Record<string, number> = {
|
||||||
|
B: 1,
|
||||||
|
KB: 1024,
|
||||||
|
MB: 1024 ** 2,
|
||||||
|
GB: 1024 ** 3,
|
||||||
|
TB: 1024 ** 4,
|
||||||
|
};
|
||||||
|
return value * (multipliers[unit] || 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDockerDiskUsage = async (): Promise<DockerDiskUsageItem[]> => {
|
||||||
|
const command = "docker system df --format '{{json .}}'";
|
||||||
|
const { stdout } = await execAsync(command);
|
||||||
|
|
||||||
|
const lines = stdout.trim().split("\n").filter(Boolean);
|
||||||
|
return lines.map((line) => {
|
||||||
|
const data = JSON.parse(line);
|
||||||
|
return {
|
||||||
|
type: data.Type,
|
||||||
|
totalCount: Number.parseInt(data.TotalCount, 10) || 0,
|
||||||
|
active: Number.parseInt(data.Active, 10) || 0,
|
||||||
|
size: data.Size,
|
||||||
|
reclaimable: data.Reclaimable,
|
||||||
|
sizeBytes: parseSizeToBytes(data.Size),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Volume cleanup should always be performed manually by the user. The reason is that during automatic cleanup, a volume may be deleted due to a stopped container, which is a dangerous situation.
|
* Volume cleanup should always be performed manually by the user. The reason is that during automatic cleanup, a volume may be deleted due to a stopped container, which is a dangerous situation.
|
||||||
*
|
*
|
||||||
|
|||||||
Reference in New Issue
Block a user