Preview
Full width desktop view
Code
monitoring-1.tsx
1"use client";
2import React, { useState, useMemo } from "react";
3import {
4 Search,
5 Filter,
6 Download,
7 RotateCcw,
8 Trash2,
9 ChevronDown,
10 ChevronRight,
11 Calendar,
12 Database,
13 HardDrive,
14 Clock,
15 MapPin,
16 CheckCircle,
17 XCircle,
18 AlertCircle,
19 Loader2,
20 MoreHorizontal,
21 Eye,
22 RefreshCw,
23 FileText,
24 Settings,
25} from "lucide-react";
26import { Button } from "@/components/ui/button";
27import { Input } from "@/components/ui/input";
28import { Card } from "@/components/ui/card";
29import { Badge } from "@/components/ui/badge";
30import {
31 Table,
32 TableBody,
33 TableCell,
34 TableHead,
35 TableHeader,
36 TableRow,
37} from "@/components/ui/table";
38import {
39 Select,
40 SelectContent,
41 SelectItem,
42 SelectTrigger,
43 SelectValue,
44} from "@/components/ui/select";
45import { Checkbox } from "@/components/ui/checkbox";
46import {
47 Dialog,
48 DialogContent,
49 DialogDescription,
50 DialogFooter,
51 DialogHeader,
52 DialogTitle,
53} from "@/components/ui/dialog";
54import { Alert, AlertDescription } from "@/components/ui/alert";
55import { Progress } from "@/components/ui/progress";
56import { TooltipProvider } from "@/components/ui/tooltip";
57import {
58 DropdownMenu,
59 DropdownMenuContent,
60 DropdownMenuItem,
61 DropdownMenuTrigger,
62} from "@/components/ui/dropdown-menu";
63
64interface BackupEntry {
65 id: string;
66 databaseName: string;
67 backupType: "full" | "incremental" | "differential";
68 status: "completed" | "failed" | "in-progress" | "pending";
69 dateTime: string;
70 vpsRegion: string;
71 size: string;
72 duration: string;
73 logs: string[];
74 healthScore: number;
75}
76
77interface BackupMetrics {
78 totalBackups: number;
79 successfulBackups: number;
80 failedBackups: number;
81 totalSize: string;
82 avgDuration: string;
83 lastBackup: string;
84}
85
86const DatabaseBackupDashboard: React.FC = () => {
87 const [searchTerm, setSearchTerm] = useState("");
88 const [statusFilter, setStatusFilter] = useState("all");
89 const [typeFilter, setTypeFilter] = useState("all");
90 const [regionFilter, setRegionFilter] = useState("all");
91 const [sortField, setSortField] = useState<keyof BackupEntry>("dateTime");
92 const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
93 const [selectedBackups, setSelectedBackups] = useState<string[]>([]);
94 const [expandedRows, setExpandedRows] = useState<string[]>([]);
95 const [isLoading, setIsLoading] = useState(false);
96 const [showDeleteDialog, setShowDeleteDialog] = useState(false);
97 const [showRestoreDialog, setShowRestoreDialog] = useState(false);
98 const [currentPage, setCurrentPage] = useState(1);
99 const [itemsPerPage] = useState(10);
100
101 const mockBackups: BackupEntry[] = [
102 {
103 id: "1",
104 databaseName: "production_db",
105 backupType: "full",
106 status: "completed",
107 dateTime: "2024-01-15T14:30:00Z",
108 vpsRegion: "us-east-1",
109 size: "2.4 GB",
110 duration: "12m 34s",
111 logs: [
112 "Backup initiated",
113 "Database locked",
114 "Data export completed",
115 "Backup verified",
116 ],
117 healthScore: 98,
118 },
119 {
120 id: "2",
121 databaseName: "staging_db",
122 backupType: "incremental",
123 status: "failed",
124 dateTime: "2024-01-15T12:15:00Z",
125 vpsRegion: "eu-west-1",
126 size: "450 MB",
127 duration: "3m 21s",
128 logs: ["Backup initiated", "Connection timeout", "Retry attempt failed"],
129 healthScore: 45,
130 },
131 {
132 id: "3",
133 databaseName: "analytics_db",
134 backupType: "differential",
135 status: "in-progress",
136 dateTime: "2024-01-15T16:00:00Z",
137 vpsRegion: "ap-south-1",
138 size: "1.2 GB",
139 duration: "8m 12s",
140 logs: ["Backup initiated", "Processing data chunks", "Progress: 65%"],
141 healthScore: 85,
142 },
143 {
144 id: "4",
145 databaseName: "user_data_db",
146 backupType: "full",
147 status: "completed",
148 dateTime: "2024-01-14T22:00:00Z",
149 vpsRegion: "us-west-2",
150 size: "5.8 GB",
151 duration: "25m 45s",
152 logs: [
153 "Backup initiated",
154 "Database locked",
155 "Data export completed",
156 "Compression applied",
157 "Backup verified",
158 ],
159 healthScore: 95,
160 },
161 {
162 id: "5",
163 databaseName: "logs_db",
164 backupType: "incremental",
165 status: "pending",
166 dateTime: "2024-01-15T18:00:00Z",
167 vpsRegion: "eu-central-1",
168 size: "0 MB",
169 duration: "0s",
170 logs: ["Backup scheduled"],
171 healthScore: 0,
172 },
173 ];
174
175 const mockMetrics: BackupMetrics = {
176 totalBackups: 156,
177 successfulBackups: 142,
178 failedBackups: 14,
179 totalSize: "2.4 TB",
180 avgDuration: "15m 32s",
181 lastBackup: "2024-01-15T16:00:00Z",
182 };
183
184 const getStatusIcon = (status: string) => {
185 switch (status) {
186 case "completed":
187 return <CheckCircle className="h-4 w-4 text-green-500" />;
188 case "failed":
189 return <XCircle className="h-4 w-4 text-red-500" />;
190 case "in-progress":
191 return <Loader2 className="h-4 w-4 text-blue-500 animate-spin" />;
192 case "pending":
193 return <Clock className="h-4 w-4 text-yellow-500" />;
194 default:
195 return <AlertCircle className="h-4 w-4 text-gray-500" />;
196 }
197 };
198
199 const getStatusBadge = (status: string) => {
200 const variants = {
201 completed: "bg-green-100 text-green-800 border-green-200",
202 failed: "bg-red-100 text-red-800 border-red-200",
203 "in-progress": "bg-blue-100 text-blue-800 border-blue-200",
204 pending: "bg-yellow-100 text-yellow-800 border-yellow-200",
205 };
206
207 return (
208 <Badge className={`${variants[status as keyof typeof variants]} border`}>
209 {getStatusIcon(status)}
210 <span className="ml-1 capitalize">{status.replace("-", " ")}</span>
211 </Badge>
212 );
213 };
214
215 const getTypeIcon = (type: string) => {
216 switch (type) {
217 case "full":
218 return <Database className="h-4 w-4" />;
219 case "incremental":
220 return <RefreshCw className="h-4 w-4" />;
221 case "differential":
222 return <FileText className="h-4 w-4" />;
223 default:
224 return <Database className="h-4 w-4" />;
225 }
226 };
227
228 const filteredAndSortedBackups = useMemo(() => {
229 let filtered = mockBackups.filter((backup) => {
230 const matchesSearch =
231 backup.databaseName.toLowerCase().includes(searchTerm.toLowerCase()) ||
232 backup.vpsRegion.toLowerCase().includes(searchTerm.toLowerCase());
233 const matchesStatus =
234 statusFilter === "all" || backup.status === statusFilter;
235 const matchesType =
236 typeFilter === "all" || backup.backupType === typeFilter;
237 const matchesRegion =
238 regionFilter === "all" || backup.vpsRegion === regionFilter;
239
240 return matchesSearch && matchesStatus && matchesType && matchesRegion;
241 });
242
243 filtered.sort((a, b) => {
244 const aValue = a[sortField];
245 const bValue = b[sortField];
246
247 if (sortDirection === "asc") {
248 return aValue < bValue ? -1 : aValue > bValue ? 1 : 0;
249 } else {
250 return aValue > bValue ? -1 : aValue < bValue ? 1 : 0;
251 }
252 });
253
254 return filtered;
255 }, [
256 mockBackups,
257 searchTerm,
258 statusFilter,
259 typeFilter,
260 regionFilter,
261 sortField,
262 sortDirection,
263 ]);
264
265 const paginatedBackups = useMemo(() => {
266 const startIndex = (currentPage - 1) * itemsPerPage;
267 return filteredAndSortedBackups.slice(
268 startIndex,
269 startIndex + itemsPerPage
270 );
271 }, [filteredAndSortedBackups, currentPage, itemsPerPage]);
272
273 const totalPages = Math.ceil(filteredAndSortedBackups.length / itemsPerPage);
274
275 const handleSort = (field: keyof BackupEntry) => {
276 if (sortField === field) {
277 setSortDirection(sortDirection === "asc" ? "desc" : "asc");
278 } else {
279 setSortField(field);
280 setSortDirection("desc");
281 }
282 };
283
284 const handleSelectAll = (checked: boolean) => {
285 if (checked) {
286 setSelectedBackups(paginatedBackups.map((backup) => backup.id));
287 } else {
288 setSelectedBackups([]);
289 }
290 };
291
292 const handleSelectBackup = (backupId: string, checked: boolean) => {
293 if (checked) {
294 setSelectedBackups([...selectedBackups, backupId]);
295 } else {
296 setSelectedBackups(selectedBackups.filter((id) => id !== backupId));
297 }
298 };
299
300 const toggleRowExpansion = (backupId: string) => {
301 if (expandedRows.includes(backupId)) {
302 setExpandedRows(expandedRows.filter((id) => id !== backupId));
303 } else {
304 setExpandedRows([...expandedRows, backupId]);
305 }
306 };
307
308 const handleBulkAction = async (action: string) => {
309 setIsLoading(true);
310 // Simulate API call
311 await new Promise((resolve) => setTimeout(resolve, 2000));
312 setIsLoading(false);
313 setSelectedBackups([]);
314
315 if (action === "delete") {
316 setShowDeleteDialog(false);
317 } else if (action === "restore") {
318 setShowRestoreDialog(false);
319 }
320 };
321
322 const formatDateTime = (dateTime: string) => {
323 return new Date(dateTime).toLocaleString();
324 };
325
326 const getHealthColor = (score: number) => {
327 if (score >= 90) return "text-green-600";
328 if (score >= 70) return "text-yellow-600";
329 return "text-red-600";
330 };
331
332 return (
333 <TooltipProvider>
334 <div className="min-h-screen bg-background p-6 space-y-6">
335 {/* Header */}
336 <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
337 <div>
338 <h1 className="text-3xl font-bold text-foreground">
339 Database Backup Management
340 </h1>
341 <p className="text-muted-foreground">
342 Monitor and manage your database backups
343 </p>
344 </div>
345 <div className="flex gap-2">
346 <Button variant="outline" size="sm">
347 <Settings className="h-4 w-4 mr-2" />
348 Settings
349 </Button>
350 <Button size="sm">
351 <RefreshCw className="h-4 w-4 mr-2" />
352 Refresh
353 </Button>
354 </div>
355 </div>
356
357 {/* Metrics Cards */}
358 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-4">
359 <Card className="p-4">
360 <div className="flex items-center justify-between">
361 <div>
362 <p className="text-sm text-muted-foreground">Total Backups</p>
363 <p className="text-2xl font-bold">{mockMetrics.totalBackups}</p>
364 </div>
365 <Database className="h-8 w-8 text-blue-500" />
366 </div>
367 </Card>
368
369 <Card className="p-4">
370 <div className="flex items-center justify-between">
371 <div>
372 <p className="text-sm text-muted-foreground">Successful</p>
373 <p className="text-2xl font-bold text-green-600">
374 {mockMetrics.successfulBackups}
375 </p>
376 </div>
377 <CheckCircle className="h-8 w-8 text-green-500" />
378 </div>
379 </Card>
380
381 <Card className="p-4">
382 <div className="flex items-center justify-between">
383 <div>
384 <p className="text-sm text-muted-foreground">Failed</p>
385 <p className="text-2xl font-bold text-red-600">
386 {mockMetrics.failedBackups}
387 </p>
388 </div>
389 <XCircle className="h-8 w-8 text-red-500" />
390 </div>
391 </Card>
392
393 <Card className="p-4">
394 <div className="flex items-center justify-between">
395 <div>
396 <p className="text-sm text-muted-foreground">Total Size</p>
397 <p className="text-2xl font-bold">{mockMetrics.totalSize}</p>
398 </div>
399 <HardDrive className="h-8 w-8 text-purple-500" />
400 </div>
401 </Card>
402
403 <Card className="p-4">
404 <div className="flex items-center justify-between">
405 <div>
406 <p className="text-sm text-muted-foreground">Avg Duration</p>
407 <p className="text-2xl font-bold">{mockMetrics.avgDuration}</p>
408 </div>
409 <Clock className="h-8 w-8 text-orange-500" />
410 </div>
411 </Card>
412
413 <Card className="p-4">
414 <div className="flex items-center justify-between">
415 <div>
416 <p className="text-sm text-muted-foreground">Success Rate</p>
417 <p className="text-2xl font-bold text-green-600">
418 {Math.round(
419 (mockMetrics.successfulBackups / mockMetrics.totalBackups) *
420 100
421 )}
422 %
423 </p>
424 </div>
425 <div className="w-8 h-8 rounded-full bg-green-100 flex items-center justify-center">
426 <div className="w-4 h-4 rounded-full bg-green-500"></div>
427 </div>
428 </div>
429 </Card>
430 </div>
431
432 {/* Filters and Search */}
433 <Card className="p-6">
434 <div className="flex flex-col lg:flex-row gap-4 items-start lg:items-center justify-between">
435 <div className="flex flex-col sm:flex-row gap-4 flex-1">
436 <div className="relative flex-1 max-w-md">
437 <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
438 <Input
439 placeholder="Search databases or regions..."
440 value={searchTerm}
441 onChange={(e) => setSearchTerm(e.target.value)}
442 className="pl-10"
443 />
444 </div>
445
446 <div className="flex gap-2">
447 <Select value={statusFilter} onValueChange={setStatusFilter}>
448 <SelectTrigger className="w-[140px]">
449 <SelectValue placeholder="Status" />
450 </SelectTrigger>
451 <SelectContent>
452 <SelectItem value="all">All Status</SelectItem>
453 <SelectItem value="completed">Completed</SelectItem>
454 <SelectItem value="failed">Failed</SelectItem>
455 <SelectItem value="in-progress">In Progress</SelectItem>
456 <SelectItem value="pending">Pending</SelectItem>
457 </SelectContent>
458 </Select>
459
460 <Select value={typeFilter} onValueChange={setTypeFilter}>
461 <SelectTrigger className="w-[140px]">
462 <SelectValue placeholder="Type" />
463 </SelectTrigger>
464 <SelectContent>
465 <SelectItem value="all">All Types</SelectItem>
466 <SelectItem value="full">Full</SelectItem>
467 <SelectItem value="incremental">Incremental</SelectItem>
468 <SelectItem value="differential">Differential</SelectItem>
469 </SelectContent>
470 </Select>
471
472 <Select value={regionFilter} onValueChange={setRegionFilter}>
473 <SelectTrigger className="w-[140px]">
474 <SelectValue placeholder="Region" />
475 </SelectTrigger>
476 <SelectContent>
477 <SelectItem value="all">All Regions</SelectItem>
478 <SelectItem value="us-east-1">US East 1</SelectItem>
479 <SelectItem value="us-west-2">US West 2</SelectItem>
480 <SelectItem value="eu-west-1">EU West 1</SelectItem>
481 <SelectItem value="eu-central-1">EU Central 1</SelectItem>
482 <SelectItem value="ap-south-1">AP South 1</SelectItem>
483 </SelectContent>
484 </Select>
485 </div>
486 </div>
487
488 {selectedBackups.length > 0 && (
489 <div className="flex gap-2">
490 <Button
491 variant="outline"
492 size="sm"
493 onClick={() => setShowRestoreDialog(true)}
494 disabled={isLoading}
495 >
496 <RotateCcw className="h-4 w-4 mr-2" />
497 Restore ({selectedBackups.length})
498 </Button>
499 <Button variant="outline" size="sm" disabled={isLoading}>
500 <Download className="h-4 w-4 mr-2" />
501 Download ({selectedBackups.length})
502 </Button>
503 <Button
504 variant="destructive"
505 size="sm"
506 onClick={() => setShowDeleteDialog(true)}
507 disabled={isLoading}
508 >
509 <Trash2 className="h-4 w-4 mr-2" />
510 Delete ({selectedBackups.length})
511 </Button>
512 </div>
513 )}
514 </div>
515 </Card>
516
517 {/* Backup Table */}
518 <Card>
519 <div className="overflow-x-auto">
520 <Table>
521 <TableHeader>
522 <TableRow>
523 <TableHead className="w-12">
524 <Checkbox
525 checked={
526 selectedBackups.length === paginatedBackups.length &&
527 paginatedBackups.length > 0
528 }
529 onCheckedChange={handleSelectAll}
530 />
531 </TableHead>
532 <TableHead className="w-12"></TableHead>
533 <TableHead
534 className="cursor-pointer hover:bg-muted/50"
535 onClick={() => handleSort("databaseName")}
536 >
537 <div className="flex items-center gap-2">
538 <Database className="h-4 w-4" />
539 Database Name
540 {sortField === "databaseName" && (
541 <ChevronDown
542 className={`h-4 w-4 transition-transform ${
543 sortDirection === "asc" ? "rotate-180" : ""
544 }`}
545 />
546 )}
547 </div>
548 </TableHead>
549 <TableHead
550 className="cursor-pointer hover:bg-muted/50"
551 onClick={() => handleSort("backupType")}
552 >
553 <div className="flex items-center gap-2">
554 Type
555 {sortField === "backupType" && (
556 <ChevronDown
557 className={`h-4 w-4 transition-transform ${
558 sortDirection === "asc" ? "rotate-180" : ""
559 }`}
560 />
561 )}
562 </div>
563 </TableHead>
564 <TableHead
565 className="cursor-pointer hover:bg-muted/50"
566 onClick={() => handleSort("dateTime")}
567 >
568 <div className="flex items-center gap-2">
569 <Calendar className="h-4 w-4" />
570 Date & Time
571 {sortField === "dateTime" && (
572 <ChevronDown
573 className={`h-4 w-4 transition-transform ${
574 sortDirection === "asc" ? "rotate-180" : ""
575 }`}
576 />
577 )}
578 </div>
579 </TableHead>
580 <TableHead
581 className="cursor-pointer hover:bg-muted/50"
582 onClick={() => handleSort("vpsRegion")}
583 >
584 <div className="flex items-center gap-2">
585 <MapPin className="h-4 w-4" />
586 Region
587 {sortField === "vpsRegion" && (
588 <ChevronDown
589 className={`h-4 w-4 transition-transform ${
590 sortDirection === "asc" ? "rotate-180" : ""
591 }`}
592 />
593 )}
594 </div>
595 </TableHead>
596 <TableHead
597 className="cursor-pointer hover:bg-muted/50"
598 onClick={() => handleSort("size")}
599 >
600 <div className="flex items-center gap-2">
601 <HardDrive className="h-4 w-4" />
602 Size
603 {sortField === "size" && (
604 <ChevronDown
605 className={`h-4 w-4 transition-transform ${
606 sortDirection === "asc" ? "rotate-180" : ""
607 }`}
608 />
609 )}
610 </div>
611 </TableHead>
612 <TableHead
613 className="cursor-pointer hover:bg-muted/50"
614 onClick={() => handleSort("duration")}
615 >
616 <div className="flex items-center gap-2">
617 <Clock className="h-4 w-4" />
618 Duration
619 {sortField === "duration" && (
620 <ChevronDown
621 className={`h-4 w-4 transition-transform ${
622 sortDirection === "asc" ? "rotate-180" : ""
623 }`}
624 />
625 )}
626 </div>
627 </TableHead>
628 <TableHead>Status</TableHead>
629 <TableHead>Health</TableHead>
630 <TableHead className="w-12">Actions</TableHead>
631 </TableRow>
632 </TableHeader>
633 <TableBody>
634 {paginatedBackups.length === 0 ? (
635 <TableRow>
636 <TableCell colSpan={11} className="text-center py-8">
637 <div className="flex flex-col items-center gap-2">
638 <Database className="h-12 w-12 text-muted-foreground" />
639 <p className="text-lg font-medium">No backups found</p>
640 <p className="text-muted-foreground">
641 Try adjusting your search or filter criteria
642 </p>
643 </div>
644 </TableCell>
645 </TableRow>
646 ) : (
647 paginatedBackups.map((backup) => (
648 <React.Fragment key={backup.id}>
649 <TableRow className="hover:bg-muted/50 transition-colors">
650 <TableCell>
651 <Checkbox
652 checked={selectedBackups.includes(backup.id)}
653 onCheckedChange={(checked) =>
654 handleSelectBackup(backup.id, checked as boolean)
655 }
656 />
657 </TableCell>
658 <TableCell>
659 <Button
660 variant="ghost"
661 size="sm"
662 onClick={() => toggleRowExpansion(backup.id)}
663 className="p-0 h-8 w-8"
664 >
665 {expandedRows.includes(backup.id) ? (
666 <ChevronDown className="h-4 w-4" />
667 ) : (
668 <ChevronRight className="h-4 w-4" />
669 )}
670 </Button>
671 </TableCell>
672 <TableCell className="font-medium">
673 {backup.databaseName}
674 </TableCell>
675 <TableCell>
676 <div className="flex items-center gap-2">
677 {getTypeIcon(backup.backupType)}
678 <span className="capitalize">
679 {backup.backupType}
680 </span>
681 </div>
682 </TableCell>
683 <TableCell>{formatDateTime(backup.dateTime)}</TableCell>
684 <TableCell>
685 <Badge variant="outline">{backup.vpsRegion}</Badge>
686 </TableCell>
687 <TableCell>{backup.size}</TableCell>
688 <TableCell>{backup.duration}</TableCell>
689 <TableCell>{getStatusBadge(backup.status)}</TableCell>
690 <TableCell>
691 <div className="flex items-center gap-2">
692 <div
693 className={`text-sm font-medium ${getHealthColor(
694 backup.healthScore
695 )}`}
696 >
697 {backup.healthScore}%
698 </div>
699 <Progress
700 value={backup.healthScore}
701 className="w-16 h-2"
702 />
703 </div>
704 </TableCell>
705 <TableCell>
706 <DropdownMenu>
707 <DropdownMenuTrigger asChild>
708 <Button
709 variant="ghost"
710 size="sm"
711 className="p-0 h-8 w-8"
712 >
713 <MoreHorizontal className="h-4 w-4" />
714 </Button>
715 </DropdownMenuTrigger>
716 <DropdownMenuContent align="end">
717 <DropdownMenuItem>
718 <Eye className="h-4 w-4 mr-2" />
719 View Details
720 </DropdownMenuItem>
721 <DropdownMenuItem>
722 <Download className="h-4 w-4 mr-2" />
723 Download
724 </DropdownMenuItem>
725 <DropdownMenuItem>
726 <RotateCcw className="h-4 w-4 mr-2" />
727 Restore
728 </DropdownMenuItem>
729 <DropdownMenuItem className="text-red-600">
730 <Trash2 className="h-4 w-4 mr-2" />
731 Delete
732 </DropdownMenuItem>
733 </DropdownMenuContent>
734 </DropdownMenu>
735 </TableCell>
736 </TableRow>
737
738 {expandedRows.includes(backup.id) && (
739 <TableRow>
740 <TableCell colSpan={11} className="bg-muted/20">
741 <div className="p-4 space-y-3">
742 <h4 className="font-medium">Backup Logs</h4>
743 <div className="space-y-2">
744 {backup.logs.map((log, index) => (
745 <div
746 key={index}
747 className="flex items-center gap-2 text-sm"
748 >
749 <div className="w-2 h-2 rounded-full bg-blue-500"></div>
750 <span className="text-muted-foreground">
751 {log}
752 </span>
753 </div>
754 ))}
755 </div>
756 </div>
757 </TableCell>
758 </TableRow>
759 )}
760 </React.Fragment>
761 ))
762 )}
763 </TableBody>
764 </Table>
765 </div>
766
767 {/* Pagination */}
768 {totalPages > 1 && (
769 <div className="flex items-center justify-between px-6 py-4 border-t">
770 <div className="text-sm text-muted-foreground">
771 Showing {(currentPage - 1) * itemsPerPage + 1} to{" "}
772 {Math.min(
773 currentPage * itemsPerPage,
774 filteredAndSortedBackups.length
775 )}{" "}
776 of {filteredAndSortedBackups.length} results
777 </div>
778 <div className="flex gap-2">
779 <Button
780 variant="outline"
781 size="sm"
782 onClick={() => setCurrentPage(currentPage - 1)}
783 disabled={currentPage === 1}
784 >
785 Previous
786 </Button>
787 {Array.from({ length: totalPages }, (_, i) => i + 1).map(
788 (page) => (
789 <Button
790 key={page}
791 variant={currentPage === page ? "default" : "outline"}
792 size="sm"
793 onClick={() => setCurrentPage(page)}
794 className="w-8"
795 >
796 {page}
797 </Button>
798 )
799 )}
800 <Button
801 variant="outline"
802 size="sm"
803 onClick={() => setCurrentPage(currentPage + 1)}
804 disabled={currentPage === totalPages}
805 >
806 Next
807 </Button>
808 </div>
809 </div>
810 )}
811 </Card>
812
813 {/* Delete Confirmation Dialog */}
814 <Dialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
815 <DialogContent>
816 <DialogHeader>
817 <DialogTitle>Confirm Deletion</DialogTitle>
818 <DialogDescription>
819 Are you sure you want to delete {selectedBackups.length}{" "}
820 backup(s)? This action cannot be undone.
821 </DialogDescription>
822 </DialogHeader>
823 <DialogFooter>
824 <Button
825 variant="outline"
826 onClick={() => setShowDeleteDialog(false)}
827 >
828 Cancel
829 </Button>
830 <Button
831 variant="destructive"
832 onClick={() => handleBulkAction("delete")}
833 disabled={isLoading}
834 >
835 {isLoading && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
836 Delete
837 </Button>
838 </DialogFooter>
839 </DialogContent>
840 </Dialog>
841
842 {/* Restore Confirmation Dialog */}
843 <Dialog open={showRestoreDialog} onOpenChange={setShowRestoreDialog}>
844 <DialogContent>
845 <DialogHeader>
846 <DialogTitle>Confirm Restore</DialogTitle>
847 <DialogDescription>
848 Are you sure you want to restore {selectedBackups.length}{" "}
849 backup(s)? This will overwrite the current database(s).
850 </DialogDescription>
851 </DialogHeader>
852 <Alert className="my-4">
853 <AlertCircle className="h-4 w-4" />
854 <AlertDescription>
855 This action will replace your current database with the backup
856 data. Make sure you have a recent backup before proceeding.
857 </AlertDescription>
858 </Alert>
859 <DialogFooter>
860 <Button
861 variant="outline"
862 onClick={() => setShowRestoreDialog(false)}
863 >
864 Cancel
865 </Button>
866 <Button
867 onClick={() => handleBulkAction("restore")}
868 disabled={isLoading}
869 >
870 {isLoading && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
871 Restore
872 </Button>
873 </DialogFooter>
874 </DialogContent>
875 </Dialog>
876 </div>
877 </TooltipProvider>
878 );
879};
880
881export default DatabaseBackupDashboard;
Dependencies
External Libraries
lucide-reactreact
Shadcn/UI Components
alertbadgebuttoncardcheckboxdialogdropdown-menuinputprogressselecttabletooltip