Preview
Full width desktop view
Code
monitoring-14.tsx
1"use client";
2
3import React, { useState, useMemo } from "react";
4import { motion, AnimatePresence } from "framer-motion";
5import {
6 Server,
7 Plus,
8 Edit,
9 Trash2,
10 Power,
11 PowerOff,
12 Monitor,
13 HardDrive,
14 Cpu,
15 MemoryStick,
16 Network,
17 Globe,
18 Settings,
19 Users,
20 Activity,
21 ChevronDown,
22 ChevronUp,
23 Search,
24 Filter,
25 X,
26 Check,
27 AlertCircle,
28} from "lucide-react";
29import { cn } from "@/lib/utils";
30
31// Types
32interface VPSInstance {
33 id: string;
34 name: string;
35 type: "shared" | "dedicated";
36 status: "running" | "stopped" | "maintenance" | "error";
37 ip: string;
38 domain?: string;
39 cpu: number;
40 ram: number;
41 storage: number;
42 bandwidth: number;
43 location: string;
44 os: string;
45 createdAt: string;
46 lastUpdated: string;
47 monthlyPrice: number;
48 customerEmail: string;
49 customerName: string;
50}
51
52interface VPSFormData {
53 name: string;
54 type: "shared" | "dedicated";
55 cpu: number;
56 ram: number;
57 storage: number;
58 bandwidth: number;
59 location: string;
60 os: string;
61 monthlyPrice: number;
62 customerEmail: string;
63 customerName: string;
64 domain?: string;
65}
66
67type VPSFormErrors = {
68 [K in keyof VPSFormData]?: string;
69};
70
71// Enhanced Select Component
72const Select = ({
73 value,
74 onValueChange,
75 children,
76 placeholder,
77}: {
78 value?: string;
79 onValueChange: (value: string) => void;
80 children: React.ReactNode;
81 placeholder?: string;
82}) => {
83 const [isOpen, setIsOpen] = useState(false);
84
85 return (
86 <div className="relative">
87 <button
88 type="button"
89 onClick={() => setIsOpen(!isOpen)}
90 className="flex h-9 w-full items-center justify-between gap-2 rounded-lg border border-input bg-background px-3 py-2 text-start text-sm text-foreground shadow-sm focus:border-ring focus:outline-none focus:ring-[3px] focus:ring-ring/20"
91 >
92 <span className={cn(!value && "text-muted-foreground")}>
93 {value || placeholder}
94 </span>
95 <ChevronDown className="h-4 w-4 text-muted-foreground" />
96 </button>
97
98 <AnimatePresence>
99 {isOpen && (
100 <motion.div
101 initial={{ opacity: 0, y: -10 }}
102 animate={{ opacity: 1, y: 0 }}
103 exit={{ opacity: 0, y: -10 }}
104 className="absolute top-full left-0 right-0 z-50 mt-1 rounded-lg border border-input bg-popover shadow-lg"
105 >
106 <div className="p-1">{children}</div>
107 </motion.div>
108 )}
109 </AnimatePresence>
110 </div>
111 );
112};
113
114const SelectItem = ({
115 value,
116 onSelect,
117 children,
118}: {
119 value: string;
120 onSelect: (value: string) => void;
121 children: React.ReactNode;
122}) => (
123 <button
124 type="button"
125 onClick={() => onSelect(value)}
126 className="w-full text-left px-3 py-2 text-sm rounded-md hover:bg-accent transition-colors"
127 >
128 {children}
129 </button>
130);
131
132// Input Component
133const Input = React.forwardRef<
134 HTMLInputElement,
135 React.InputHTMLAttributes<HTMLInputElement> & {
136 label?: string;
137 error?: string;
138 }
139>(({ className, type, label, error, ...props }, ref) => {
140 return (
141 <div className="space-y-1.5">
142 {label && (
143 <label className="text-sm font-medium text-foreground">{label}</label>
144 )}
145 <input
146 type={type}
147 className={cn(
148 "flex h-9 w-full rounded-lg border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
149 error && "border-destructive",
150 className
151 )}
152 ref={ref}
153 {...props}
154 />
155 {error && <p className="text-sm text-destructive">{error}</p>}
156 </div>
157 );
158});
159Input.displayName = "Input";
160
161// Button Component
162const Button = React.forwardRef<
163 HTMLButtonElement,
164 React.ButtonHTMLAttributes<HTMLButtonElement> & {
165 variant?: "default" | "destructive" | "outline" | "secondary" | "ghost";
166 size?: "default" | "sm" | "lg" | "icon";
167 }
168>(({ className, variant = "default", size = "default", ...props }, ref) => {
169 return (
170 <button
171 className={cn(
172 "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background 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",
173 {
174 "bg-primary text-primary-foreground hover:bg-primary/90":
175 variant === "default",
176 "bg-destructive text-destructive-foreground hover:bg-destructive/90":
177 variant === "destructive",
178 "border border-input bg-background hover:bg-accent hover:text-accent-foreground":
179 variant === "outline",
180 "bg-secondary text-secondary-foreground hover:bg-secondary/80":
181 variant === "secondary",
182 "hover:bg-accent hover:text-accent-foreground": variant === "ghost",
183 },
184 {
185 "h-10 px-4 py-2": size === "default",
186 "h-9 rounded-md px-3": size === "sm",
187 "h-11 rounded-md px-8": size === "lg",
188 "h-10 w-10": size === "icon",
189 },
190 className
191 )}
192 ref={ref}
193 {...props}
194 />
195 );
196});
197Button.displayName = "Button";
198
199// Card Components
200const Card = React.forwardRef<
201 HTMLDivElement,
202 React.HTMLAttributes<HTMLDivElement>
203>(({ className, ...props }, ref) => (
204 <div
205 ref={ref}
206 className={cn(
207 "rounded-lg border bg-card text-card-foreground shadow-sm",
208 className
209 )}
210 {...props}
211 />
212));
213Card.displayName = "Card";
214
215const CardHeader = React.forwardRef<
216 HTMLDivElement,
217 React.HTMLAttributes<HTMLDivElement>
218>(({ className, ...props }, ref) => (
219 <div
220 ref={ref}
221 className={cn("flex flex-col space-y-1.5 p-6", className)}
222 {...props}
223 />
224));
225CardHeader.displayName = "CardHeader";
226
227const CardTitle = React.forwardRef<
228 HTMLParagraphElement,
229 React.HTMLAttributes<HTMLHeadingElement>
230>(({ className, ...props }, ref) => (
231 <h3
232 ref={ref}
233 className={cn(
234 "text-2xl font-semibold leading-none tracking-tight",
235 className
236 )}
237 {...props}
238 />
239));
240CardTitle.displayName = "CardTitle";
241
242const CardContent = React.forwardRef<
243 HTMLDivElement,
244 React.HTMLAttributes<HTMLDivElement>
245>(({ className, ...props }, ref) => (
246 <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
247));
248CardContent.displayName = "CardContent";
249
250// Badge Component
251const Badge = ({
252 className,
253 variant = "default",
254 ...props
255}: React.HTMLAttributes<HTMLDivElement> & {
256 variant?: "default" | "secondary" | "destructive" | "outline";
257}) => {
258 return (
259 <div
260 className={cn(
261 "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
262 {
263 "border-transparent bg-primary text-primary-foreground hover:bg-primary/80":
264 variant === "default",
265 "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80":
266 variant === "secondary",
267 "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80":
268 variant === "destructive",
269 "text-foreground": variant === "outline",
270 },
271 className
272 )}
273 {...props}
274 />
275 );
276};
277
278// Dialog Components
279const Dialog = ({
280 open,
281 onOpenChange,
282 children,
283}: {
284 open: boolean;
285 onOpenChange: (open: boolean) => void;
286 children: React.ReactNode;
287}) => {
288 if (!open) return null;
289
290 return (
291 <div className="fixed inset-0 z-50 flex items-center justify-center">
292 <div
293 className="fixed inset-0 bg-black/80"
294 onClick={() => onOpenChange(false)}
295 />
296 <motion.div
297 initial={{ opacity: 0, scale: 0.95 }}
298 animate={{ opacity: 1, scale: 1 }}
299 exit={{ opacity: 0, scale: 0.95 }}
300 className="relative z-50 grid w-full max-w-lg gap-4 border bg-background p-6 shadow-lg sm:rounded-lg"
301 >
302 <button
303 onClick={() => onOpenChange(false)}
304 className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100"
305 >
306 <X className="h-4 w-4" />
307 </button>
308 {children}
309 </motion.div>
310 </div>
311 );
312};
313
314const DialogHeader = ({
315 className,
316 ...props
317}: React.HTMLAttributes<HTMLDivElement>) => (
318 <div
319 className={cn(
320 "flex flex-col space-y-1.5 text-center sm:text-left",
321 className
322 )}
323 {...props}
324 />
325);
326
327const DialogTitle = ({
328 className,
329 ...props
330}: React.HTMLAttributes<HTMLHeadingElement>) => (
331 <h2
332 className={cn(
333 "text-lg font-semibold leading-none tracking-tight",
334 className
335 )}
336 {...props}
337 />
338);
339
340const DialogContent = ({
341 className,
342 ...props
343}: React.HTMLAttributes<HTMLDivElement>) => (
344 <div className={cn("grid gap-4 py-4", className)} {...props} />
345);
346
347// Data Table Component
348const DataTable = <T extends Record<string, any>>({
349 data,
350 columns,
351 searchable = true,
352 onRowClick,
353}: {
354 data: T[];
355 columns: Array<{
356 key: keyof T;
357 header: string;
358 sortable?: boolean;
359 render?: (value: any, row: T) => React.ReactNode;
360 }>;
361 searchable?: boolean;
362 onRowClick?: (row: T) => void;
363}) => {
364 const [search, setSearch] = useState("");
365 const [sortConfig, setSortConfig] = useState<{
366 key: keyof T | null;
367 direction: "asc" | "desc";
368 }>({ key: null, direction: "asc" });
369
370 const filteredData = useMemo(() => {
371 if (!search) return data;
372 return data.filter((row) =>
373 columns.some((column) => {
374 const value = row[column.key];
375 return value?.toString().toLowerCase().includes(search.toLowerCase());
376 })
377 );
378 }, [data, search, columns]);
379
380 const sortedData = useMemo(() => {
381 if (!sortConfig.key) return filteredData;
382
383 return [...filteredData].sort((a, b) => {
384 const aValue = a[sortConfig.key!];
385 const bValue = b[sortConfig.key!];
386
387 if (aValue < bValue) {
388 return sortConfig.direction === "asc" ? -1 : 1;
389 }
390 if (aValue > bValue) {
391 return sortConfig.direction === "asc" ? 1 : -1;
392 }
393 return 0;
394 });
395 }, [filteredData, sortConfig]);
396
397 const handleSort = (key: keyof T) => {
398 setSortConfig((current) => ({
399 key,
400 direction:
401 current.key === key && current.direction === "asc" ? "desc" : "asc",
402 }));
403 };
404
405 return (
406 <div className="w-full bg-card rounded-lg border">
407 {searchable && (
408 <div className="p-4 border-b">
409 <div className="relative">
410 <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
411 <input
412 type="text"
413 placeholder="Search VPS instances..."
414 value={search}
415 onChange={(e) => setSearch(e.target.value)}
416 className="w-full pl-10 pr-4 py-2 border border-input rounded-lg bg-background text-sm focus:outline-none focus:ring-2 focus:ring-ring"
417 />
418 </div>
419 </div>
420 )}
421
422 <div className="overflow-x-auto">
423 <table className="w-full">
424 <thead className="bg-muted/30">
425 <tr>
426 {columns.map((column) => (
427 <th
428 key={String(column.key)}
429 className={cn(
430 "text-left font-medium text-muted-foreground px-6 py-4",
431 column.sortable && "cursor-pointer hover:bg-muted/50"
432 )}
433 onClick={() => column.sortable && handleSort(column.key)}
434 >
435 <div className="flex items-center gap-2">
436 <span className="text-sm font-semibold">
437 {column.header}
438 </span>
439 {column.sortable && (
440 <div className="flex flex-col">
441 <ChevronUp
442 className={cn(
443 "h-3 w-3",
444 sortConfig.key === column.key &&
445 sortConfig.direction === "asc"
446 ? "text-primary"
447 : "text-muted-foreground/40"
448 )}
449 />
450 <ChevronDown
451 className={cn(
452 "h-3 w-3 -mt-1",
453 sortConfig.key === column.key &&
454 sortConfig.direction === "desc"
455 ? "text-primary"
456 : "text-muted-foreground/40"
457 )}
458 />
459 </div>
460 )}
461 </div>
462 </th>
463 ))}
464 </tr>
465 </thead>
466 <tbody>
467 {sortedData.map((row, index) => (
468 <tr
469 key={index}
470 className={cn(
471 "border-t border-border hover:bg-muted/30 transition-colors",
472 onRowClick && "cursor-pointer"
473 )}
474 onClick={() => onRowClick?.(row)}
475 >
476 {columns.map((column) => (
477 <td key={String(column.key)} className="px-6 py-4 text-sm">
478 {column.render
479 ? column.render(row[column.key], row)
480 : String(row[column.key] ?? "")}
481 </td>
482 ))}
483 </tr>
484 ))}
485 </tbody>
486 </table>
487 </div>
488 </div>
489 );
490};
491
492// Main VPS Admin Component
493const VPSAdminUI = () => {
494 const [vpsInstances, setVpsInstances] = useState<VPSInstance[]>([
495 {
496 id: "vps-001",
497 name: "WebStore-Primary",
498 type: "dedicated",
499 status: "running",
500 ip: "192.168.1.100",
501 domain: "shop.example.com",
502 cpu: 4,
503 ram: 8,
504 storage: 100,
505 bandwidth: 1000,
506 location: "US-East",
507 os: "Ubuntu 22.04",
508 createdAt: "2024-01-15",
509 lastUpdated: "2024-01-20",
510 monthlyPrice: 89.99,
511 customerEmail: "john@example.com",
512 customerName: "John Doe",
513 },
514 {
515 id: "vps-002",
516 name: "WebStore-Dev",
517 type: "shared",
518 status: "stopped",
519 ip: "192.168.1.101",
520 cpu: 2,
521 ram: 4,
522 storage: 50,
523 bandwidth: 500,
524 location: "EU-West",
525 os: "CentOS 8",
526 createdAt: "2024-01-10",
527 lastUpdated: "2024-01-18",
528 monthlyPrice: 29.99,
529 customerEmail: "jane@example.com",
530 customerName: "Jane Smith",
531 },
532 ]);
533
534 const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
535 const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
536 const [selectedVPS, setSelectedVPS] = useState<VPSInstance | null>(null);
537 const [formData, setFormData] = useState<VPSFormData>({
538 name: "",
539 type: "shared",
540 cpu: 1,
541 ram: 1,
542 storage: 20,
543 bandwidth: 100,
544 location: "",
545 os: "",
546 monthlyPrice: 0,
547 customerEmail: "",
548 customerName: "",
549 domain: "",
550 });
551 const [formErrors, setFormErrors] = useState<Partial<VPSFormErrors>>({});
552
553 const validateForm = (data: VPSFormData): boolean => {
554 const errors: Partial<VPSFormErrors> = {};
555
556 if (!data.name.trim()) errors.name = "Name is required";
557 if (!data.location.trim()) errors.location = "Location is required";
558 if (!data.os.trim()) errors.os = "Operating System is required";
559 if (!data.customerEmail.trim())
560 errors.customerEmail = "Customer email is required";
561 if (!data.customerName.trim())
562 errors.customerName = "Customer name is required";
563 if (data.monthlyPrice <= 0)
564 errors.monthlyPrice = "Price must be greater than 0";
565
566 setFormErrors(errors);
567 return Object.keys(errors).length === 0;
568 };
569
570 const handleAddVPS = () => {
571 if (!validateForm(formData)) return;
572
573 const newVPS: VPSInstance = {
574 id: `vps-${Date.now()}`,
575 ...formData,
576 status: "stopped",
577 ip: `192.168.1.${Math.floor(Math.random() * 255)}`,
578 createdAt: new Date().toISOString().split("T")[0],
579 lastUpdated: new Date().toISOString().split("T")[0],
580 };
581
582 setVpsInstances((prev) => [...prev, newVPS]);
583 setIsAddDialogOpen(false);
584 resetForm();
585 };
586
587 const handleEditVPS = () => {
588 if (!selectedVPS || !validateForm(formData)) return;
589
590 setVpsInstances((prev) =>
591 prev.map((vps) =>
592 vps.id === selectedVPS.id
593 ? {
594 ...vps,
595 ...formData,
596 lastUpdated: new Date().toISOString().split("T")[0],
597 }
598 : vps
599 )
600 );
601 setIsEditDialogOpen(false);
602 setSelectedVPS(null);
603 resetForm();
604 };
605
606 const handleDeleteVPS = (id: string) => {
607 setVpsInstances((prev) => prev.filter((vps) => vps.id !== id));
608 };
609
610 const handleToggleStatus = (id: string) => {
611 setVpsInstances((prev) =>
612 prev.map((vps) =>
613 vps.id === id
614 ? {
615 ...vps,
616 status: vps.status === "running" ? "stopped" : "running",
617 lastUpdated: new Date().toISOString().split("T")[0],
618 }
619 : vps
620 )
621 );
622 };
623
624 const resetForm = () => {
625 setFormData({
626 name: "",
627 type: "shared",
628 cpu: 1,
629 ram: 1,
630 storage: 20,
631 bandwidth: 100,
632 location: "",
633 os: "",
634 monthlyPrice: 0,
635 customerEmail: "",
636 customerName: "",
637 domain: "",
638 });
639 setFormErrors({});
640 };
641
642 const openEditDialog = (vps: VPSInstance) => {
643 setSelectedVPS(vps);
644 setFormData({
645 name: vps.name,
646 type: vps.type,
647 cpu: vps.cpu,
648 ram: vps.ram,
649 storage: vps.storage,
650 bandwidth: vps.bandwidth,
651 location: vps.location,
652 os: vps.os,
653 monthlyPrice: vps.monthlyPrice,
654 customerEmail: vps.customerEmail,
655 customerName: vps.customerName,
656 domain: vps.domain || "",
657 });
658 setIsEditDialogOpen(true);
659 };
660
661 const getStatusColor = (status: VPSInstance["status"]) => {
662 switch (status) {
663 case "running":
664 return "bg-green-500";
665 case "stopped":
666 return "bg-red-500";
667 case "maintenance":
668 return "bg-yellow-500";
669 case "error":
670 return "bg-orange-500";
671 default:
672 return "bg-gray-500";
673 }
674 };
675
676 const getStatusIcon = (status: VPSInstance["status"]) => {
677 switch (status) {
678 case "running":
679 return <Power className="h-4 w-4" />;
680 case "stopped":
681 return <PowerOff className="h-4 w-4" />;
682 case "maintenance":
683 return <Settings className="h-4 w-4" />;
684 case "error":
685 return <AlertCircle className="h-4 w-4" />;
686 default:
687 return <Monitor className="h-4 w-4" />;
688 }
689 };
690
691 const columns = [
692 {
693 key: "name" as keyof VPSInstance,
694 header: "Name",
695 sortable: true,
696 render: (value: string, row: VPSInstance) => (
697 <div className="flex items-center gap-3">
698 <div
699 className={cn("w-2 h-2 rounded-full", getStatusColor(row.status))}
700 />
701 <div>
702 <div className="font-medium">{value}</div>
703 <div className="text-xs text-muted-foreground">{row.id}</div>
704 </div>
705 </div>
706 ),
707 },
708 {
709 key: "type" as keyof VPSInstance,
710 header: "Type",
711 sortable: true,
712 render: (value: string) => (
713 <Badge variant={value === "dedicated" ? "default" : "secondary"}>
714 {value === "dedicated" ? "Dedicated" : "Shared"}
715 </Badge>
716 ),
717 },
718 {
719 key: "status" as keyof VPSInstance,
720 header: "Status",
721 sortable: true,
722 render: (value: string, row: VPSInstance) => (
723 <div className="flex items-center gap-2">
724 {getStatusIcon(row.status)}
725 <span className="capitalize">{value}</span>
726 </div>
727 ),
728 },
729 {
730 key: "customerName" as keyof VPSInstance,
731 header: "Customer",
732 sortable: true,
733 render: (value: string, row: VPSInstance) => (
734 <div>
735 <div className="font-medium">{value}</div>
736 <div className="text-xs text-muted-foreground">
737 {row.customerEmail}
738 </div>
739 </div>
740 ),
741 },
742 {
743 key: "ip" as keyof VPSInstance,
744 header: "IP Address",
745 render: (value: string, row: VPSInstance) => (
746 <div>
747 <div className="font-mono text-sm">{value}</div>
748 {row.domain && (
749 <div className="text-xs text-muted-foreground flex items-center gap-1">
750 <Globe className="h-3 w-3" />
751 {row.domain}
752 </div>
753 )}
754 </div>
755 ),
756 },
757 {
758 key: "cpu" as keyof VPSInstance,
759 header: "Resources",
760 render: (value: number, row: VPSInstance) => (
761 <div className="space-y-1">
762 <div className="flex items-center gap-1 text-xs">
763 <Cpu className="h-3 w-3" />
764 {row.cpu} vCPU
765 </div>
766 <div className="flex items-center gap-1 text-xs">
767 <MemoryStick className="h-3 w-3" />
768 {row.ram} GB RAM
769 </div>
770 <div className="flex items-center gap-1 text-xs">
771 <HardDrive className="h-3 w-3" />
772 {row.storage} GB
773 </div>
774 </div>
775 ),
776 },
777 {
778 key: "location" as keyof VPSInstance,
779 header: "Location",
780 sortable: true,
781 },
782 {
783 key: "monthlyPrice" as keyof VPSInstance,
784 header: "Price",
785 sortable: true,
786 render: (value: number) => (
787 <span className="font-medium">${value.toFixed(2)}/mo</span>
788 ),
789 },
790 {
791 key: "actions" as keyof VPSInstance,
792 header: "Actions",
793 render: (value: any, row: VPSInstance) => (
794 <div className="flex items-center gap-2">
795 <Button
796 size="icon"
797 variant="ghost"
798 onClick={(e) => {
799 e.stopPropagation();
800 handleToggleStatus(row.id);
801 }}
802 className="h-8 w-8"
803 >
804 {row.status === "running" ? (
805 <PowerOff className="h-4 w-4" />
806 ) : (
807 <Power className="h-4 w-4" />
808 )}
809 </Button>
810 <Button
811 size="icon"
812 variant="ghost"
813 onClick={(e) => {
814 e.stopPropagation();
815 openEditDialog(row);
816 }}
817 className="h-8 w-8"
818 >
819 <Edit className="h-4 w-4" />
820 </Button>
821 <Button
822 size="icon"
823 variant="ghost"
824 onClick={(e) => {
825 e.stopPropagation();
826 handleDeleteVPS(row.id);
827 }}
828 className="h-8 w-8 text-destructive hover:text-destructive"
829 >
830 <Trash2 className="h-4 w-4" />
831 </Button>
832 </div>
833 ),
834 },
835 ];
836
837 const VPSForm = ({
838 onSubmit,
839 submitLabel,
840 }: {
841 onSubmit: () => void;
842 submitLabel: string;
843 }) => (
844 <div className="grid gap-4">
845 <div className="grid grid-cols-2 gap-4">
846 <Input
847 label="VPS Name"
848 value={formData.name}
849 onChange={(e) =>
850 setFormData((prev) => ({ ...prev, name: e.target.value }))
851 }
852 placeholder="e.g., WebStore-Primary"
853 error={formErrors.name}
854 />
855 <div className="space-y-1.5">
856 <label className="text-sm font-medium text-foreground">
857 VPS Type
858 </label>
859 <Select
860 value={formData.type}
861 onValueChange={(value) =>
862 setFormData((prev) => ({
863 ...prev,
864 type: value as "shared" | "dedicated",
865 }))
866 }
867 placeholder="Select type"
868 >
869 <SelectItem
870 value="shared"
871 onSelect={(value) =>
872 setFormData((prev) => ({
873 ...prev,
874 type: value as "shared" | "dedicated",
875 }))
876 }
877 >
878 Shared VPS
879 </SelectItem>
880 <SelectItem
881 value="dedicated"
882 onSelect={(value) =>
883 setFormData((prev) => ({
884 ...prev,
885 type: value as "shared" | "dedicated",
886 }))
887 }
888 >
889 Dedicated VPS
890 </SelectItem>
891 </Select>
892 </div>
893 </div>
894
895 <div className="grid grid-cols-2 gap-4">
896 <Input
897 label="Customer Name"
898 value={formData.customerName}
899 onChange={(e) =>
900 setFormData((prev) => ({ ...prev, customerName: e.target.value }))
901 }
902 placeholder="John Doe"
903 error={formErrors.customerName}
904 />
905 <Input
906 label="Customer Email"
907 type="email"
908 value={formData.customerEmail}
909 onChange={(e) =>
910 setFormData((prev) => ({ ...prev, customerEmail: e.target.value }))
911 }
912 placeholder="john@example.com"
913 error={formErrors.customerEmail}
914 />
915 </div>
916
917 <div className="grid grid-cols-4 gap-4">
918 <Input
919 label="CPU (vCores)"
920 type="number"
921 min="1"
922 max="32"
923 value={formData.cpu}
924 onChange={(e) =>
925 setFormData((prev) => ({
926 ...prev,
927 cpu: parseInt(e.target.value) || 1,
928 }))
929 }
930 />
931 <Input
932 label="RAM (GB)"
933 type="number"
934 min="1"
935 max="128"
936 value={formData.ram}
937 onChange={(e) =>
938 setFormData((prev) => ({
939 ...prev,
940 ram: parseInt(e.target.value) || 1,
941 }))
942 }
943 />
944 <Input
945 label="Storage (GB)"
946 type="number"
947 min="10"
948 max="2000"
949 value={formData.storage}
950 onChange={(e) =>
951 setFormData((prev) => ({
952 ...prev,
953 storage: parseInt(e.target.value) || 20,
954 }))
955 }
956 />
957 <Input
958 label="Bandwidth (GB)"
959 type="number"
960 min="100"
961 max="10000"
962 value={formData.bandwidth}
963 onChange={(e) =>
964 setFormData((prev) => ({
965 ...prev,
966 bandwidth: parseInt(e.target.value) || 100,
967 }))
968 }
969 />
970 </div>
971
972 <div className="grid grid-cols-2 gap-4">
973 <div className="space-y-1.5">
974 <label className="text-sm font-medium text-foreground">
975 Location
976 </label>
977 <Select
978 value={formData.location}
979 onValueChange={(value) =>
980 setFormData((prev) => ({ ...prev, location: value }))
981 }
982 placeholder="Select location"
983 >
984 <SelectItem
985 value="US-East"
986 onSelect={(value) =>
987 setFormData((prev) => ({ ...prev, location: value }))
988 }
989 >
990 US East (Virginia)
991 </SelectItem>
992 <SelectItem
993 value="US-West"
994 onSelect={(value) =>
995 setFormData((prev) => ({ ...prev, location: value }))
996 }
997 >
998 US West (California)
999 </SelectItem>
1000 <SelectItem
1001 value="EU-West"
1002 onSelect={(value) =>
1003 setFormData((prev) => ({ ...prev, location: value }))
1004 }
1005 >
1006 EU West (Ireland)
1007 </SelectItem>
1008 <SelectItem
1009 value="Asia-Pacific"
1010 onSelect={(value) =>
1011 setFormData((prev) => ({ ...prev, location: value }))
1012 }
1013 >
1014 Asia Pacific (Singapore)
1015 </SelectItem>
1016 </Select>
1017 {formErrors.location && (
1018 <p className="text-sm text-destructive">{formErrors.location}</p>
1019 )}
1020 </div>
1021 <div className="space-y-1.5">
1022 <label className="text-sm font-medium text-foreground">
1023 Operating System
1024 </label>
1025 <Select
1026 value={formData.os}
1027 onValueChange={(value) =>
1028 setFormData((prev) => ({ ...prev, os: value }))
1029 }
1030 placeholder="Select OS"
1031 >
1032 <SelectItem
1033 value="Ubuntu 22.04"
1034 onSelect={(value) =>
1035 setFormData((prev) => ({ ...prev, os: value }))
1036 }
1037 >
1038 Ubuntu 22.04 LTS
1039 </SelectItem>
1040 <SelectItem
1041 value="Ubuntu 20.04"
1042 onSelect={(value) =>
1043 setFormData((prev) => ({ ...prev, os: value }))
1044 }
1045 >
1046 Ubuntu 20.04 LTS
1047 </SelectItem>
1048 <SelectItem
1049 value="CentOS 8"
1050 onSelect={(value) =>
1051 setFormData((prev) => ({ ...prev, os: value }))
1052 }
1053 >
1054 CentOS 8
1055 </SelectItem>
1056 <SelectItem
1057 value="Debian 11"
1058 onSelect={(value) =>
1059 setFormData((prev) => ({ ...prev, os: value }))
1060 }
1061 >
1062 Debian 11
1063 </SelectItem>
1064 <SelectItem
1065 value="Windows Server 2022"
1066 onSelect={(value) =>
1067 setFormData((prev) => ({ ...prev, os: value }))
1068 }
1069 >
1070 Windows Server 2022
1071 </SelectItem>
1072 </Select>
1073 {formErrors.os && (
1074 <p className="text-sm text-destructive">{formErrors.os}</p>
1075 )}
1076 </div>
1077 </div>
1078
1079 <div className="grid grid-cols-2 gap-4">
1080 <Input
1081 label="Domain (Optional)"
1082 value={formData.domain}
1083 onChange={(e) =>
1084 setFormData((prev) => ({ ...prev, domain: e.target.value }))
1085 }
1086 placeholder="shop.example.com"
1087 />
1088 <Input
1089 label="Monthly Price ($)"
1090 type="number"
1091 min="0"
1092 step="0.01"
1093 value={formData.monthlyPrice}
1094 onChange={(e) =>
1095 setFormData((prev) => ({
1096 ...prev,
1097 monthlyPrice: parseFloat(e.target.value) || 0,
1098 }))
1099 }
1100 placeholder="29.99"
1101 error={formErrors.monthlyPrice}
1102 />
1103 </div>
1104
1105 <div className="flex justify-end gap-2 pt-4">
1106 <Button
1107 variant="outline"
1108 onClick={() => {
1109 setIsAddDialogOpen(false);
1110 setIsEditDialogOpen(false);
1111 resetForm();
1112 }}
1113 >
1114 Cancel
1115 </Button>
1116 <Button onClick={onSubmit}>{submitLabel}</Button>
1117 </div>
1118 </div>
1119 );
1120
1121 return (
1122 <div className="min-h-screen bg-background p-6">
1123 <div className="max-w-7xl mx-auto space-y-6">
1124 {/* Header */}
1125 <div className="flex items-center justify-between">
1126 <div>
1127 <h1 className="text-3xl font-bold tracking-tight">
1128 VPS Management
1129 </h1>
1130 <p className="text-muted-foreground">
1131 Manage and monitor VPS instances for webstore deployments
1132 </p>
1133 </div>
1134 <Button onClick={() => setIsAddDialogOpen(true)} className="gap-2">
1135 <Plus className="h-4 w-4" />
1136 Add VPS Instance
1137 </Button>
1138 </div>
1139
1140 {/* Stats Cards */}
1141 <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
1142 <Card>
1143 <CardContent className="p-6">
1144 <div className="flex items-center gap-2">
1145 <Server className="h-5 w-5 text-primary" />
1146 <div>
1147 <p className="text-sm font-medium">Total VPS</p>
1148 <p className="text-2xl font-bold">{vpsInstances.length}</p>
1149 </div>
1150 </div>
1151 </CardContent>
1152 </Card>
1153 <Card>
1154 <CardContent className="p-6">
1155 <div className="flex items-center gap-2">
1156 <Activity className="h-5 w-5 text-green-500" />
1157 <div>
1158 <p className="text-sm font-medium">Running</p>
1159 <p className="text-2xl font-bold">
1160 {
1161 vpsInstances.filter((vps) => vps.status === "running")
1162 .length
1163 }
1164 </p>
1165 </div>
1166 </div>
1167 </CardContent>
1168 </Card>
1169 <Card>
1170 <CardContent className="p-6">
1171 <div className="flex items-center gap-2">
1172 <Users className="h-5 w-5 text-blue-500" />
1173 <div>
1174 <p className="text-sm font-medium">Customers</p>
1175 <p className="text-2xl font-bold">
1176 {new Set(vpsInstances.map((vps) => vps.customerEmail)).size}
1177 </p>
1178 </div>
1179 </div>
1180 </CardContent>
1181 </Card>
1182 <Card>
1183 <CardContent className="p-6">
1184 <div className="flex items-center gap-2">
1185 <Network className="h-5 w-5 text-purple-500" />
1186 <div>
1187 <p className="text-sm font-medium">Monthly Revenue</p>
1188 <p className="text-2xl font-bold">
1189 $
1190 {vpsInstances
1191 .reduce((sum, vps) => sum + vps.monthlyPrice, 0)
1192 .toFixed(2)}
1193 </p>
1194 </div>
1195 </div>
1196 </CardContent>
1197 </Card>
1198 </div>
1199
1200 {/* VPS Table */}
1201 <Card>
1202 <CardHeader>
1203 <CardTitle>VPS Instances</CardTitle>
1204 </CardHeader>
1205 <CardContent className="p-0">
1206 <DataTable
1207 data={vpsInstances}
1208 columns={columns}
1209 searchable={true}
1210 onRowClick={(vps) => openEditDialog(vps)}
1211 />
1212 </CardContent>
1213 </Card>
1214
1215 {/* Add VPS Dialog */}
1216 <Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
1217 <DialogHeader>
1218 <DialogTitle>Add New VPS Instance</DialogTitle>
1219 </DialogHeader>
1220 <DialogContent>
1221 <VPSForm onSubmit={handleAddVPS} submitLabel="Create VPS" />
1222 </DialogContent>
1223 </Dialog>
1224
1225 {/* Edit VPS Dialog */}
1226 <Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
1227 <DialogHeader>
1228 <DialogTitle>Edit VPS Instance</DialogTitle>
1229 </DialogHeader>
1230 <DialogContent>
1231 <VPSForm onSubmit={handleEditVPS} submitLabel="Update VPS" />
1232 </DialogContent>
1233 </Dialog>
1234 </div>
1235 </div>
1236 );
1237};
1238
1239export default VPSAdminUI;
Dependencies
External Libraries
framer-motionlucide-reactreact
Local Components
/lib/utils