Preview
Full width desktop view
Code
monitoring-3.tsx
1"use client";
2
3import * as React from "react";
4import { useState, useMemo } from "react";
5import {
6 Database,
7 Calendar,
8 MapPin,
9 Clock,
10 CheckCircle,
11 XCircle,
12 AlertCircle,
13 Download,
14 RotateCcw,
15 Trash2,
16 Eye,
17 Filter,
18 Search,
19 ChevronDown,
20 ChevronUp,
21 Server,
22 HardDrive,
23 Activity,
24} from "lucide-react";
25import { cn } from "@/lib/utils";
26import { Button } from "@/components/ui/button";
27import { Badge } from "@/components/ui/badge";
28import {
29 Card,
30 CardContent,
31 CardDescription,
32 CardHeader,
33 CardTitle,
34} from "@/components/ui/card";
35import { Input } from "@/components/ui/input";
36import {
37 Select,
38 SelectContent,
39 SelectItem,
40 SelectTrigger,
41 SelectValue,
42} from "@/components/ui/select";
43import {
44 Table,
45 TableBody,
46 TableCell,
47 TableHead,
48 TableHeader,
49 TableRow,
50} from "@/components/ui/table";
51import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
52
53interface BackupLog {
54 id: string;
55 timestamp: string;
56 level: "info" | "warning" | "error";
57 message: string;
58}
59
60interface DatabaseBackup {
61 id: string;
62 databaseName: string;
63 backupDate: string;
64 backupTime: string;
65 vpsRegion: string;
66 status: "completed" | "failed" | "in_progress" | "pending";
67 size: string;
68 duration: string;
69 type: "full" | "incremental" | "differential";
70 retentionDays: number;
71 compression: boolean;
72 encryption: boolean;
73 logs: BackupLog[];
74 downloadUrl?: string;
75 nextScheduled?: string;
76}
77
78const mockBackups: DatabaseBackup[] = [
79 {
80 id: "backup-001",
81 databaseName: "production-db",
82 backupDate: "2024-01-15",
83 backupTime: "02:00:00",
84 vpsRegion: "us-east-1",
85 status: "completed",
86 size: "2.4 GB",
87 duration: "12m 34s",
88 type: "full",
89 retentionDays: 30,
90 compression: true,
91 encryption: true,
92 nextScheduled: "2024-01-16 02:00:00",
93 logs: [
94 {
95 id: "log-1",
96 timestamp: "2024-01-15 02:00:00",
97 level: "info",
98 message: "Backup started",
99 },
100 {
101 id: "log-2",
102 timestamp: "2024-01-15 02:05:30",
103 level: "info",
104 message: "Database locked for backup",
105 },
106 {
107 id: "log-3",
108 timestamp: "2024-01-15 02:12:34",
109 level: "info",
110 message: "Backup completed successfully",
111 },
112 ],
113 },
114 {
115 id: "backup-002",
116 databaseName: "staging-db",
117 backupDate: "2024-01-15",
118 backupTime: "03:00:00",
119 vpsRegion: "eu-west-1",
120 status: "failed",
121 size: "0 B",
122 duration: "2m 15s",
123 type: "incremental",
124 retentionDays: 7,
125 compression: true,
126 encryption: false,
127 nextScheduled: "2024-01-16 03:00:00",
128 logs: [
129 {
130 id: "log-4",
131 timestamp: "2024-01-15 03:00:00",
132 level: "info",
133 message: "Backup started",
134 },
135 {
136 id: "log-5",
137 timestamp: "2024-01-15 03:01:45",
138 level: "warning",
139 message: "Connection timeout detected",
140 },
141 {
142 id: "log-6",
143 timestamp: "2024-01-15 03:02:15",
144 level: "error",
145 message: "Backup failed: Database connection lost",
146 },
147 ],
148 },
149 {
150 id: "backup-003",
151 databaseName: "analytics-db",
152 backupDate: "2024-01-15",
153 backupTime: "04:00:00",
154 vpsRegion: "ap-southeast-1",
155 status: "in_progress",
156 size: "1.8 GB",
157 duration: "8m 22s",
158 type: "differential",
159 retentionDays: 14,
160 compression: true,
161 encryption: true,
162 nextScheduled: "2024-01-16 04:00:00",
163 logs: [
164 {
165 id: "log-7",
166 timestamp: "2024-01-15 04:00:00",
167 level: "info",
168 message: "Backup started",
169 },
170 {
171 id: "log-8",
172 timestamp: "2024-01-15 04:03:12",
173 level: "info",
174 message: "Processing table: users",
175 },
176 {
177 id: "log-9",
178 timestamp: "2024-01-15 04:06:45",
179 level: "info",
180 message: "Processing table: transactions",
181 },
182 ],
183 },
184 {
185 id: "backup-004",
186 databaseName: "user-db",
187 backupDate: "2024-01-14",
188 backupTime: "02:00:00",
189 vpsRegion: "us-west-2",
190 status: "completed",
191 size: "856 MB",
192 duration: "5m 18s",
193 type: "full",
194 retentionDays: 30,
195 compression: true,
196 encryption: true,
197 nextScheduled: "2024-01-15 02:00:00",
198 logs: [
199 {
200 id: "log-10",
201 timestamp: "2024-01-14 02:00:00",
202 level: "info",
203 message: "Backup started",
204 },
205 {
206 id: "log-11",
207 timestamp: "2024-01-14 02:05:18",
208 level: "info",
209 message: "Backup completed successfully",
210 },
211 ],
212 },
213];
214
215function StatusBadge({ status }: { status: DatabaseBackup["status"] }) {
216 const variants = {
217 completed: {
218 variant: "default" as const,
219 icon: CheckCircle,
220 className: "bg-green-100 text-green-800 border-green-200",
221 },
222 failed: {
223 variant: "destructive" as const,
224 icon: XCircle,
225 className: "bg-red-100 text-red-800 border-red-200",
226 },
227 in_progress: {
228 variant: "secondary" as const,
229 icon: Activity,
230 className: "bg-blue-100 text-blue-800 border-blue-200",
231 },
232 pending: {
233 variant: "outline" as const,
234 icon: Clock,
235 className: "bg-yellow-100 text-yellow-800 border-yellow-200",
236 },
237 };
238
239 const config = variants[status];
240 const Icon = config.icon;
241
242 return (
243 <Badge
244 variant={config.variant}
245 className={cn("flex items-center gap-1", config.className)}
246 >
247 <Icon className="h-3 w-3" />
248 {status.replace("_", " ")}
249 </Badge>
250 );
251}
252
253function TypeBadge({ type }: { type: DatabaseBackup["type"] }) {
254 const variants = {
255 full: "bg-purple-100 text-purple-800 border-purple-200",
256 incremental: "bg-blue-100 text-blue-800 border-blue-200",
257 differential: "bg-orange-100 text-orange-800 border-orange-200",
258 };
259
260 return (
261 <Badge variant="outline" className={variants[type]}>
262 {type}
263 </Badge>
264 );
265}
266
267function LogLevelBadge({ level }: { level: BackupLog["level"] }) {
268 const variants = {
269 info: "bg-blue-100 text-blue-800 border-blue-200",
270 warning: "bg-yellow-100 text-yellow-800 border-yellow-200",
271 error: "bg-red-100 text-red-800 border-red-200",
272 };
273
274 return (
275 <Badge variant="outline" className={cn("text-xs", variants[level])}>
276 {level}
277 </Badge>
278 );
279}
280
281interface DatabaseBackupManagerProps {
282 backups?: DatabaseBackup[];
283}
284
285export function DatabaseBackupManager({
286 backups = mockBackups,
287}: DatabaseBackupManagerProps) {
288 const [searchTerm, setSearchTerm] = useState("");
289 const [statusFilter, setStatusFilter] = useState<string>("all");
290 const [regionFilter, setRegionFilter] = useState<string>("all");
291 const [selectedBackup, setSelectedBackup] = useState<DatabaseBackup | null>(
292 null
293 );
294 const [sortConfig, setSortConfig] = useState<{
295 key: keyof DatabaseBackup | null;
296 direction: "asc" | "desc";
297 }>({ key: null, direction: "asc" });
298
299 const filteredBackups = useMemo(() => {
300 return backups.filter((backup) => {
301 const matchesSearch =
302 backup.databaseName.toLowerCase().includes(searchTerm.toLowerCase()) ||
303 backup.vpsRegion.toLowerCase().includes(searchTerm.toLowerCase());
304 const matchesStatus =
305 statusFilter === "all" || backup.status === statusFilter;
306 const matchesRegion =
307 regionFilter === "all" || backup.vpsRegion === regionFilter;
308
309 return matchesSearch && matchesStatus && matchesRegion;
310 });
311 }, [backups, searchTerm, statusFilter, regionFilter]);
312
313 const sortedBackups = useMemo(() => {
314 if (!sortConfig.key) return filteredBackups;
315
316 return [...filteredBackups].sort((a, b) => {
317 const aValue = a[sortConfig.key!];
318 const bValue = b[sortConfig.key!];
319
320 // Handle undefined values
321 if (aValue === undefined && bValue === undefined) return 0;
322 if (aValue === undefined) return sortConfig.direction === "asc" ? 1 : -1;
323 if (bValue === undefined) return sortConfig.direction === "asc" ? -1 : 1;
324
325 if (aValue < bValue) {
326 return sortConfig.direction === "asc" ? -1 : 1;
327 }
328 if (aValue > bValue) {
329 return sortConfig.direction === "asc" ? 1 : -1;
330 }
331 return 0;
332 });
333 }, [filteredBackups, sortConfig]);
334
335 const handleSort = (key: keyof DatabaseBackup) => {
336 setSortConfig((current) => ({
337 key,
338 direction:
339 current.key === key && current.direction === "asc" ? "desc" : "asc",
340 }));
341 };
342
343 const uniqueRegions = Array.from(
344 new Set(backups.map((backup) => backup.vpsRegion))
345 );
346
347 const stats = {
348 total: backups.length,
349 completed: backups.filter((b) => b.status === "completed").length,
350 failed: backups.filter((b) => b.status === "failed").length,
351 inProgress: backups.filter((b) => b.status === "in_progress").length,
352 totalSize: backups.reduce((acc, backup) => {
353 const sizeValue = parseFloat(backup.size.split(" ")[0]);
354 const unit = backup.size.split(" ")[1];
355 if (unit === "GB") return acc + sizeValue * 1024;
356 if (unit === "MB") return acc + sizeValue;
357 return acc;
358 }, 0),
359 };
360
361 return (
362 <div className="w-full max-w-7xl mx-auto p-6 space-y-6">
363 {/* Header */}
364 <div className="flex flex-col gap-4">
365 <div className="flex items-center gap-2">
366 <Database className="h-8 w-8 text-primary" />
367 <div>
368 <h1 className="text-3xl font-bold">Database Backup Manager</h1>
369 <p className="text-muted-foreground">
370 Monitor and manage your database backups across all regions
371 </p>
372 </div>
373 </div>
374
375 {/* Stats Cards */}
376 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
377 <Card>
378 <CardContent className="p-4">
379 <div className="flex items-center gap-2">
380 <HardDrive className="h-5 w-5 text-blue-500" />
381 <div>
382 <p className="text-sm text-muted-foreground">Total Backups</p>
383 <p className="text-2xl font-bold">{stats.total}</p>
384 </div>
385 </div>
386 </CardContent>
387 </Card>
388 <Card>
389 <CardContent className="p-4">
390 <div className="flex items-center gap-2">
391 <CheckCircle className="h-5 w-5 text-green-500" />
392 <div>
393 <p className="text-sm text-muted-foreground">Completed</p>
394 <p className="text-2xl font-bold">{stats.completed}</p>
395 </div>
396 </div>
397 </CardContent>
398 </Card>
399 <Card>
400 <CardContent className="p-4">
401 <div className="flex items-center gap-2">
402 <XCircle className="h-5 w-5 text-red-500" />
403 <div>
404 <p className="text-sm text-muted-foreground">Failed</p>
405 <p className="text-2xl font-bold">{stats.failed}</p>
406 </div>
407 </div>
408 </CardContent>
409 </Card>
410 <Card>
411 <CardContent className="p-4">
412 <div className="flex items-center gap-2">
413 <Server className="h-5 w-5 text-purple-500" />
414 <div>
415 <p className="text-sm text-muted-foreground">Total Size</p>
416 <p className="text-2xl font-bold">
417 {(stats.totalSize / 1024).toFixed(1)} GB
418 </p>
419 </div>
420 </div>
421 </CardContent>
422 </Card>
423 </div>
424 </div>
425
426 {/* Filters */}
427 <Card>
428 <CardContent className="p-4">
429 <div className="flex flex-col sm:flex-row gap-4">
430 <div className="relative flex-1">
431 <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
432 <Input
433 placeholder="Search databases or regions..."
434 value={searchTerm}
435 onChange={(e) => setSearchTerm(e.target.value)}
436 className="pl-10"
437 />
438 </div>
439 <Select value={statusFilter} onValueChange={setStatusFilter}>
440 <SelectTrigger className="w-full sm:w-[180px]">
441 <SelectValue placeholder="Filter by status" />
442 </SelectTrigger>
443 <SelectContent>
444 <SelectItem value="all">All Statuses</SelectItem>
445 <SelectItem value="completed">Completed</SelectItem>
446 <SelectItem value="failed">Failed</SelectItem>
447 <SelectItem value="in_progress">In Progress</SelectItem>
448 <SelectItem value="pending">Pending</SelectItem>
449 </SelectContent>
450 </Select>
451 <Select value={regionFilter} onValueChange={setRegionFilter}>
452 <SelectTrigger className="w-full sm:w-[180px]">
453 <SelectValue placeholder="Filter by region" />
454 </SelectTrigger>
455 <SelectContent>
456 <SelectItem value="all">All Regions</SelectItem>
457 {uniqueRegions.map((region) => (
458 <SelectItem key={region} value={region}>
459 {region}
460 </SelectItem>
461 ))}
462 </SelectContent>
463 </Select>
464 </div>
465 </CardContent>
466 </Card>
467
468 {/* Main Content */}
469 <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
470 {/* Backup List */}
471 <div className="lg:col-span-2">
472 <Card>
473 <CardHeader>
474 <CardTitle>Backup History</CardTitle>
475 <CardDescription>
476 Recent database backup operations
477 </CardDescription>
478 </CardHeader>
479 <CardContent className="p-0">
480 <Table>
481 <TableHeader>
482 <TableRow>
483 <TableHead
484 className="cursor-pointer hover:bg-muted/50"
485 onClick={() => handleSort("databaseName")}
486 >
487 <div className="flex items-center gap-2">
488 Database
489 {sortConfig.key === "databaseName" &&
490 (sortConfig.direction === "asc" ? (
491 <ChevronUp className="h-4 w-4" />
492 ) : (
493 <ChevronDown className="h-4 w-4" />
494 ))}
495 </div>
496 </TableHead>
497 <TableHead
498 className="cursor-pointer hover:bg-muted/50"
499 onClick={() => handleSort("backupDate")}
500 >
501 <div className="flex items-center gap-2">
502 Date/Time
503 {sortConfig.key === "backupDate" &&
504 (sortConfig.direction === "asc" ? (
505 <ChevronUp className="h-4 w-4" />
506 ) : (
507 <ChevronDown className="h-4 w-4" />
508 ))}
509 </div>
510 </TableHead>
511 <TableHead>Status</TableHead>
512 <TableHead>Region</TableHead>
513 <TableHead>Size</TableHead>
514 <TableHead>Actions</TableHead>
515 </TableRow>
516 </TableHeader>
517 <TableBody>
518 {sortedBackups.map((backup) => (
519 <TableRow
520 key={backup.id}
521 className="cursor-pointer hover:bg-muted/50"
522 onClick={() => setSelectedBackup(backup)}
523 >
524 <TableCell>
525 <div className="flex flex-col">
526 <span className="font-medium">
527 {backup.databaseName}
528 </span>
529 <div className="flex items-center gap-2 mt-1">
530 <TypeBadge type={backup.type} />
531 {backup.encryption && (
532 <Badge
533 variant="outline"
534 className="text-xs bg-green-50 text-green-700 border-green-200"
535 >
536 Encrypted
537 </Badge>
538 )}
539 </div>
540 </div>
541 </TableCell>
542 <TableCell>
543 <div className="flex items-center gap-2">
544 <Calendar className="h-4 w-4 text-muted-foreground" />
545 <div className="flex flex-col">
546 <span className="text-sm">{backup.backupDate}</span>
547 <span className="text-xs text-muted-foreground">
548 {backup.backupTime}
549 </span>
550 </div>
551 </div>
552 </TableCell>
553 <TableCell>
554 <StatusBadge status={backup.status} />
555 </TableCell>
556 <TableCell>
557 <div className="flex items-center gap-2">
558 <MapPin className="h-4 w-4 text-muted-foreground" />
559 <span className="text-sm">{backup.vpsRegion}</span>
560 </div>
561 </TableCell>
562 <TableCell>
563 <div className="flex flex-col">
564 <span className="text-sm font-medium">
565 {backup.size}
566 </span>
567 <span className="text-xs text-muted-foreground">
568 {backup.duration}
569 </span>
570 </div>
571 </TableCell>
572 <TableCell>
573 <div className="flex items-center gap-1">
574 <Button
575 variant="ghost"
576 size="sm"
577 onClick={(e) => {
578 e.stopPropagation();
579 setSelectedBackup(backup);
580 }}
581 >
582 <Eye className="h-4 w-4" />
583 </Button>
584 {backup.status === "completed" && (
585 <Button variant="ghost" size="sm">
586 <Download className="h-4 w-4" />
587 </Button>
588 )}
589 {backup.status === "failed" && (
590 <Button variant="ghost" size="sm">
591 <RotateCcw className="h-4 w-4" />
592 </Button>
593 )}
594 </div>
595 </TableCell>
596 </TableRow>
597 ))}
598 </TableBody>
599 </Table>
600 </CardContent>
601 </Card>
602 </div>
603
604 {/* Backup Details */}
605 <div>
606 <Card>
607 <CardHeader>
608 <CardTitle>Backup Details</CardTitle>
609 <CardDescription>
610 {selectedBackup
611 ? `Details for ${selectedBackup.databaseName}`
612 : "Select a backup to view details"}
613 </CardDescription>
614 </CardHeader>
615 <CardContent>
616 {selectedBackup ? (
617 <Tabs defaultValue="overview" className="w-full">
618 <TabsList className="grid w-full grid-cols-2">
619 <TabsTrigger value="overview">Overview</TabsTrigger>
620 <TabsTrigger value="logs">Logs</TabsTrigger>
621 </TabsList>
622 <TabsContent value="overview" className="space-y-4">
623 <div className="space-y-3">
624 <div className="flex justify-between">
625 <span className="text-sm text-muted-foreground">
626 Database:
627 </span>
628 <span className="text-sm font-medium">
629 {selectedBackup.databaseName}
630 </span>
631 </div>
632 <div className="flex justify-between">
633 <span className="text-sm text-muted-foreground">
634 Status:
635 </span>
636 <StatusBadge status={selectedBackup.status} />
637 </div>
638 <div className="flex justify-between">
639 <span className="text-sm text-muted-foreground">
640 Type:
641 </span>
642 <TypeBadge type={selectedBackup.type} />
643 </div>
644 <div className="flex justify-between">
645 <span className="text-sm text-muted-foreground">
646 Region:
647 </span>
648 <span className="text-sm">
649 {selectedBackup.vpsRegion}
650 </span>
651 </div>
652 <div className="flex justify-between">
653 <span className="text-sm text-muted-foreground">
654 Size:
655 </span>
656 <span className="text-sm font-medium">
657 {selectedBackup.size}
658 </span>
659 </div>
660 <div className="flex justify-between">
661 <span className="text-sm text-muted-foreground">
662 Duration:
663 </span>
664 <span className="text-sm">
665 {selectedBackup.duration}
666 </span>
667 </div>
668 <div className="flex justify-between">
669 <span className="text-sm text-muted-foreground">
670 Retention:
671 </span>
672 <span className="text-sm">
673 {selectedBackup.retentionDays} days
674 </span>
675 </div>
676 <div className="flex justify-between">
677 <span className="text-sm text-muted-foreground">
678 Compression:
679 </span>
680 <Badge
681 variant={
682 selectedBackup.compression ? "default" : "secondary"
683 }
684 className="text-xs"
685 >
686 {selectedBackup.compression ? "Enabled" : "Disabled"}
687 </Badge>
688 </div>
689 <div className="flex justify-between">
690 <span className="text-sm text-muted-foreground">
691 Encryption:
692 </span>
693 <Badge
694 variant={
695 selectedBackup.encryption ? "default" : "secondary"
696 }
697 className="text-xs"
698 >
699 {selectedBackup.encryption ? "Enabled" : "Disabled"}
700 </Badge>
701 </div>
702 {selectedBackup.nextScheduled && (
703 <div className="flex justify-between">
704 <span className="text-sm text-muted-foreground">
705 Next Backup:
706 </span>
707 <span className="text-sm">
708 {selectedBackup.nextScheduled}
709 </span>
710 </div>
711 )}
712 </div>
713 {selectedBackup.status === "completed" && (
714 <div className="pt-4 space-y-2">
715 <Button className="w-full" size="sm">
716 <Download className="h-4 w-4 mr-2" />
717 Download Backup
718 </Button>
719 <Button variant="outline" className="w-full" size="sm">
720 <RotateCcw className="h-4 w-4 mr-2" />
721 Restore Database
722 </Button>
723 </div>
724 )}
725 </TabsContent>
726 <TabsContent value="logs" className="space-y-3">
727 <div className="max-h-64 overflow-y-auto space-y-2">
728 {selectedBackup.logs.map((log) => (
729 <div
730 key={log.id}
731 className="p-3 border rounded-lg bg-muted/30"
732 >
733 <div className="flex items-center justify-between mb-1">
734 <LogLevelBadge level={log.level} />
735 <span className="text-xs text-muted-foreground">
736 {log.timestamp}
737 </span>
738 </div>
739 <p className="text-sm">{log.message}</p>
740 </div>
741 ))}
742 </div>
743 </TabsContent>
744 </Tabs>
745 ) : (
746 <div className="text-center py-8">
747 <Database className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
748 <p className="text-sm text-muted-foreground">
749 Select a backup from the list to view details
750 </p>
751 </div>
752 )}
753 </CardContent>
754 </Card>
755 </div>
756 </div>
757 </div>
758 );
759}
760
761export default function Component() {
762 return <DatabaseBackupManager />;
763}
Dependencies
External Libraries
lucide-reactreact
Shadcn/UI Components
badgebuttoncardinputselecttabletabs
Local Components
/lib/utils