Removed containers page

This commit is contained in:
2026-01-02 12:41:56 +01:00
parent 767afd9fef
commit be750824c5
2 changed files with 1 additions and 275 deletions
-270
View File
@@ -1,270 +0,0 @@
"use client";
import { DashboardLayout } from "@/components/dashboard/DashboardLayout";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { MoreVertical, Play, Square, Container } from "lucide-react";
import { useEffect, useState } from "react";
import { getDocker, removeContainer } from "@/core/docker";
import { getNodes } from "@/core/node";
import { ContainerInfo } from "dockerode";
import { formatBytes } from "@/lib/format";
import {
getContainerStats,
startContainer,
stopContainer,
} from "@/core/docker";
import RemoveDialog from "@/components/dialogs/Remove";
type ContainerSummary = {
id: string;
name: string;
image: string;
node: string;
status: "running" | "stopped";
uptime: string;
cpu: number;
memory: number;
};
async function summarizeContainer(
container: ContainerInfo,
nodeName: string
): Promise<ContainerSummary> {
// 1. Shorten the long container ID
const shortId = container.Id;
// 2. Clean the name
const name = container.Names[0]?.replace(/^\//, "") ?? "unknown";
// 3. Normalize status
const status = container.State === "running" ? "running" : "stopped";
// 4. Compute uptime from Status string (if available)
const uptime = container.Status?.replace(/^Up\s*/, "") ?? "unknown";
const stats = await getContainerStats(container.Id);
return {
id: shortId,
name,
image: container.Image,
node: nodeName,
status,
uptime,
cpu: stats?.cpu || 0,
memory: stats?.memory || 0,
};
}
export default function ContainersPage() {
const [containers, setContainers] = useState<ContainerSummary[]>([]);
useEffect(() => {
const fetchNodes = async () => {
const nodes = await getNodes();
const dockerNodes = await getDocker(nodes);
setContainers(
await Promise.all(
nodes.flatMap((node, i) =>
dockerNodes[i].containers.map((c) =>
summarizeContainer(c, node.name)
)
)
)
);
};
fetchNodes();
const iid = setInterval(fetchNodes, 3000);
return () => clearInterval(iid);
}, []);
return (
<DashboardLayout
title="Containers"
subtitle="View and manage container instances"
>
{/* Stats */}
<div className="flex flex-wrap gap-2 mb-6">
<Badge>{containers.length} containers</Badge>
<Badge>
{containers.filter((c) => c.status === "running").length} running
</Badge>
</div>
<Card>
<CardContent className="p-0">
{/* ================= DESKTOP TABLE ================= */}
<div className="hidden xl:block">
<table className="w-full">
<thead>
<tr className="border-b border-border">
{[
"Container",
"Image",
"Node",
"Status",
"Uptime",
"CPU",
"Memory",
"Actions",
].map((h) => (
<th
key={h}
className={`text-left text-xs text-muted-foreground uppercase tracking-wider py-3 px-4 ${
h === "Actions" ? "text-right" : ""
}`}
>
{h}
</th>
))}
</tr>
</thead>
<tbody>
{containers.map((container) => (
<tr key={container.id} className="data-table-row">
<td className="py-3 px-4">
<div className="flex items-center gap-2">
<Container className="w-4 h-4 text-muted-foreground" />
<div>
<div className="font-mono text-sm">
{container.name}
</div>
<div className="text-[10px] text-muted-foreground font-mono">
{container.id.slice(0, 12)}
</div>
</div>
</div>
</td>
<td className="py-3 px-4 font-mono text-xs text-muted-foreground">
{container.image}
</td>
<td className="py-3 px-4">{container.node}</td>
<td className="py-3 px-4">{container.status}</td>
<td className="py-3 px-4 font-mono text-xs">
{container.uptime}
</td>
<td className="py-3 px-4 font-mono text-xs">
{container.cpu.toFixed(1)}%
</td>
<td className="py-3 px-4 font-mono text-xs">
{container.memory.toFixed(1)}%
</td>
<td className="py-3 px-4">
<div className="flex justify-end gap-1">
{container.status === "running" ? (
<Button
variant="ghost"
size="icon-sm"
onClick={() => stopContainer(container.id)}
>
<Square className="w-3 h-3" />
</Button>
) : (
<Button
variant="ghost"
size="icon-sm"
onClick={() => startContainer(container.id)}
>
<Play className="w-3 h-3" />
</Button>
)}
<RemoveDialog
remove={() => removeContainer(container.id)}
/>
<Button variant="ghost" size="icon-sm">
<MoreVertical className="w-3 h-3" />
</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* ================= MOBILE CARDS ================= */}
<div className="xl:hidden divide-y divide-border">
{containers.map((container) => (
<div key={container.id} className="p-4 space-y-3">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Container className="w-4 h-4 text-muted-foreground" />
<span className="font-mono text-sm">{container.name}</span>
</div>
{container.status}
</div>
<div className="text-[10px] font-mono text-muted-foreground">
{container.id}
</div>
<div className="text-xs font-mono text-muted-foreground truncate">
{container.image}
</div>
<div className="grid grid-cols-2 gap-2 text-xs">
<div>
<span className="text-muted-foreground">Node</span>
<div>{container.node}</div>
</div>
<div>
<span className="text-muted-foreground">Uptime</span>
<div className="font-mono">{container.uptime}</div>
</div>
<div>
<span className="text-muted-foreground">CPU</span>
<div className="font-mono">{container.cpu.toFixed(1)}%</div>
</div>
<div>
<span className="text-muted-foreground">Memory</span>
<div className="font-mono">
{container.memory.toFixed(1)}%
</div>
</div>
</div>
<div className="flex justify-end gap-1 pt-2">
{container.status === "running" ? (
<Button
variant="ghost"
size="icon-sm"
onClick={() => stopContainer(container.id)}
>
<Square className="w-3 h-3" />
</Button>
) : (
<Button
variant="ghost"
size="icon-sm"
onClick={() => startContainer(container.id)}
>
<Play className="w-3 h-3" />
</Button>
)}
<RemoveDialog remove={() => removeContainer(container.id)} />
<Button variant="ghost" size="icon-sm">
<MoreVertical className="w-3 h-3" />
</Button>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</DashboardLayout>
);
}
@@ -8,8 +8,6 @@ import {
FileText, FileText,
Settings, Settings,
Activity, Activity,
Container,
Command,
} from "lucide-react"; } from "lucide-react";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import Link from "next/link"; import Link from "next/link";
@@ -25,7 +23,6 @@ import {
SidebarMenuButton, SidebarMenuButton,
SidebarMenuItem, SidebarMenuItem,
SidebarRail, SidebarRail,
useSidebar,
} from "@/components/ui/sidebar"; } from "@/components/ui/sidebar";
import Image from "next/image"; import Image from "next/image";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@@ -34,9 +31,8 @@ import { version } from "../../../package.json";
const navigation = [ const navigation = [
{ name: "Overview", href: "/overview", icon: LayoutGrid }, { name: "Overview", href: "/overview", icon: LayoutGrid },
{ name: "Applications", href: "/applications", icon: Box }, { name: "Applications", href: "/applications", icon: Box },
{ name: "Containers", href: "/containers", icon: Container },
{ name: "Nodes", href: "/nodes", icon: Server },
{ name: "Deployments", href: "/deployments", icon: Rocket }, { name: "Deployments", href: "/deployments", icon: Rocket },
{ name: "Nodes", href: "/nodes", icon: Server },
{ name: "Logs", href: "/logs", icon: FileText }, { name: "Logs", href: "/logs", icon: FileText },
{ name: "Environment", href: "/environment", icon: Settings }, { name: "Environment", href: "/environment", icon: Settings },
{ name: "Monitoring", href: "/monitoring", icon: Activity }, { name: "Monitoring", href: "/monitoring", icon: Activity },