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