feat: Implement docker monitoring dashboard
This commit is contained in:
@@ -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';
|
||||
Reference in New Issue
Block a user