Fix server delete function and Implement pause/resume functionality

- Implement the server delete functionality in the server table and dropdown menu.
- Implement pause and resume functionality for server monitoring in the server table dropdown menu.
This commit is contained in:
Tola Leng
2025-07-17 23:03:40 +07:00
parent 6906a65952
commit 0cad374616
2 changed files with 310 additions and 190 deletions
+309 -189
View File
@@ -8,11 +8,14 @@ import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Progress } from "@/components/ui/progress"; import { Progress } from "@/components/ui/progress";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
import { RefreshCw, Search, Eye, Activity, MoreHorizontal, Pause, Play, Edit, Trash2 } from "lucide-react"; import { RefreshCw, Search, Eye, Activity, MoreHorizontal, Pause, Play, Edit, Trash2 } from "lucide-react";
import { Server } from "@/types/server.types"; import { Server } from "@/types/server.types";
import { ServerStatusBadge } from "./ServerStatusBadge"; import { ServerStatusBadge } from "./ServerStatusBadge";
import { OSTypeIcon } from "./OSTypeIcon"; import { OSTypeIcon } from "./OSTypeIcon";
import { serverService } from "@/services/serverService"; import { serverService } from "@/services/serverService";
import { useToast } from "@/hooks/use-toast";
import { pb } from "@/lib/pocketbase";
interface ServerTableProps { interface ServerTableProps {
servers: Server[]; servers: Server[];
@@ -22,8 +25,12 @@ interface ServerTableProps {
export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps) => { export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps) => {
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const [pausedServers, setPausedServers] = useState<Set<string>>(new Set()); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [selectedServer, setSelectedServer] = useState<Server | null>(null);
const [isDeleting, setIsDeleting] = useState(false);
const [pausingServers, setPausingServers] = useState<Set<string>>(new Set());
const navigate = useNavigate(); const navigate = useNavigate();
const { toast } = useToast();
const filteredServers = servers.filter(server => const filteredServers = servers.filter(server =>
server.name.toLowerCase().includes(searchTerm.toLowerCase()) || server.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
@@ -39,29 +46,105 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
navigate(`/container-monitoring/${serverId}`); navigate(`/container-monitoring/${serverId}`);
}; };
const handlePauseResume = (serverId: string) => { const handlePauseResume = async (server: Server) => {
const isPaused = pausedServers.has(serverId); const serverId = server.id;
if (isPaused) { const isPaused = server.status === "paused";
setPausedServers(prev => {
if (pausingServers.has(serverId)) {
return; // Already processing this server
}
try {
setPausingServers(prev => new Set(prev).add(serverId));
if (isPaused) {
// Resume monitoring
await pb.collection('servers').update(serverId, {
status: "up",
last_checked: new Date().toISOString()
});
toast({
title: "Server resumed",
description: `Monitoring resumed for ${server.name}`,
});
console.log(`Resume server monitoring: ${serverId}`);
} else {
// Pause monitoring
await pb.collection('servers').update(serverId, {
status: "paused",
last_checked: new Date().toISOString()
});
toast({
title: "Server paused",
description: `Monitoring paused for ${server.name}`,
});
console.log(`Pause server monitoring: ${serverId}`);
}
// Refresh the server list to show updated status
onRefresh();
} catch (error) {
console.error('Error updating server status:', error);
toast({
variant: "destructive",
title: "Error",
description: `Failed to ${isPaused ? 'resume' : 'pause'} server monitoring. Please try again.`,
});
} finally {
setPausingServers(prev => {
const newSet = new Set(prev); const newSet = new Set(prev);
newSet.delete(serverId); newSet.delete(serverId);
return newSet; return newSet;
}); });
// console.log('Resume server monitoring:', serverId);
} else {
setPausedServers(prev => new Set(prev).add(serverId));
// console.log('Pause server monitoring:', serverId);
} }
}; };
const handleEdit = (serverId: string) => { const handleEdit = (serverId: string) => {
// TODO: Implement edit functionality // TODO: Implement edit functionality
// console.log('Edit server:', serverId); console.log('Edit server:', serverId);
}; };
const handleDelete = (serverId: string) => { const handleDelete = (server: Server) => {
// TODO: Implement delete functionality setSelectedServer(server);
// console.log('Delete server:', serverId); setDeleteDialogOpen(true);
};
const confirmDelete = async () => {
if (!selectedServer || isDeleting) return;
try {
setIsDeleting(true);
// Delete the server from the database
await pb.collection('servers').delete(selectedServer.id);
toast({
title: "Server deleted",
description: `${selectedServer.name} has been deleted successfully.`,
});
// Refresh the server list
onRefresh();
// Close the dialog
setDeleteDialogOpen(false);
setSelectedServer(null);
} catch (error) {
console.error('Error deleting server:', error);
toast({
variant: "destructive",
title: "Error",
description: "Failed to delete server. Please try again.",
});
} finally {
setIsDeleting(false);
}
}; };
if (isLoading) { if (isLoading) {
@@ -81,189 +164,226 @@ export const ServerTable = ({ servers, isLoading, onRefresh }: ServerTableProps)
} }
return ( return (
<Card> <>
<CardHeader> <Card>
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> <CardHeader>
<CardTitle className="text-xl font-semibold">Servers</CardTitle> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div className="flex items-center gap-2"> <CardTitle className="text-xl font-semibold">Servers</CardTitle>
<div className="relative flex-1 sm:w-64"> <div className="flex items-center gap-2">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" /> <div className="relative flex-1 sm:w-64">
<Input <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
placeholder="Search servers..." <Input
value={searchTerm} placeholder="Search servers..."
onChange={(e) => setSearchTerm(e.target.value)} value={searchTerm}
className="pl-8" onChange={(e) => setSearchTerm(e.target.value)}
/> className="pl-8"
/>
</div>
<Button onClick={onRefresh} variant="outline" size="icon">
<RefreshCw className="h-4 w-4" />
</Button>
</div> </div>
<Button onClick={onRefresh} variant="outline" size="icon">
<RefreshCw className="h-4 w-4" />
</Button>
</div> </div>
</div> </CardHeader>
</CardHeader> <CardContent>
<CardContent> {filteredServers.length === 0 ? (
{filteredServers.length === 0 ? ( <div className="text-center py-8">
<div className="text-center py-8"> <p className="text-muted-foreground">No servers found</p>
<p className="text-muted-foreground">No servers found</p> </div>
</div> ) : (
) : ( <div className="rounded-md border overflow-x-auto">
<div className="rounded-md border overflow-x-auto"> <Table>
<Table> <TableHeader>
<TableHeader> <TableRow>
<TableRow> <TableHead>Name</TableHead>
<TableHead>Name</TableHead> <TableHead>Status</TableHead>
<TableHead>Status</TableHead> <TableHead>OS</TableHead>
<TableHead>OS</TableHead> <TableHead>IP Address</TableHead>
<TableHead>IP Address</TableHead> <TableHead>CPU</TableHead>
<TableHead>CPU</TableHead> <TableHead>Memory</TableHead>
<TableHead>Memory</TableHead> <TableHead>Disk</TableHead>
<TableHead>Disk</TableHead> <TableHead>Uptime</TableHead>
<TableHead>Uptime</TableHead> <TableHead>Last Checked</TableHead>
<TableHead>Last Checked</TableHead> <TableHead className="text-right">Actions</TableHead>
<TableHead className="text-right">Actions</TableHead> </TableRow>
</TableRow> </TableHeader>
</TableHeader> <TableBody>
<TableBody> {filteredServers.map((server) => {
{filteredServers.map((server) => { const cpuUsage = server.cpu_usage || 0;
const cpuUsage = server.cpu_usage || 0; const memoryUsage = server.ram_total > 0 ? (server.ram_used / server.ram_total) * 100 : 0;
const memoryUsage = server.ram_total > 0 ? (server.ram_used / server.ram_total) * 100 : 0; const diskUsage = server.disk_total > 0 ? (server.disk_used / server.disk_total) * 100 : 0;
const diskUsage = server.disk_total > 0 ? (server.disk_used / server.disk_total) * 100 : 0; const isPaused = server.status === "paused";
const isPaused = pausedServers.has(server.id); const isProcessing = pausingServers.has(server.id);
return ( return (
<TableRow key={server.id} className="hover:bg-muted/50"> <TableRow key={server.id} className="hover:bg-muted/50">
<TableCell> <TableCell>
<div> <div>
<div className="font-medium">{server.name}</div> <div className="font-medium">{server.name}</div>
</div>
</TableCell>
<TableCell>
<ServerStatusBadge status={server.status} />
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<OSTypeIcon osType={server.os_type} />
<span className="text-sm">{server.os_type}</span>
</div>
</TableCell>
<TableCell>
<code className="text-sm bg-muted px-1 py-0.5 rounded">{server.ip_address}</code>
</TableCell>
<TableCell>
<div className="space-y-1 min-w-[120px]">
<div className="flex justify-between text-sm">
<span>{cpuUsage.toFixed(1)}%</span>
<span className="text-muted-foreground">{server.cpu_cores} cores</span>
</div> </div>
<Progress </TableCell>
value={cpuUsage} <TableCell>
className="h-2" <ServerStatusBadge status={server.status} />
indicatorClassName={ </TableCell>
cpuUsage > 90 ? "bg-red-00" : <TableCell>
cpuUsage > 75 ? "bg-orange-500" : <div className="flex items-center gap-2">
cpuUsage > 60 ? "bg-yellow-500" : "bg-green-500" <OSTypeIcon osType={server.os_type} />
} <span className="text-sm">{server.os_type}</span>
/>
</div>
</TableCell>
<TableCell>
<div className="space-y-1 min-w-[120px]">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">{memoryUsage.toFixed(1)}%</span>
<span>{serverService.formatBytes(server.ram_total)}</span>
</div> </div>
<Progress </TableCell>
value={memoryUsage} <TableCell>
className="h-2" <code className="text-sm bg-muted px-1 py-0.5 rounded">{server.ip_address}</code>
indicatorClassName={ </TableCell>
memoryUsage > 90 ? "bg-red-500" : <TableCell>
memoryUsage > 75 ? "bg-yellow-500" : "bg-blue-500" <div className="space-y-1 min-w-[120px]">
} <div className="flex justify-between text-sm">
/> <span>{cpuUsage.toFixed(1)}%</span>
</div> <span className="text-muted-foreground">{server.cpu_cores} cores</span>
</TableCell> </div>
<TableCell> <Progress
<div className="space-y-1 min-w-[120px]"> value={cpuUsage}
<div className="flex justify-between text-sm"> className="h-2"
<span className="text-muted-foreground">{diskUsage.toFixed(1)}%</span> indicatorClassName={
<span>{serverService.formatBytes(server.disk_total)}</span> cpuUsage > 90 ? "bg-red-500" :
cpuUsage > 75 ? "bg-orange-500" :
cpuUsage > 60 ? "bg-yellow-500" : "bg-green-500"
}
/>
</div> </div>
<Progress </TableCell>
value={diskUsage} <TableCell>
className="h-2" <div className="space-y-1 min-w-[120px]">
indicatorClassName={ <div className="flex justify-between text-sm">
diskUsage > 95 ? "bg-red-500" : <span className="text-muted-foreground">{memoryUsage.toFixed(1)}%</span>
diskUsage > 85 ? "bg-yellow-500" : "bg-orange-500" <span>{serverService.formatBytes(server.ram_total)}</span>
} </div>
/> <Progress
</div> value={memoryUsage}
</TableCell> className="h-2"
<TableCell> indicatorClassName={
<div className="text-sm">{server.uptime}</div> memoryUsage > 90 ? "bg-red-500" :
</TableCell> memoryUsage > 75 ? "bg-yellow-500" : "bg-blue-500"
}
<TableCell> />
<div className="text-sm text-muted-foreground"> </div>
{new Date(server.last_checked).toLocaleString()} </TableCell>
</div> <TableCell>
</TableCell> <div className="space-y-1 min-w-[120px]">
<TableCell className="text-right"> <div className="flex justify-between text-sm">
<DropdownMenu> <span className="text-muted-foreground">{diskUsage.toFixed(1)}%</span>
<DropdownMenuTrigger asChild> <span>{serverService.formatBytes(server.disk_total)}</span>
<Button variant="ghost" className="h-8 w-8 p-0"> </div>
<span className="sr-only">Open menu</span> <Progress
<MoreHorizontal className="h-4 w-4" /> value={diskUsage}
</Button> className="h-2"
</DropdownMenuTrigger> indicatorClassName={
<DropdownMenuContent align="end" className="w-[200px]"> diskUsage > 95 ? "bg-red-500" :
<DropdownMenuItem onClick={() => handleViewDetails(server.id)}> diskUsage > 85 ? "bg-yellow-500" : "bg-orange-500"
<Eye className="mr-2 h-4 w-4" /> }
View Server Detail />
</DropdownMenuItem> </div>
{server.docker === 'true' && ( </TableCell>
<DropdownMenuItem onClick={() => handleViewContainers(server.id)}> <TableCell>
<Activity className="mr-2 h-4 w-4" /> <div className="text-sm">{server.uptime}</div>
Container Monitoring </TableCell>
<TableCell>
<div className="text-sm text-muted-foreground">
{new Date(server.last_checked).toLocaleString()}
</div>
</TableCell>
<TableCell className="text-right">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0" disabled={isProcessing}>
<span className="sr-only">Open menu</span>
{isProcessing ? (
<RefreshCw className="h-4 w-4 animate-spin" />
) : (
<MoreHorizontal className="h-4 w-4" />
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[200px]">
<DropdownMenuItem onClick={() => handleViewDetails(server.id)}>
<Eye className="mr-2 h-4 w-4" />
View Server Detail
</DropdownMenuItem> </DropdownMenuItem>
)} {server.docker === 'true' && (
<DropdownMenuSeparator /> <DropdownMenuItem onClick={() => handleViewContainers(server.id)}>
<DropdownMenuItem onClick={() => handlePauseResume(server.id)}> <Activity className="mr-2 h-4 w-4" />
{isPaused ? ( Container Monitoring
<> </DropdownMenuItem>
<Play className="mr-2 h-4 w-4" />
Resume Monitoring
</>
) : (
<>
<Pause className="mr-2 h-4 w-4" />
Pause Monitoring
</>
)} )}
</DropdownMenuItem> <DropdownMenuSeparator />
<DropdownMenuSeparator /> <DropdownMenuItem
<DropdownMenuItem onClick={() => handleEdit(server.id)}> onClick={() => handlePauseResume(server)}
<Edit className="mr-2 h-4 w-4" /> disabled={isProcessing}
Edit Server >
</DropdownMenuItem> {isPaused ? (
<DropdownMenuItem <>
onClick={() => handleDelete(server.id)} <Play className="mr-2 h-4 w-4" />
className="text-red-600 focus:text-red-600" Resume Monitoring
> </>
<Trash2 className="mr-2 h-4 w-4" /> ) : (
Delete Server <>
</DropdownMenuItem> <Pause className="mr-2 h-4 w-4" />
</DropdownMenuContent> Pause Monitoring
</DropdownMenu> </>
</TableCell> )}
</TableRow> </DropdownMenuItem>
); <DropdownMenuSeparator />
})} <DropdownMenuItem onClick={() => handleEdit(server.id)}>
</TableBody> <Edit className="mr-2 h-4 w-4" />
</Table> Edit Server
</div> </DropdownMenuItem>
)} <DropdownMenuItem
</CardContent> onClick={() => handleDelete(server)}
</Card> className="text-red-600 focus:text-red-600"
>
<Trash2 className="mr-2 h-4 w-4" />
Delete Server
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
)}
</CardContent>
</Card>
{/* Delete Confirmation Dialog */}
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you sure you want to delete this server?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete{' '}
<span className="font-semibold text-foreground">
{selectedServer?.name}
</span>{' '}
and all of its monitoring data.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isDeleting}>
Cancel
</AlertDialogCancel>
<AlertDialogAction
onClick={confirmDelete}
disabled={isDeleting}
className="bg-red-600 text-white hover:bg-red-700"
>
{isDeleting ? "Deleting..." : "Delete"}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
); );
}; };
+1 -1
View File
@@ -8,7 +8,7 @@ export interface Server {
hostname: string; hostname: string;
ip_address: string; ip_address: string;
os_type: string; os_type: string;
status: 'up' | 'down' | 'warning'; status: 'up' | 'down' | 'warning' | 'paused';
uptime: string; uptime: string;
ram_total: number; ram_total: number;
ram_used: number; ram_used: number;