Preview
Full width desktop view
Code
monitoring-4.tsx
1"use client";
2
3import * as React from "react";
4import { format } from "date-fns";
5import {
6 Database,
7 Server,
8 Calendar,
9 MapPin,
10 CheckCircle,
11 XCircle,
12 Clock,
13 Download,
14 Eye,
15 MoreHorizontal,
16 AlertTriangle,
17 FileText,
18 HardDrive,
19 Activity,
20} from "lucide-react";
21import { cn } from "@/lib/utils";
22
23// Types
24interface BackupLog {
25 id: string;
26 timestamp: string;
27 level: "info" | "warning" | "error" | "success";
28 message: string;
29}
30
31interface DatabaseBackup {
32 id: string;
33 databaseName: string;
34 backupDate: string;
35 vpsRegion: string;
36 status: "completed" | "failed" | "in-progress" | "scheduled";
37 size: string;
38 duration: string;
39 backupType: "full" | "incremental" | "differential";
40 retentionDays: number;
41 compressionRatio: string;
42 logs: BackupLog[];
43 nextScheduled?: string;
44}
45
46// Mock data
47const mockBackups: DatabaseBackup[] = [
48 {
49 id: "backup-001",
50 databaseName: "production_db",
51 backupDate: "2024-01-15T02:00:00Z",
52 vpsRegion: "US-East-1",
53 status: "completed",
54 size: "2.4 GB",
55 duration: "12m 34s",
56 backupType: "full",
57 retentionDays: 30,
58 compressionRatio: "65%",
59 nextScheduled: "2024-01-16T02:00:00Z",
60 logs: [
61 {
62 id: "log-1",
63 timestamp: "2024-01-15T02:00:00Z",
64 level: "info",
65 message: "Backup started",
66 },
67 {
68 id: "log-2",
69 timestamp: "2024-01-15T02:05:00Z",
70 level: "info",
71 message: "Database locked for backup",
72 },
73 {
74 id: "log-3",
75 timestamp: "2024-01-15T02:12:34Z",
76 level: "success",
77 message: "Backup completed successfully",
78 },
79 ],
80 },
81 {
82 id: "backup-002",
83 databaseName: "analytics_db",
84 backupDate: "2024-01-15T03:00:00Z",
85 vpsRegion: "EU-West-1",
86 status: "failed",
87 size: "1.8 GB",
88 duration: "8m 12s",
89 backupType: "incremental",
90 retentionDays: 14,
91 compressionRatio: "72%",
92 nextScheduled: "2024-01-16T03:00:00Z",
93 logs: [
94 {
95 id: "log-4",
96 timestamp: "2024-01-15T03:00:00Z",
97 level: "info",
98 message: "Backup started",
99 },
100 {
101 id: "log-5",
102 timestamp: "2024-01-15T03:05:00Z",
103 level: "warning",
104 message: "High memory usage detected",
105 },
106 {
107 id: "log-6",
108 timestamp: "2024-01-15T03:08:12Z",
109 level: "error",
110 message: "Backup failed: Connection timeout",
111 },
112 ],
113 },
114 {
115 id: "backup-003",
116 databaseName: "user_sessions",
117 backupDate: "2024-01-15T04:00:00Z",
118 vpsRegion: "Asia-Pacific-1",
119 status: "in-progress",
120 size: "856 MB",
121 duration: "5m 23s",
122 backupType: "differential",
123 retentionDays: 7,
124 compressionRatio: "58%",
125 logs: [
126 {
127 id: "log-7",
128 timestamp: "2024-01-15T04:00:00Z",
129 level: "info",
130 message: "Backup started",
131 },
132 {
133 id: "log-8",
134 timestamp: "2024-01-15T04:03:00Z",
135 level: "info",
136 message: "Processing table: user_sessions",
137 },
138 ],
139 },
140 {
141 id: "backup-004",
142 databaseName: "inventory_db",
143 backupDate: "2024-01-14T02:00:00Z",
144 vpsRegion: "US-West-2",
145 status: "completed",
146 size: "4.2 GB",
147 duration: "18m 45s",
148 backupType: "full",
149 retentionDays: 90,
150 compressionRatio: "68%",
151 logs: [
152 {
153 id: "log-9",
154 timestamp: "2024-01-14T02:00:00Z",
155 level: "info",
156 message: "Backup started",
157 },
158 {
159 id: "log-10",
160 timestamp: "2024-01-14T02:18:45Z",
161 level: "success",
162 message: "Backup completed successfully",
163 },
164 ],
165 },
166];
167
168// Components
169const Badge = React.forwardRef<
170 HTMLDivElement,
171 React.HTMLAttributes<HTMLDivElement> & {
172 variant?: "default" | "secondary" | "destructive" | "outline";
173 }
174>(({ className, variant = "default", ...props }, ref) => {
175 const variants = {
176 default: "bg-primary text-primary-foreground",
177 secondary: "bg-secondary text-secondary-foreground",
178 destructive: "bg-destructive text-destructive-foreground",
179 outline: "text-foreground border border-border",
180 };
181
182 return (
183 <div
184 ref={ref}
185 className={cn(
186 "inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold transition-colors",
187 variants[variant],
188 className
189 )}
190 {...props}
191 />
192 );
193});
194Badge.displayName = "Badge";
195
196const Button = React.forwardRef<
197 HTMLButtonElement,
198 React.ButtonHTMLAttributes<HTMLButtonElement> & {
199 variant?:
200 | "default"
201 | "destructive"
202 | "outline"
203 | "secondary"
204 | "ghost"
205 | "link";
206 size?: "default" | "sm" | "lg" | "icon";
207 }
208>(({ className, variant = "default", size = "default", ...props }, ref) => {
209 const variants = {
210 default: "bg-primary text-primary-foreground hover:bg-primary/90",
211 destructive:
212 "bg-destructive text-destructive-foreground hover:bg-destructive/90",
213 outline:
214 "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
215 secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
216 ghost: "hover:bg-accent hover:text-accent-foreground",
217 link: "text-primary underline-offset-4 hover:underline",
218 };
219
220 const sizes = {
221 default: "h-10 px-4 py-2",
222 sm: "h-9 rounded-md px-3",
223 lg: "h-11 rounded-md px-8",
224 icon: "h-10 w-10",
225 };
226
227 return (
228 <button
229 ref={ref}
230 className={cn(
231 "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
232 variants[variant],
233 sizes[size],
234 className
235 )}
236 {...props}
237 />
238 );
239});
240Button.displayName = "Button";
241
242const Card = React.forwardRef<
243 HTMLDivElement,
244 React.HTMLAttributes<HTMLDivElement>
245>(({ className, ...props }, ref) => (
246 <div
247 ref={ref}
248 className={cn(
249 "rounded-lg border border-border bg-card text-card-foreground shadow-sm",
250 className
251 )}
252 {...props}
253 />
254));
255Card.displayName = "Card";
256
257const CardHeader = React.forwardRef<
258 HTMLDivElement,
259 React.HTMLAttributes<HTMLDivElement>
260>(({ className, ...props }, ref) => (
261 <div
262 ref={ref}
263 className={cn("flex flex-col space-y-1.5 p-6", className)}
264 {...props}
265 />
266));
267CardHeader.displayName = "CardHeader";
268
269const CardTitle = React.forwardRef<
270 HTMLParagraphElement,
271 React.HTMLAttributes<HTMLHeadingElement>
272>(({ className, ...props }, ref) => (
273 <h3
274 ref={ref}
275 className={cn(
276 "text-2xl font-semibold leading-none tracking-tight",
277 className
278 )}
279 {...props}
280 />
281));
282CardTitle.displayName = "CardTitle";
283
284const CardContent = React.forwardRef<
285 HTMLDivElement,
286 React.HTMLAttributes<HTMLDivElement>
287>(({ className, ...props }, ref) => (
288 <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
289));
290CardContent.displayName = "CardContent";
291
292const Tabs = React.forwardRef<
293 HTMLDivElement,
294 React.HTMLAttributes<HTMLDivElement> & {
295 defaultValue?: string;
296 value?: string;
297 onValueChange?: (value: string) => void;
298 }
299>(({ className, defaultValue, value, onValueChange, ...props }, ref) => {
300 const [internalValue, setInternalValue] = React.useState(defaultValue || "");
301 const currentValue = value !== undefined ? value : internalValue;
302
303 const handleValueChange = (newValue: string) => {
304 if (value === undefined) {
305 setInternalValue(newValue);
306 }
307 onValueChange?.(newValue);
308 };
309
310 return (
311 <div
312 ref={ref}
313 className={cn("w-full", className)}
314 data-value={currentValue}
315 {...props}
316 >
317 {React.Children.map(props.children, (child) => {
318 if (!React.isValidElement(child)) return child;
319 // Only pass value/onValueChange/currentValue to TabsTrigger and TabsContent
320 const displayName = (child.type as any).displayName;
321 if (displayName === "TabsTrigger") {
322 return React.cloneElement(child as React.ReactElement<any>, {
323 value: (child.props as any).value,
324 onValueChange: handleValueChange,
325 currentValue: currentValue,
326 });
327 }
328 if (displayName === "TabsContent") {
329 return React.cloneElement(child as React.ReactElement<any>, {
330 value: (child.props as any).value,
331 currentValue: currentValue,
332 });
333 }
334 // For TabsList and others, just return as is
335 return child;
336 })}
337 </div>
338 );
339});
340Tabs.displayName = "Tabs";
341
342const TabsList = React.forwardRef<
343 HTMLDivElement,
344 React.HTMLAttributes<HTMLDivElement>
345>(({ className, ...props }, ref) => (
346 <div
347 ref={ref}
348 className={cn(
349 "inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
350 className
351 )}
352 {...props}
353 />
354));
355TabsList.displayName = "TabsList";
356
357const TabsTrigger = React.forwardRef<
358 HTMLButtonElement,
359 React.ButtonHTMLAttributes<HTMLButtonElement> & {
360 value: string;
361 onValueChange?: (value: string) => void;
362 currentValue?: string;
363 }
364>(({ className, value, onValueChange, currentValue, ...props }, ref) => (
365 <button
366 ref={ref}
367 className={cn(
368 "inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
369 currentValue === value
370 ? "bg-background text-foreground shadow-sm"
371 : "hover:bg-background/50",
372 className
373 )}
374 onClick={() => onValueChange?.(value)}
375 {...props}
376 />
377));
378TabsTrigger.displayName = "TabsTrigger";
379
380const TabsContent = React.forwardRef<
381 HTMLDivElement,
382 React.HTMLAttributes<HTMLDivElement> & {
383 value: string;
384 currentValue?: string;
385 }
386>(({ className, value, currentValue, ...props }, ref) => {
387 if (currentValue !== value) return null;
388
389 return (
390 <div
391 ref={ref}
392 className={cn(
393 "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
394 className
395 )}
396 {...props}
397 />
398 );
399});
400TabsContent.displayName = "TabsContent";
401
402// Status components
403const StatusIcon = ({ status }: { status: DatabaseBackup["status"] }) => {
404 switch (status) {
405 case "completed":
406 return <CheckCircle className="h-4 w-4 text-green-500" />;
407 case "failed":
408 return <XCircle className="h-4 w-4 text-red-500" />;
409 case "in-progress":
410 return <Clock className="h-4 w-4 text-blue-500" />;
411 case "scheduled":
412 return <Calendar className="h-4 w-4 text-yellow-500" />;
413 default:
414 return <Clock className="h-4 w-4 text-gray-500" />;
415 }
416};
417
418const StatusBadge = ({ status }: { status: DatabaseBackup["status"] }) => {
419 const variants = {
420 completed: "bg-green-100 text-green-800 border-green-200",
421 failed: "bg-red-100 text-red-800 border-red-200",
422 "in-progress": "bg-blue-100 text-blue-800 border-blue-200",
423 scheduled: "bg-yellow-100 text-yellow-800 border-yellow-200",
424 };
425
426 return (
427 <Badge className={cn("border", variants[status])}>
428 <StatusIcon status={status} />
429 <span className="ml-1 capitalize">{status.replace("-", " ")}</span>
430 </Badge>
431 );
432};
433
434// Log level components
435const LogLevelIcon = ({ level }: { level: BackupLog["level"] }) => {
436 switch (level) {
437 case "success":
438 return <CheckCircle className="h-3 w-3 text-green-500" />;
439 case "error":
440 return <XCircle className="h-3 w-3 text-red-500" />;
441 case "warning":
442 return <AlertTriangle className="h-3 w-3 text-yellow-500" />;
443 case "info":
444 default:
445 return <FileText className="h-3 w-3 text-blue-500" />;
446 }
447};
448
449// Backup card component
450const BackupCard = ({ backup }: { backup: DatabaseBackup }) => {
451 const [selectedTab, setSelectedTab] = React.useState("overview");
452
453 return (
454 <Card className="w-full">
455 <CardHeader className="pb-4">
456 <div className="flex items-center justify-between">
457 <div className="flex items-center space-x-3">
458 <div className="p-2 bg-blue-100 rounded-lg">
459 <Database className="h-5 w-5 text-blue-600" />
460 </div>
461 <div>
462 <CardTitle className="text-lg">{backup.databaseName}</CardTitle>
463 <p className="text-sm text-muted-foreground">
464 {format(new Date(backup.backupDate), "PPp")}
465 </p>
466 </div>
467 </div>
468 <div className="flex items-center space-x-2">
469 <StatusBadge status={backup.status} />
470 <Button variant="ghost" size="icon">
471 <MoreHorizontal className="h-4 w-4" />
472 </Button>
473 </div>
474 </div>
475 </CardHeader>
476
477 <CardContent>
478 <Tabs value={selectedTab} onValueChange={setSelectedTab}>
479 <TabsList className="grid w-full grid-cols-3">
480 <TabsTrigger value="overview" currentValue={selectedTab}>
481 Overview
482 </TabsTrigger>
483 <TabsTrigger value="details" currentValue={selectedTab}>
484 Details
485 </TabsTrigger>
486 <TabsTrigger value="logs" currentValue={selectedTab}>
487 Logs
488 </TabsTrigger>
489 </TabsList>
490
491 <TabsContent value="overview" currentValue={selectedTab}>
492 <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-4">
493 <div className="space-y-1">
494 <div className="flex items-center text-sm text-muted-foreground">
495 <MapPin className="h-3 w-3 mr-1" />
496 Region
497 </div>
498 <p className="font-medium">{backup.vpsRegion}</p>
499 </div>
500 <div className="space-y-1">
501 <div className="flex items-center text-sm text-muted-foreground">
502 <HardDrive className="h-3 w-3 mr-1" />
503 Size
504 </div>
505 <p className="font-medium">{backup.size}</p>
506 </div>
507 <div className="space-y-1">
508 <div className="flex items-center text-sm text-muted-foreground">
509 <Clock className="h-3 w-3 mr-1" />
510 Duration
511 </div>
512 <p className="font-medium">{backup.duration}</p>
513 </div>
514 <div className="space-y-1">
515 <div className="flex items-center text-sm text-muted-foreground">
516 <Activity className="h-3 w-3 mr-1" />
517 Type
518 </div>
519 <p className="font-medium capitalize">{backup.backupType}</p>
520 </div>
521 </div>
522
523 {backup.nextScheduled && (
524 <div className="mt-4 p-3 bg-blue-50 rounded-lg border border-blue-200">
525 <p className="text-sm text-blue-800">
526 <Calendar className="h-4 w-4 inline mr-1" />
527 Next backup scheduled:{" "}
528 {format(new Date(backup.nextScheduled), "PPp")}
529 </p>
530 </div>
531 )}
532 </TabsContent>
533
534 <TabsContent value="details" currentValue={selectedTab}>
535 <div className="space-y-4 mt-4">
536 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
537 <div className="space-y-3">
538 <h4 className="font-medium text-sm text-muted-foreground">
539 Backup Configuration
540 </h4>
541 <div className="space-y-2">
542 <div className="flex justify-between">
543 <span className="text-sm">Backup Type:</span>
544 <span className="text-sm font-medium capitalize">
545 {backup.backupType}
546 </span>
547 </div>
548 <div className="flex justify-between">
549 <span className="text-sm">Retention Period:</span>
550 <span className="text-sm font-medium">
551 {backup.retentionDays} days
552 </span>
553 </div>
554 <div className="flex justify-between">
555 <span className="text-sm">Compression Ratio:</span>
556 <span className="text-sm font-medium">
557 {backup.compressionRatio}
558 </span>
559 </div>
560 </div>
561 </div>
562 <div className="space-y-3">
563 <h4 className="font-medium text-sm text-muted-foreground">
564 Infrastructure
565 </h4>
566 <div className="space-y-2">
567 <div className="flex justify-between">
568 <span className="text-sm">VPS Region:</span>
569 <span className="text-sm font-medium">
570 {backup.vpsRegion}
571 </span>
572 </div>
573 <div className="flex justify-between">
574 <span className="text-sm">Backup Size:</span>
575 <span className="text-sm font-medium">{backup.size}</span>
576 </div>
577 <div className="flex justify-between">
578 <span className="text-sm">Duration:</span>
579 <span className="text-sm font-medium">
580 {backup.duration}
581 </span>
582 </div>
583 </div>
584 </div>
585 </div>
586
587 <div className="flex space-x-2 pt-4">
588 <Button variant="outline" size="sm">
589 <Download className="h-4 w-4 mr-2" />
590 Download Backup
591 </Button>
592 <Button variant="outline" size="sm">
593 <Eye className="h-4 w-4 mr-2" />
594 View Details
595 </Button>
596 </div>
597 </div>
598 </TabsContent>
599
600 <TabsContent value="logs" currentValue={selectedTab}>
601 <div className="space-y-2 mt-4 max-h-64 overflow-y-auto">
602 {backup.logs.map((log) => (
603 <div
604 key={log.id}
605 className="flex items-start space-x-3 p-2 rounded-lg hover:bg-muted/50"
606 >
607 <LogLevelIcon level={log.level} />
608 <div className="flex-1 min-w-0">
609 <div className="flex items-center justify-between">
610 <p className="text-sm font-medium">{log.message}</p>
611 <span className="text-xs text-muted-foreground">
612 {format(new Date(log.timestamp), "HH:mm:ss")}
613 </span>
614 </div>
615 </div>
616 </div>
617 ))}
618 </div>
619 </TabsContent>
620 </Tabs>
621 </CardContent>
622 </Card>
623 );
624};
625
626// Main component
627const DatabaseBackupDashboard = ({
628 backups = mockBackups,
629}: {
630 backups?: DatabaseBackup[];
631}) => {
632 const [filter, setFilter] = React.useState<"all" | DatabaseBackup["status"]>(
633 "all"
634 );
635
636 const filteredBackups = React.useMemo(() => {
637 if (filter === "all") return backups;
638 return backups.filter((backup) => backup.status === filter);
639 }, [backups, filter]);
640
641 const stats = React.useMemo(() => {
642 const total = backups.length;
643 const completed = backups.filter((b) => b.status === "completed").length;
644 const failed = backups.filter((b) => b.status === "failed").length;
645 const inProgress = backups.filter((b) => b.status === "in-progress").length;
646
647 return { total, completed, failed, inProgress };
648 }, [backups]);
649
650 return (
651 <div className="w-full max-w-7xl mx-auto p-6 space-y-6">
652 {/* Header */}
653 <div className="flex items-center justify-between">
654 <div>
655 <h1 className="text-3xl font-bold tracking-tight">
656 Database Backups
657 </h1>
658 <p className="text-muted-foreground">
659 Monitor and manage your database backup operations
660 </p>
661 </div>
662 <Button>
663 <Database className="h-4 w-4 mr-2" />
664 New Backup
665 </Button>
666 </div>
667
668 {/* Stats Cards */}
669 <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
670 <Card>
671 <CardContent className="p-6">
672 <div className="flex items-center space-x-2">
673 <Server className="h-5 w-5 text-blue-500" />
674 <div>
675 <p className="text-2xl font-bold">{stats.total}</p>
676 <p className="text-sm text-muted-foreground">Total Backups</p>
677 </div>
678 </div>
679 </CardContent>
680 </Card>
681
682 <Card>
683 <CardContent className="p-6">
684 <div className="flex items-center space-x-2">
685 <CheckCircle className="h-5 w-5 text-green-500" />
686 <div>
687 <p className="text-2xl font-bold">{stats.completed}</p>
688 <p className="text-sm text-muted-foreground">Completed</p>
689 </div>
690 </div>
691 </CardContent>
692 </Card>
693
694 <Card>
695 <CardContent className="p-6">
696 <div className="flex items-center space-x-2">
697 <XCircle className="h-5 w-5 text-red-500" />
698 <div>
699 <p className="text-2xl font-bold">{stats.failed}</p>
700 <p className="text-sm text-muted-foreground">Failed</p>
701 </div>
702 </div>
703 </CardContent>
704 </Card>
705
706 <Card>
707 <CardContent className="p-6">
708 <div className="flex items-center space-x-2">
709 <Clock className="h-5 w-5 text-blue-500" />
710 <div>
711 <p className="text-2xl font-bold">{stats.inProgress}</p>
712 <p className="text-sm text-muted-foreground">In Progress</p>
713 </div>
714 </div>
715 </CardContent>
716 </Card>
717 </div>
718
719 {/* Filters */}
720 <div className="flex space-x-2">
721 <Button
722 variant={filter === "all" ? "default" : "outline"}
723 size="sm"
724 onClick={() => setFilter("all")}
725 >
726 All
727 </Button>
728 <Button
729 variant={filter === "completed" ? "default" : "outline"}
730 size="sm"
731 onClick={() => setFilter("completed")}
732 >
733 Completed
734 </Button>
735 <Button
736 variant={filter === "failed" ? "default" : "outline"}
737 size="sm"
738 onClick={() => setFilter("failed")}
739 >
740 Failed
741 </Button>
742 <Button
743 variant={filter === "in-progress" ? "default" : "outline"}
744 size="sm"
745 onClick={() => setFilter("in-progress")}
746 >
747 In Progress
748 </Button>
749 </div>
750
751 {/* Backup Cards */}
752 <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
753 {filteredBackups.map((backup) => (
754 <BackupCard key={backup.id} backup={backup} />
755 ))}
756 </div>
757
758 {filteredBackups.length === 0 && (
759 <div className="text-center py-12">
760 <Database className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
761 <h3 className="text-lg font-medium text-muted-foreground">
762 No backups found
763 </h3>
764 <p className="text-sm text-muted-foreground">
765 {filter === "all"
766 ? "No database backups have been created yet."
767 : `No backups with status "${filter}" found.`}
768 </p>
769 </div>
770 )}
771 </div>
772 );
773};
774
775export default DatabaseBackupDashboard;
Dependencies
External Libraries
date-fnslucide-reactreact
Local Components
/lib/utils