ShadCN Vaults

Back to Blocks

Database Backup Dashboard

Unknown Block

UnknownComponent

Database Backup Dashboard

Comprehensive database backup management dashboard with filtering, sorting, and bulk operations.

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

LICENSE

MIT License

Copyright (c) 2025 Aldhaneka

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Shadcn Vaults Project (CC BY-NC 4.0 with Internal Use Exception)

All user-submitted components in the '/blocks' directory are licensed under the Creative Commons Attribution-NonCommercial 4.0 International License (CC BY-NC 4.0),
with the following clarification and exception:

You are free to:
- Share — copy and redistribute the material in any medium or format
- Adapt — remix, transform, and build upon the material

Under these conditions:
- Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made.
- NonCommercial — You may NOT use the material for commercial redistribution, resale, or monetization.

🚫 You MAY NOT:
- Sell or redistribute the components individually or as part of a product (e.g. a UI kit, template marketplace, SaaS component library)
- Offer the components or derivative works in any paid tool, theme pack, or design system

✅ You MAY:
- Use the components in internal company tools, dashboards, or applications that are not sold as products
- Remix or adapt components for private or enterprise projects
- Use them in open-source non-commercial projects

This license encourages sharing, learning, and internal innovation — but prohibits using these components as a basis for monetized products.

Full license text: https://creativecommons.org/licenses/by-nc/4.0/

By submitting a component, contributors agree to these terms.

Contributors

Ramiro Godoy@milogodoy

Review Form Block

Aldhaneka@Aldhanekaa

Project Creator

For questions about licensing, please contact the project maintainers.