ShadCN Vaults

Back to Blocks

Backup Manager with Logs

System Monitoring Block

System MonitoringComponent

Backup Manager with Logs

Database backup manager with detailed logs, status tracking, and real-time monitoring capabilities.

Preview

Full width desktop view

Code

monitoring-2.tsx
1"use client";
2
3import React, { useState, useEffect } from "react";
4import { cn } from "@/lib/utils";
5import {
6  Database,
7  Server,
8  Calendar,
9  MapPin,
10  CheckCircle,
11  AlertCircle,
12  Clock,
13  Download,
14  Eye,
15  MoreHorizontal,
16  Filter,
17  Search,
18  RefreshCw,
19  Archive,
20  Trash2,
21  Play,
22  Pause,
23  Settings,
24} from "lucide-react";
25import { Button } from "@/components/ui/button";
26import { Badge } from "@/components/ui/badge";
27import { Input } from "@/components/ui/input";
28import {
29  Table,
30  TableBody,
31  TableCell,
32  TableHead,
33  TableHeader,
34  TableRow,
35} from "@/components/ui/table";
36import {
37  Select,
38  SelectContent,
39  SelectItem,
40  SelectTrigger,
41  SelectValue,
42} from "@/components/ui/select";
43import {
44  Popover,
45  PopoverContent,
46  PopoverTrigger,
47} from "@/components/ui/popover";
48import { Card } from "@/components/ui/card";
49
50interface BackupLog {
51  id: string;
52  timestamp: string;
53  level: "info" | "warning" | "error" | "success";
54  message: string;
55}
56
57interface DatabaseBackup {
58  id: string;
59  databaseName: string;
60  backupDate: Date;
61  vpsRegion: string;
62  status: "completed" | "running" | "failed" | "scheduled";
63  size: string;
64  duration: string;
65  retentionDays: number;
66  backupType: "full" | "incremental" | "differential";
67  compressionRatio: string;
68  logs: BackupLog[];
69  nextScheduled?: Date;
70  errorMessage?: string;
71}
72
73const mockBackups: DatabaseBackup[] = [
74  {
75    id: "backup-001",
76    databaseName: "production-db",
77    backupDate: new Date("2024-01-15T02:30:00Z"),
78    vpsRegion: "us-east-1",
79    status: "completed",
80    size: "2.4 GB",
81    duration: "12m 34s",
82    retentionDays: 30,
83    backupType: "full",
84    compressionRatio: "68%",
85    nextScheduled: new Date("2024-01-16T02:30:00Z"),
86    logs: [
87      {
88        id: "log-1",
89        timestamp: "02:30:00",
90        level: "info",
91        message: "Backup process started",
92      },
93      {
94        id: "log-2",
95        timestamp: "02:35:12",
96        level: "info",
97        message: "Database locked for backup",
98      },
99      {
100        id: "log-3",
101        timestamp: "02:42:34",
102        level: "success",
103        message: "Backup completed successfully",
104      },
105    ],
106  },
107  {
108    id: "backup-002",
109    databaseName: "staging-db",
110    backupDate: new Date("2024-01-15T01:15:00Z"),
111    vpsRegion: "eu-west-1",
112    status: "running",
113    size: "1.8 GB",
114    duration: "8m 45s",
115    retentionDays: 14,
116    backupType: "incremental",
117    compressionRatio: "72%",
118    logs: [
119      {
120        id: "log-4",
121        timestamp: "01:15:00",
122        level: "info",
123        message: "Incremental backup started",
124      },
125      {
126        id: "log-5",
127        timestamp: "01:18:23",
128        level: "info",
129        message: "Analyzing changes since last backup",
130      },
131      {
132        id: "log-6",
133        timestamp: "01:23:45",
134        level: "info",
135        message: "Compressing backup data...",
136      },
137    ],
138  },
139  {
140    id: "backup-003",
141    databaseName: "analytics-db",
142    backupDate: new Date("2024-01-14T23:45:00Z"),
143    vpsRegion: "ap-southeast-1",
144    status: "failed",
145    size: "0 GB",
146    duration: "2m 15s",
147    retentionDays: 7,
148    backupType: "full",
149    compressionRatio: "0%",
150    errorMessage: "Connection timeout to database server",
151    logs: [
152      {
153        id: "log-7",
154        timestamp: "23:45:00",
155        level: "info",
156        message: "Backup process initiated",
157      },
158      {
159        id: "log-8",
160        timestamp: "23:46:30",
161        level: "warning",
162        message: "Database connection unstable",
163      },
164      {
165        id: "log-9",
166        timestamp: "23:47:15",
167        level: "error",
168        message: "Connection timeout - backup failed",
169      },
170    ],
171  },
172  {
173    id: "backup-004",
174    databaseName: "user-data-db",
175    backupDate: new Date("2024-01-16T03:00:00Z"),
176    vpsRegion: "us-west-2",
177    status: "scheduled",
178    size: "3.2 GB",
179    duration: "15m 20s",
180    retentionDays: 90,
181    backupType: "full",
182    compressionRatio: "65%",
183    logs: [],
184  },
185];
186
187const getStatusColor = (status: DatabaseBackup["status"]) => {
188  switch (status) {
189    case "completed":
190      return "bg-green-500/10 text-green-700 border-green-200 dark:text-green-400 dark:border-green-800";
191    case "running":
192      return "bg-blue-500/10 text-blue-700 border-blue-200 dark:text-blue-400 dark:border-blue-800";
193    case "failed":
194      return "bg-red-500/10 text-red-700 border-red-200 dark:text-red-400 dark:border-red-800";
195    case "scheduled":
196      return "bg-yellow-500/10 text-yellow-700 border-yellow-200 dark:text-yellow-400 dark:border-yellow-800";
197    default:
198      return "bg-gray-500/10 text-gray-700 border-gray-200 dark:text-gray-400 dark:border-gray-800";
199  }
200};
201
202const getStatusIcon = (status: DatabaseBackup["status"]) => {
203  switch (status) {
204    case "completed":
205      return <CheckCircle className="w-4 h-4" />;
206    case "running":
207      return <RefreshCw className="w-4 h-4 animate-spin" />;
208    case "failed":
209      return <AlertCircle className="w-4 h-4" />;
210    case "scheduled":
211      return <Clock className="w-4 h-4" />;
212    default:
213      return <Clock className="w-4 h-4" />;
214  }
215};
216
217const getLogLevelColor = (level: BackupLog["level"]) => {
218  switch (level) {
219    case "success":
220      return "text-green-600 dark:text-green-400";
221    case "warning":
222      return "text-yellow-600 dark:text-yellow-400";
223    case "error":
224      return "text-red-600 dark:text-red-400";
225    default:
226      return "text-blue-600 dark:text-blue-400";
227  }
228};
229
230const formatDate = (date: Date) => {
231  return new Intl.DateTimeFormat("en-US", {
232    year: "numeric",
233    month: "short",
234    day: "numeric",
235    hour: "2-digit",
236    minute: "2-digit",
237    timeZoneName: "short",
238  }).format(date);
239};
240
241const formatRelativeTime = (date: Date) => {
242  const now = new Date();
243  const diffInMinutes = Math.floor(
244    (now.getTime() - date.getTime()) / (1000 * 60)
245  );
246
247  if (diffInMinutes < 60) {
248    return `${diffInMinutes}m ago`;
249  } else if (diffInMinutes < 1440) {
250    return `${Math.floor(diffInMinutes / 60)}h ago`;
251  } else {
252    return `${Math.floor(diffInMinutes / 1440)}d ago`;
253  }
254};
255
256interface LogViewerProps {
257  logs: BackupLog[];
258  isOpen: boolean;
259  onClose: () => void;
260}
261
262const LogViewer: React.FC<LogViewerProps> = ({ logs, isOpen, onClose }) => {
263  if (!isOpen) return null;
264
265  return (
266    <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
267      <div className="bg-background border border-border rounded-lg w-full max-w-4xl max-h-[80vh] flex flex-col">
268        <div className="flex items-center justify-between p-4 border-b border-border">
269          <h3 className="text-lg font-semibold">Backup Logs</h3>
270          <Button variant="ghost" size="sm" onClick={onClose}>
271            ×
272          </Button>
273        </div>
274        <div className="flex-1 overflow-auto p-4">
275          <div className="space-y-2 font-mono text-sm">
276            {logs.length === 0 ? (
277              <p className="text-muted-foreground">No logs available</p>
278            ) : (
279              logs.map((log) => (
280                <div key={log.id} className="flex items-start gap-3 py-1">
281                  <span className="text-muted-foreground text-xs w-20 flex-shrink-0">
282                    {log.timestamp}
283                  </span>
284                  <span
285                    className={cn(
286                      "text-xs font-medium w-16 flex-shrink-0",
287                      getLogLevelColor(log.level)
288                    )}
289                  >
290                    [{log.level.toUpperCase()}]
291                  </span>
292                  <span className="text-foreground">{log.message}</span>
293                </div>
294              ))
295            )}
296          </div>
297        </div>
298      </div>
299    </div>
300  );
301};
302
303const DatabaseBackupManager: React.FC = () => {
304  const [backups, setBackups] = useState<DatabaseBackup[]>(mockBackups);
305  const [selectedBackup, setSelectedBackup] = useState<DatabaseBackup | null>(
306    null
307  );
308  const [showLogs, setShowLogs] = useState(false);
309  const [searchTerm, setSearchTerm] = useState("");
310  const [statusFilter, setStatusFilter] = useState<string>("all");
311  const [regionFilter, setRegionFilter] = useState<string>("all");
312
313  const filteredBackups = backups.filter((backup) => {
314    const matchesSearch = backup.databaseName
315      .toLowerCase()
316      .includes(searchTerm.toLowerCase());
317    const matchesStatus =
318      statusFilter === "all" || backup.status === statusFilter;
319    const matchesRegion =
320      regionFilter === "all" || backup.vpsRegion === regionFilter;
321    return matchesSearch && matchesStatus && matchesRegion;
322  });
323
324  const regions = Array.from(new Set(backups.map((b) => b.vpsRegion)));
325  const statuses = Array.from(new Set(backups.map((b) => b.status)));
326
327  const handleViewLogs = (backup: DatabaseBackup) => {
328    setSelectedBackup(backup);
329    setShowLogs(true);
330  };
331
332  const handleStartBackup = (backupId: string) => {
333    setBackups((prev) =>
334      prev.map((backup) =>
335        backup.id === backupId
336          ? { ...backup, status: "running" as const }
337          : backup
338      )
339    );
340  };
341
342  const handleStopBackup = (backupId: string) => {
343    setBackups((prev) =>
344      prev.map((backup) =>
345        backup.id === backupId
346          ? {
347              ...backup,
348              status: "failed" as const,
349              errorMessage: "Backup cancelled by user",
350            }
351          : backup
352      )
353    );
354  };
355
356  const stats = {
357    total: backups.length,
358    completed: backups.filter((b) => b.status === "completed").length,
359    running: backups.filter((b) => b.status === "running").length,
360    failed: backups.filter((b) => b.status === "failed").length,
361    scheduled: backups.filter((b) => b.status === "scheduled").length,
362  };
363
364  return (
365    <div className="w-full space-y-6 p-6 bg-background">
366      {/* Header */}
367      <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
368        <div>
369          <h1 className="text-3xl font-bold text-foreground">
370            Database Backups
371          </h1>
372          <p className="text-muted-foreground">
373            Manage and monitor your database backup operations
374          </p>
375        </div>
376        <div className="flex items-center gap-2">
377          <Button variant="outline" size="sm">
378            <Settings className="w-4 h-4 mr-2" />
379            Settings
380          </Button>
381          <Button size="sm">
382            <Database className="w-4 h-4 mr-2" />
383            New Backup
384          </Button>
385        </div>
386      </div>
387
388      {/* Stats Cards */}
389      <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4">
390        <Card className="p-4">
391          <div className="flex items-center justify-between">
392            <div>
393              <p className="text-sm text-muted-foreground">Total Backups</p>
394              <p className="text-2xl font-bold">{stats.total}</p>
395            </div>
396            <Database className="w-8 h-8 text-muted-foreground" />
397          </div>
398        </Card>
399        <Card className="p-4">
400          <div className="flex items-center justify-between">
401            <div>
402              <p className="text-sm text-muted-foreground">Completed</p>
403              <p className="text-2xl font-bold text-green-600">
404                {stats.completed}
405              </p>
406            </div>
407            <CheckCircle className="w-8 h-8 text-green-600" />
408          </div>
409        </Card>
410        <Card className="p-4">
411          <div className="flex items-center justify-between">
412            <div>
413              <p className="text-sm text-muted-foreground">Running</p>
414              <p className="text-2xl font-bold text-blue-600">
415                {stats.running}
416              </p>
417            </div>
418            <RefreshCw className="w-8 h-8 text-blue-600" />
419          </div>
420        </Card>
421        <Card className="p-4">
422          <div className="flex items-center justify-between">
423            <div>
424              <p className="text-sm text-muted-foreground">Failed</p>
425              <p className="text-2xl font-bold text-red-600">{stats.failed}</p>
426            </div>
427            <AlertCircle className="w-8 h-8 text-red-600" />
428          </div>
429        </Card>
430        <Card className="p-4">
431          <div className="flex items-center justify-between">
432            <div>
433              <p className="text-sm text-muted-foreground">Scheduled</p>
434              <p className="text-2xl font-bold text-yellow-600">
435                {stats.scheduled}
436              </p>
437            </div>
438            <Clock className="w-8 h-8 text-yellow-600" />
439          </div>
440        </Card>
441      </div>
442
443      {/* Filters */}
444      <div className="flex flex-col sm:flex-row gap-4">
445        <div className="relative flex-1">
446          <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
447          <Input
448            placeholder="Search databases..."
449            value={searchTerm}
450            onChange={(e) => setSearchTerm(e.target.value)}
451            className="pl-10"
452          />
453        </div>
454        <Select value={statusFilter} onValueChange={setStatusFilter}>
455          <SelectTrigger className="w-full sm:w-48">
456            <SelectValue placeholder="Filter by status" />
457          </SelectTrigger>
458          <SelectContent>
459            <SelectItem value="all">All Statuses</SelectItem>
460            {statuses.map((status) => (
461              <SelectItem key={status} value={status}>
462                {status.charAt(0).toUpperCase() + status.slice(1)}
463              </SelectItem>
464            ))}
465          </SelectContent>
466        </Select>
467        <Select value={regionFilter} onValueChange={setRegionFilter}>
468          <SelectTrigger className="w-full sm:w-48">
469            <SelectValue placeholder="Filter by region" />
470          </SelectTrigger>
471          <SelectContent>
472            <SelectItem value="all">All Regions</SelectItem>
473            {regions.map((region) => (
474              <SelectItem key={region} value={region}>
475                {region}
476              </SelectItem>
477            ))}
478          </SelectContent>
479        </Select>
480      </div>
481
482      {/* Backup Table */}
483      <Card>
484        <Table>
485          <TableHeader>
486            <TableRow>
487              <TableHead>Database</TableHead>
488              <TableHead>Status</TableHead>
489              <TableHead>Last Backup</TableHead>
490              <TableHead>Region</TableHead>
491              <TableHead>Size</TableHead>
492              <TableHead>Type</TableHead>
493              <TableHead>Duration</TableHead>
494              <TableHead>Next Scheduled</TableHead>
495              <TableHead>Actions</TableHead>
496            </TableRow>
497          </TableHeader>
498          <TableBody>
499            {filteredBackups.map((backup) => (
500              <TableRow key={backup.id}>
501                <TableCell>
502                  <div className="flex items-center gap-2">
503                    <Database className="w-4 h-4 text-muted-foreground" />
504                    <div>
505                      <p className="font-medium">{backup.databaseName}</p>
506                      <p className="text-sm text-muted-foreground">
507                        Retention: {backup.retentionDays} days
508                      </p>
509                    </div>
510                  </div>
511                </TableCell>
512                <TableCell>
513                  <Badge
514                    variant="outline"
515                    className={cn("gap-1", getStatusColor(backup.status))}
516                  >
517                    {getStatusIcon(backup.status)}
518                    {backup.status.charAt(0).toUpperCase() +
519                      backup.status.slice(1)}
520                  </Badge>
521                  {backup.errorMessage && (
522                    <p className="text-xs text-red-600 mt-1">
523                      {backup.errorMessage}
524                    </p>
525                  )}
526                </TableCell>
527                <TableCell>
528                  <div className="flex items-center gap-2">
529                    <Calendar className="w-4 h-4 text-muted-foreground" />
530                    <div>
531                      <p className="text-sm">
532                        {formatRelativeTime(backup.backupDate)}
533                      </p>
534                      <p className="text-xs text-muted-foreground">
535                        {formatDate(backup.backupDate)}
536                      </p>
537                    </div>
538                  </div>
539                </TableCell>
540                <TableCell>
541                  <div className="flex items-center gap-2">
542                    <MapPin className="w-4 h-4 text-muted-foreground" />
543                    <span className="text-sm">{backup.vpsRegion}</span>
544                  </div>
545                </TableCell>
546                <TableCell>
547                  <div>
548                    <p className="text-sm font-medium">{backup.size}</p>
549                    <p className="text-xs text-muted-foreground">
550                      {backup.compressionRatio} compressed
551                    </p>
552                  </div>
553                </TableCell>
554                <TableCell>
555                  <Badge variant="secondary">{backup.backupType}</Badge>
556                </TableCell>
557                <TableCell>
558                  <span className="text-sm">{backup.duration}</span>
559                </TableCell>
560                <TableCell>
561                  {backup.nextScheduled ? (
562                    <div>
563                      <p className="text-sm">
564                        {formatRelativeTime(backup.nextScheduled)}
565                      </p>
566                      <p className="text-xs text-muted-foreground">
567                        {formatDate(backup.nextScheduled)}
568                      </p>
569                    </div>
570                  ) : (
571                    <span className="text-sm text-muted-foreground">
572                      Not scheduled
573                    </span>
574                  )}
575                </TableCell>
576                <TableCell>
577                  <div className="flex items-center gap-1">
578                    <Button
579                      variant="ghost"
580                      size="sm"
581                      onClick={() => handleViewLogs(backup)}
582                    >
583                      <Eye className="w-4 h-4" />
584                    </Button>
585                    {backup.status === "completed" && (
586                      <Button variant="ghost" size="sm">
587                        <Download className="w-4 h-4" />
588                      </Button>
589                    )}
590                    {backup.status === "scheduled" && (
591                      <Button
592                        variant="ghost"
593                        size="sm"
594                        onClick={() => handleStartBackup(backup.id)}
595                      >
596                        <Play className="w-4 h-4" />
597                      </Button>
598                    )}
599                    {backup.status === "running" && (
600                      <Button
601                        variant="ghost"
602                        size="sm"
603                        onClick={() => handleStopBackup(backup.id)}
604                      >
605                        <Pause className="w-4 h-4" />
606                      </Button>
607                    )}
608                    <Popover>
609                      <PopoverTrigger asChild>
610                        <Button variant="ghost" size="sm">
611                          <MoreHorizontal className="w-4 h-4" />
612                        </Button>
613                      </PopoverTrigger>
614                      <PopoverContent className="w-48" align="end">
615                        <div className="space-y-1">
616                          <Button
617                            variant="ghost"
618                            size="sm"
619                            className="w-full justify-start"
620                          >
621                            <Archive className="w-4 h-4 mr-2" />
622                            Archive
623                          </Button>
624                          <Button
625                            variant="ghost"
626                            size="sm"
627                            className="w-full justify-start text-red-600"
628                          >
629                            <Trash2 className="w-4 h-4 mr-2" />
630                            Delete
631                          </Button>
632                        </div>
633                      </PopoverContent>
634                    </Popover>
635                  </div>
636                </TableCell>
637              </TableRow>
638            ))}
639          </TableBody>
640        </Table>
641      </Card>
642
643      {/* Log Viewer Modal */}
644      <LogViewer
645        logs={selectedBackup?.logs || []}
646        isOpen={showLogs}
647        onClose={() => setShowLogs(false)}
648      />
649    </div>
650  );
651};
652
653export default function DatabaseBackupDemo() {
654  return (
655    <div className="min-h-screen bg-background">
656      <DatabaseBackupManager />
657    </div>
658  );
659}

Dependencies

External Libraries

lucide-reactreact

Shadcn/UI Components

badgebuttoncardinputpopoverselecttable

Local Components

/lib/utils

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.

For questions about licensing, please contact the project maintainers.