feat: Implement docker monitoring dashboard

This commit is contained in:
Tola Leng
2025-06-30 23:05:45 +07:00
parent 691cd4f298
commit 0b029dc914
13 changed files with 1583 additions and 0 deletions
@@ -0,0 +1,23 @@
import { TableCell, TableRow } from "@/components/ui/table";
interface DockerEmptyStateProps {
searchTerm: string;
}
export const DockerEmptyState = ({ searchTerm }: DockerEmptyStateProps) => {
return (
<TableRow>
<TableCell colSpan={8} className="text-center py-12 text-muted-foreground">
<div className="flex flex-col items-center gap-2">
<div className="text-lg font-medium">
{searchTerm ? "No containers found" : "No containers running"}
</div>
<div className="text-sm">
{searchTerm ? "Try adjusting your search terms." : "Start some containers to see them here."}
</div>
</div>
</TableCell>
</TableRow>
);
};
@@ -0,0 +1,73 @@
import { Button } from "@/components/ui/button";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator } from "@/components/ui/dropdown-menu";
import { MoreHorizontal, Eye, Play, Pause, Square, Trash2, BarChart3, RefreshCw } from "lucide-react";
import { DockerContainer } from "@/types/docker.types";
interface DockerRowActionsProps {
container: DockerContainer;
containerStatus: 'running' | 'stopped' | 'warning';
onContainerAction: (action: string, containerId: string, containerName: string) => void;
onViewMetrics: (container: DockerContainer) => void;
}
export const DockerRowActions = ({ container, containerStatus, onContainerAction, onViewMetrics }: DockerRowActionsProps) => {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0 hover:bg-muted">
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48 bg-popover border-border shadow-md">
<DropdownMenuItem
onClick={() => onViewMetrics(container)}
className="cursor-pointer hover:bg-muted"
>
<BarChart3 className="mr-2 h-4 w-4" />
View Metrics
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => onContainerAction('view-detail', container.id, container.name)}
className="cursor-pointer hover:bg-muted"
>
<Eye className="mr-2 h-4 w-4" />
View Details
</DropdownMenuItem>
<DropdownMenuSeparator className="bg-border" />
<DropdownMenuItem
onClick={() => onContainerAction(containerStatus === 'running' ? 'stop' : 'start', container.id, container.name)}
className="cursor-pointer hover:bg-muted"
>
{containerStatus === 'running' ? (
<>
<Square className="mr-2 h-4 w-4" />
Stop Container
</>
) : (
<>
<Play className="mr-2 h-4 w-4" />
Start Container
</>
)}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => onContainerAction('restart', container.id, container.name)}
className="cursor-pointer hover:bg-muted"
>
<RefreshCw className="mr-2 h-4 w-4" />
Restart Container
</DropdownMenuItem>
<DropdownMenuSeparator className="bg-border" />
<DropdownMenuItem
onClick={() => onContainerAction('delete', container.id, container.name)}
className="cursor-pointer hover:bg-muted text-destructive focus:text-destructive"
>
<Trash2 className="mr-2 h-4 w-4" />
Remove Container
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
};
@@ -0,0 +1,19 @@
import { TableHead, TableHeader, TableRow } from "@/components/ui/table";
export const DockerTableHeader = () => {
return (
<TableHeader>
<TableRow className="border-border bg-muted/30">
<TableHead className="min-w-[200px] font-semibold">Container</TableHead>
<TableHead className="min-w-[100px] font-semibold">Status</TableHead>
<TableHead className="min-w-[140px] font-semibold">CPU Usage</TableHead>
<TableHead className="min-w-[160px] font-semibold">Memory</TableHead>
<TableHead className="min-w-[160px] font-semibold">Disk</TableHead>
<TableHead className="min-w-[100px] font-semibold">Uptime</TableHead>
<TableHead className="min-w-[160px] font-semibold">Last Checked</TableHead>
<TableHead className="min-w-[80px] text-center font-semibold">Actions</TableHead>
</TableRow>
</TableHeader>
);
};
@@ -0,0 +1,124 @@
import { TableCell, TableRow } from "@/components/ui/table";
import { Progress } from "@/components/ui/progress";
import { DockerContainer } from "@/types/docker.types";
import { DockerStatusBadge } from "../DockerStatusBadge";
import { DockerRowActions } from "./DockerRowActions";
import { dockerService } from "@/services/dockerService";
interface DockerTableRowProps {
container: DockerContainer;
onRowClick: (container: DockerContainer) => void;
onContainerAction: (action: string, containerId: string, containerName: string) => void;
onViewMetrics: (container: DockerContainer) => void;
}
export const DockerTableRow = ({ container, onRowClick, onContainerAction, onViewMetrics }: DockerTableRowProps) => {
const cpuPercentage = container.cpu_usage;
const memoryPercentage = Math.round((container.ram_used / container.ram_total) * 100);
const diskPercentage = Math.round((container.disk_used / container.disk_total) * 100);
const containerStatus = dockerService.getStatusFromDockerStatus(container.status);
const formatPercentage = (used: number, total: number) => {
if (total === 0) return "0%";
return `${Math.round((used / total) * 100)}%`;
};
const getUsageColor = (percentage: number) => {
if (percentage >= 90) return "text-red-500";
if (percentage >= 70) return "text-amber-500";
return "text-emerald-500";
};
const getProgressColor = (percentage: number) => {
if (percentage >= 90) return "bg-red-500";
if (percentage >= 70) return "bg-amber-500";
return "bg-emerald-500";
};
return (
<TableRow
className="hover:bg-muted/50 transition-colors border-border cursor-pointer"
onClick={() => onRowClick(container)}
>
<TableCell className="font-medium">
<div className="space-y-1">
<div className="font-semibold text-sm sm:text-base text-foreground">{container.name}</div>
<div className="text-xs sm:text-sm text-muted-foreground">
<div className="font-mono">{container.docker_id}</div>
<div className="font-mono">{container.hostname}</div>
</div>
</div>
</TableCell>
<TableCell>
<DockerStatusBadge status={containerStatus} />
</TableCell>
<TableCell>
<div className="space-y-2">
<div className="flex items-center justify-between gap-3">
<Progress
value={cpuPercentage}
className="flex-1 h-2 bg-muted/50"
indicatorClassName={getProgressColor(cpuPercentage)}
/>
<span className={`font-semibold text-sm min-w-[40px] text-right ${getUsageColor(cpuPercentage)}`}>
{cpuPercentage}%
</span>
</div>
</div>
</TableCell>
<TableCell>
<div className="space-y-2">
<div className="flex items-center justify-between gap-3">
<Progress
value={memoryPercentage}
className="flex-1 h-2 bg-muted/50"
indicatorClassName={getProgressColor(memoryPercentage)}
/>
<span className={`font-semibold text-sm min-w-[40px] text-right ${getUsageColor(memoryPercentage)}`}>
{formatPercentage(container.ram_used, container.ram_total)}
</span>
</div>
<div className="text-xs text-muted-foreground font-mono">
{dockerService.formatBytes(container.ram_used)} / {dockerService.formatBytes(container.ram_total)}
</div>
</div>
</TableCell>
<TableCell>
<div className="space-y-2">
<div className="flex items-center justify-between gap-3">
<Progress
value={diskPercentage}
className="flex-1 h-2 bg-muted/50"
indicatorClassName={getProgressColor(diskPercentage)}
/>
<span className={`font-semibold text-sm min-w-[40px] text-right ${getUsageColor(diskPercentage)}`}>
{formatPercentage(container.disk_used, container.disk_total)}
</span>
</div>
<div className="text-xs text-muted-foreground font-mono">
{dockerService.formatBytes(container.disk_used)} / {dockerService.formatBytes(container.disk_total)}
</div>
</div>
</TableCell>
<TableCell>
<span className="text-xs sm:text-sm font-medium font-mono">
{dockerService.formatUptime(container.uptime)}
</span>
</TableCell>
<TableCell>
<span className="text-xs sm:text-sm text-muted-foreground">
{new Date(container.last_checked).toLocaleString()}
</span>
</TableCell>
<TableCell className="text-center" onClick={(e) => e.stopPropagation()}>
<DockerRowActions
container={container}
containerStatus={containerStatus}
onContainerAction={onContainerAction}
onViewMetrics={onViewMetrics}
/>
</TableCell>
</TableRow>
);
};
@@ -0,0 +1,37 @@
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Search, RefreshCw } from "lucide-react";
interface DockerTableSearchProps {
searchTerm: string;
onSearchChange: (value: string) => void;
onRefresh: () => void;
isLoading: boolean;
}
export const DockerTableSearch = ({ searchTerm, onSearchChange, onRefresh, isLoading }: DockerTableSearchProps) => {
return (
<div className="flex flex-col sm:flex-row gap-3 w-full sm:w-auto">
<div className="relative flex-1 sm:flex-initial">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
<Input
placeholder="Search containers..."
value={searchTerm}
onChange={(e) => onSearchChange(e.target.value)}
className="pl-10 sm:w-64 bg-background border-border"
/>
</div>
<Button
onClick={onRefresh}
disabled={isLoading}
variant="outline"
size="default"
className="w-full sm:w-auto bg-background border-border hover:bg-muted"
>
<RefreshCw className={`h-4 w-4 mr-2 ${isLoading ? 'animate-spin' : ''}`} />
<span className="sm:inline">Refresh</span>
</Button>
</div>
);
};
@@ -0,0 +1,6 @@
export { DockerTableSearch } from './DockerTableSearch';
export { DockerTableHeader } from './DockerTableHeader';
export { DockerTableRow } from './DockerTableRow';
export { DockerRowActions } from './DockerRowActions';
export { DockerEmptyState } from './DockerEmptyState';