Preview
Full width desktop view
Code
monitoring-12.tsx
1"use client";
2
3import React, { useState, useEffect } from "react";
4import * as DialogPrimitive from "@radix-ui/react-dialog";
5import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
6import { Cross2Icon } from "@radix-ui/react-icons";
7import {
8 Server,
9 Plus,
10 Edit,
11 Trash2,
12 Search,
13 Activity,
14 HardDrive,
15 Cpu,
16 MemoryStick,
17 Globe,
18 Shield,
19 Users,
20 AlertTriangle,
21 CheckCircle,
22 XCircle,
23 Clock,
24} from "lucide-react";
25import { cn } from "@/lib/utils";
26
27// Types
28interface VPSInstance {
29 id: string;
30 name: string;
31 type: "shared" | "dedicated";
32 status: "active" | "inactive" | "maintenance" | "error";
33 cpu: number;
34 ram: number;
35 storage: number;
36 bandwidth: number;
37 ip: string;
38 location: string;
39 os: string;
40 createdAt: string;
41 lastUpdated: string;
42 monthlyPrice: number;
43 customerId: string;
44 customerName: string;
45 customerEmail: string;
46}
47
48interface VPSFormData {
49 name: string;
50 type: "shared" | "dedicated";
51 cpu: number;
52 ram: number;
53 storage: number;
54 bandwidth: number;
55 location: string;
56 os: string;
57 customerId: string;
58 customerName: string;
59 customerEmail: string;
60 monthlyPrice: number;
61}
62
63// Enhanced Dialog Components
64const Dialog = DialogPrimitive.Root;
65const DialogTrigger = DialogPrimitive.Trigger;
66const DialogPortal = DialogPrimitive.Portal;
67const DialogClose = DialogPrimitive.Close;
68
69const DialogOverlay = React.forwardRef<
70 React.ElementRef<typeof DialogPrimitive.Overlay>,
71 React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
72>(({ className, ...props }, ref) => (
73 <DialogPrimitive.Overlay
74 ref={ref}
75 className={cn(
76 "fixed inset-0 z-[101] bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
77 className
78 )}
79 {...props}
80 />
81));
82DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
83
84const DialogContent = React.forwardRef<
85 React.ElementRef<typeof DialogPrimitive.Content>,
86 React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
87>(({ className, children, ...props }, ref) => (
88 <DialogPortal>
89 <DialogOverlay />
90 <DialogPrimitive.Content
91 ref={ref}
92 className={cn(
93 "fixed left-1/2 top-1/2 z-[101] grid max-h-[calc(100%-4rem)] w-full max-w-2xl -translate-x-1/2 -translate-y-1/2 gap-4 overflow-y-auto border bg-background p-6 shadow-lg shadow-black/5 duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-xl",
94 className
95 )}
96 {...props}
97 >
98 {children}
99 <DialogPrimitive.Close className="group absolute right-3 top-3 flex size-7 items-center justify-center rounded-lg outline-offset-2 transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring/70 disabled:pointer-events-none">
100 <Cross2Icon
101 width={16}
102 height={16}
103 strokeWidth={2}
104 className="opacity-60 transition-opacity group-hover:opacity-100"
105 />
106 <span className="sr-only">Close</span>
107 </DialogPrimitive.Close>
108 </DialogPrimitive.Content>
109 </DialogPortal>
110));
111DialogContent.displayName = DialogPrimitive.Content.displayName;
112
113const DialogHeader = ({
114 className,
115 ...props
116}: React.HTMLAttributes<HTMLDivElement>) => (
117 <div
118 className={cn(
119 "flex flex-col space-y-1.5 text-center sm:text-left",
120 className
121 )}
122 {...props}
123 />
124);
125DialogHeader.displayName = "DialogHeader";
126
127const DialogTitle = React.forwardRef<
128 React.ElementRef<typeof DialogPrimitive.Title>,
129 React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
130>(({ className, ...props }, ref) => (
131 <DialogPrimitive.Title
132 ref={ref}
133 className={cn("text-lg font-semibold tracking-tight", className)}
134 {...props}
135 />
136));
137DialogTitle.displayName = DialogPrimitive.Title.displayName;
138
139const DialogDescription = React.forwardRef<
140 React.ElementRef<typeof DialogPrimitive.Description>,
141 React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
142>(({ className, ...props }, ref) => (
143 <DialogPrimitive.Description
144 ref={ref}
145 className={cn("text-sm text-muted-foreground", className)}
146 {...props}
147 />
148));
149DialogDescription.displayName = DialogPrimitive.Description.displayName;
150
151// Alert Dialog Components
152const AlertDialog = AlertDialogPrimitive.Root;
153const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
154const AlertDialogPortal = AlertDialogPrimitive.Portal;
155
156const AlertDialogOverlay = React.forwardRef<
157 React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
158 React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
159>(({ className, ...props }, ref) => (
160 <AlertDialogPrimitive.Overlay
161 className={cn(
162 "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
163 className
164 )}
165 {...props}
166 ref={ref}
167 />
168));
169AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
170
171const AlertDialogContent = React.forwardRef<
172 React.ElementRef<typeof AlertDialogPrimitive.Content>,
173 React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
174>(({ className, ...props }, ref) => (
175 <AlertDialogPortal>
176 <AlertDialogOverlay />
177 <AlertDialogPrimitive.Content
178 ref={ref}
179 className={cn(
180 "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
181 className
182 )}
183 {...props}
184 />
185 </AlertDialogPortal>
186));
187AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
188
189const AlertDialogHeader = ({
190 className,
191 ...props
192}: React.HTMLAttributes<HTMLDivElement>) => (
193 <div
194 className={cn(
195 "flex flex-col space-y-2 text-center sm:text-left",
196 className
197 )}
198 {...props}
199 />
200);
201AlertDialogHeader.displayName = "AlertDialogHeader";
202
203const AlertDialogTitle = React.forwardRef<
204 React.ElementRef<typeof AlertDialogPrimitive.Title>,
205 React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
206>(({ className, ...props }, ref) => (
207 <AlertDialogPrimitive.Title
208 ref={ref}
209 className={cn("text-lg font-semibold", className)}
210 {...props}
211 />
212));
213AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
214
215const AlertDialogDescription = React.forwardRef<
216 React.ElementRef<typeof AlertDialogPrimitive.Description>,
217 React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
218>(({ className, ...props }, ref) => (
219 <AlertDialogPrimitive.Description
220 ref={ref}
221 className={cn("text-sm text-muted-foreground", className)}
222 {...props}
223 />
224));
225AlertDialogDescription.displayName =
226 AlertDialogPrimitive.Description.displayName;
227
228const AlertDialogAction = React.forwardRef<
229 React.ElementRef<typeof AlertDialogPrimitive.Action>,
230 React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
231>(({ className, ...props }, ref) => (
232 <AlertDialogPrimitive.Action
233 ref={ref}
234 className={cn(
235 "inline-flex items-center justify-center whitespace-nowrap 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 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2",
236 className
237 )}
238 {...props}
239 />
240));
241AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
242
243const AlertDialogCancel = React.forwardRef<
244 React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
245 React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
246>(({ className, ...props }, ref) => (
247 <AlertDialogPrimitive.Cancel
248 ref={ref}
249 className={cn(
250 "inline-flex items-center justify-center whitespace-nowrap 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 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-10 px-4 py-2 mt-2 sm:mt-0",
251 className
252 )}
253 {...props}
254 />
255));
256AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
257
258// Button Component
259const Button = React.forwardRef<
260 HTMLButtonElement,
261 React.ButtonHTMLAttributes<HTMLButtonElement> & {
262 variant?:
263 | "default"
264 | "destructive"
265 | "outline"
266 | "secondary"
267 | "ghost"
268 | "link";
269 size?: "default" | "sm" | "lg" | "icon";
270 }
271>(({ className, variant = "default", size = "default", ...props }, ref) => {
272 const variants = {
273 default: "bg-primary text-primary-foreground hover:bg-primary/90",
274 destructive:
275 "bg-destructive text-destructive-foreground hover:bg-destructive/90",
276 outline:
277 "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
278 secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
279 ghost: "hover:bg-accent hover:text-accent-foreground",
280 link: "text-primary underline-offset-4 hover:underline",
281 };
282
283 const sizes = {
284 default: "h-10 px-4 py-2",
285 sm: "h-9 rounded-md px-3",
286 lg: "h-11 rounded-md px-8",
287 icon: "h-10 w-10",
288 };
289
290 return (
291 <button
292 className={cn(
293 "inline-flex items-center justify-center whitespace-nowrap 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",
294 variants[variant],
295 sizes[size],
296 className
297 )}
298 ref={ref}
299 {...props}
300 />
301 );
302});
303Button.displayName = "Button";
304
305// Input Component
306const Input = React.forwardRef<
307 HTMLInputElement,
308 React.InputHTMLAttributes<HTMLInputElement>
309>(({ className, type, ...props }, ref) => {
310 return (
311 <input
312 type={type}
313 className={cn(
314 "flex h-10 w-full rounded-md 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",
315 className
316 )}
317 ref={ref}
318 {...props}
319 />
320 );
321});
322Input.displayName = "Input";
323
324// Label Component
325const Label = React.forwardRef<
326 HTMLLabelElement,
327 React.LabelHTMLAttributes<HTMLLabelElement>
328>(({ className, ...props }, ref) => (
329 <label
330 ref={ref}
331 className={cn(
332 "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
333 className
334 )}
335 {...props}
336 />
337));
338Label.displayName = "Label";
339
340// Select Component
341const Select = React.forwardRef<
342 HTMLSelectElement,
343 React.SelectHTMLAttributes<HTMLSelectElement>
344>(({ className, children, ...props }, ref) => {
345 return (
346 <select
347 className={cn(
348 "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
349 className
350 )}
351 ref={ref}
352 {...props}
353 >
354 {children}
355 </select>
356 );
357});
358Select.displayName = "Select";
359
360// Badge Component
361const Badge = React.forwardRef<
362 HTMLDivElement,
363 React.HTMLAttributes<HTMLDivElement> & {
364 variant?: "default" | "secondary" | "destructive" | "outline";
365 }
366>(({ className, variant = "default", ...props }, ref) => {
367 const variants = {
368 default:
369 "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
370 secondary:
371 "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
372 destructive:
373 "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
374 outline: "text-foreground",
375 };
376
377 return (
378 <div
379 ref={ref}
380 className={cn(
381 "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",
382 variants[variant],
383 className
384 )}
385 {...props}
386 />
387 );
388});
389Badge.displayName = "Badge";
390
391// Card Components
392const Card = React.forwardRef<
393 HTMLDivElement,
394 React.HTMLAttributes<HTMLDivElement>
395>(({ className, ...props }, ref) => (
396 <div
397 ref={ref}
398 className={cn(
399 "rounded-lg border bg-card text-card-foreground shadow-sm",
400 className
401 )}
402 {...props}
403 />
404));
405Card.displayName = "Card";
406
407const CardHeader = React.forwardRef<
408 HTMLDivElement,
409 React.HTMLAttributes<HTMLDivElement>
410>(({ className, ...props }, ref) => (
411 <div
412 ref={ref}
413 className={cn("flex flex-col space-y-1.5 p-6", className)}
414 {...props}
415 />
416));
417CardHeader.displayName = "CardHeader";
418
419const CardTitle = React.forwardRef<
420 HTMLParagraphElement,
421 React.HTMLAttributes<HTMLHeadingElement>
422>(({ className, ...props }, ref) => (
423 <h3
424 ref={ref}
425 className={cn(
426 "text-2xl font-semibold leading-none tracking-tight",
427 className
428 )}
429 {...props}
430 />
431));
432CardTitle.displayName = "CardTitle";
433
434const CardContent = React.forwardRef<
435 HTMLDivElement,
436 React.HTMLAttributes<HTMLDivElement>
437>(({ className, ...props }, ref) => (
438 <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
439));
440CardContent.displayName = "CardContent";
441
442// VPS Form Component
443const VPSForm: React.FC<{
444 initialData?: Partial<VPSFormData>;
445 onSubmit: (data: VPSFormData) => void;
446 onCancel: () => void;
447 isEditing?: boolean;
448}> = ({ initialData, onSubmit, onCancel, isEditing = false }) => {
449 const [formData, setFormData] = useState<VPSFormData>({
450 name: initialData?.name || "",
451 type: initialData?.type || "shared",
452 cpu: initialData?.cpu || 1,
453 ram: initialData?.ram || 1,
454 storage: initialData?.storage || 20,
455 bandwidth: initialData?.bandwidth || 100,
456 location: initialData?.location || "",
457 os: initialData?.os || "",
458 customerId: initialData?.customerId || "",
459 customerName: initialData?.customerName || "",
460 customerEmail: initialData?.customerEmail || "",
461 monthlyPrice: initialData?.monthlyPrice || 0,
462 });
463
464 const handleSubmit = (e: React.FormEvent) => {
465 e.preventDefault();
466 onSubmit(formData);
467 };
468
469 const handleChange = (field: keyof VPSFormData, value: string | number) => {
470 setFormData((prev) => ({ ...prev, [field]: value }));
471 };
472
473 return (
474 <form onSubmit={handleSubmit} className="space-y-6">
475 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
476 <div className="space-y-2">
477 <Label htmlFor="name">Instance Name</Label>
478 <Input
479 id="name"
480 value={formData.name}
481 onChange={(e) => handleChange("name", e.target.value)}
482 placeholder="e.g., Production Server 1"
483 required
484 />
485 </div>
486
487 <div className="space-y-2">
488 <Label htmlFor="type">VPS Type</Label>
489 <Select
490 id="type"
491 value={formData.type}
492 onChange={(e) =>
493 handleChange("type", e.target.value as "shared" | "dedicated")
494 }
495 required
496 >
497 <option value="shared">Shared VPS</option>
498 <option value="dedicated">Dedicated VPS</option>
499 </Select>
500 </div>
501
502 <div className="space-y-2">
503 <Label htmlFor="cpu">CPU Cores</Label>
504 <Input
505 id="cpu"
506 type="number"
507 min="1"
508 max="32"
509 value={formData.cpu}
510 onChange={(e) => handleChange("cpu", parseInt(e.target.value))}
511 required
512 />
513 </div>
514
515 <div className="space-y-2">
516 <Label htmlFor="ram">RAM (GB)</Label>
517 <Input
518 id="ram"
519 type="number"
520 min="1"
521 max="128"
522 value={formData.ram}
523 onChange={(e) => handleChange("ram", parseInt(e.target.value))}
524 required
525 />
526 </div>
527
528 <div className="space-y-2">
529 <Label htmlFor="storage">Storage (GB)</Label>
530 <Input
531 id="storage"
532 type="number"
533 min="10"
534 max="2000"
535 value={formData.storage}
536 onChange={(e) => handleChange("storage", parseInt(e.target.value))}
537 required
538 />
539 </div>
540
541 <div className="space-y-2">
542 <Label htmlFor="bandwidth">Bandwidth (GB/month)</Label>
543 <Input
544 id="bandwidth"
545 type="number"
546 min="100"
547 max="10000"
548 value={formData.bandwidth}
549 onChange={(e) =>
550 handleChange("bandwidth", parseInt(e.target.value))
551 }
552 required
553 />
554 </div>
555
556 <div className="space-y-2">
557 <Label htmlFor="location">Location</Label>
558 <Select
559 id="location"
560 value={formData.location}
561 onChange={(e) => handleChange("location", e.target.value)}
562 required
563 >
564 <option value="">Select Location</option>
565 <option value="us-east-1">US East (Virginia)</option>
566 <option value="us-west-2">US West (Oregon)</option>
567 <option value="eu-west-1">Europe (Ireland)</option>
568 <option value="ap-southeast-1">Asia Pacific (Singapore)</option>
569 <option value="ap-northeast-1">Asia Pacific (Tokyo)</option>
570 </Select>
571 </div>
572
573 <div className="space-y-2">
574 <Label htmlFor="os">Operating System</Label>
575 <Select
576 id="os"
577 value={formData.os}
578 onChange={(e) => handleChange("os", e.target.value)}
579 required
580 >
581 <option value="">Select OS</option>
582 <option value="ubuntu-20.04">Ubuntu 20.04 LTS</option>
583 <option value="ubuntu-22.04">Ubuntu 22.04 LTS</option>
584 <option value="centos-7">CentOS 7</option>
585 <option value="centos-8">CentOS 8</option>
586 <option value="debian-10">Debian 10</option>
587 <option value="debian-11">Debian 11</option>
588 <option value="windows-server-2019">Windows Server 2019</option>
589 <option value="windows-server-2022">Windows Server 2022</option>
590 </Select>
591 </div>
592
593 <div className="space-y-2">
594 <Label htmlFor="customerId">Customer ID</Label>
595 <Input
596 id="customerId"
597 value={formData.customerId}
598 onChange={(e) => handleChange("customerId", e.target.value)}
599 placeholder="e.g., CUST-001"
600 required
601 />
602 </div>
603
604 <div className="space-y-2">
605 <Label htmlFor="customerName">Customer Name</Label>
606 <Input
607 id="customerName"
608 value={formData.customerName}
609 onChange={(e) => handleChange("customerName", e.target.value)}
610 placeholder="e.g., John Doe"
611 required
612 />
613 </div>
614
615 <div className="space-y-2">
616 <Label htmlFor="customerEmail">Customer Email</Label>
617 <Input
618 id="customerEmail"
619 type="email"
620 value={formData.customerEmail}
621 onChange={(e) => handleChange("customerEmail", e.target.value)}
622 placeholder="e.g., john@example.com"
623 required
624 />
625 </div>
626
627 <div className="space-y-2">
628 <Label htmlFor="monthlyPrice">Monthly Price ($)</Label>
629 <Input
630 id="monthlyPrice"
631 type="number"
632 min="0"
633 step="0.01"
634 value={formData.monthlyPrice}
635 onChange={(e) =>
636 handleChange("monthlyPrice", parseFloat(e.target.value))
637 }
638 placeholder="e.g., 29.99"
639 required
640 />
641 </div>
642 </div>
643
644 <div className="flex justify-end space-x-2">
645 <Button type="button" variant="outline" onClick={onCancel}>
646 Cancel
647 </Button>
648 <Button type="submit">
649 {isEditing ? "Update Instance" : "Create Instance"}
650 </Button>
651 </div>
652 </form>
653 );
654};
655
656// Status Badge Component
657const StatusBadge: React.FC<{ status: VPSInstance["status"] }> = ({
658 status,
659}) => {
660 const statusConfig = {
661 active: { variant: "default" as const, icon: CheckCircle, text: "Active" },
662 inactive: {
663 variant: "secondary" as const,
664 icon: XCircle,
665 text: "Inactive",
666 },
667 maintenance: {
668 variant: "outline" as const,
669 icon: Clock,
670 text: "Maintenance",
671 },
672 error: {
673 variant: "destructive" as const,
674 icon: AlertTriangle,
675 text: "Error",
676 },
677 };
678
679 const config = statusConfig[status];
680 const Icon = config.icon;
681
682 return (
683 <Badge variant={config.variant} className="flex items-center gap-1">
684 <Icon className="w-3 h-3" />
685 {config.text}
686 </Badge>
687 );
688};
689
690// Main VPS Admin Component
691const VPSAdminUI: React.FC = () => {
692 const [instances, setInstances] = useState<VPSInstance[]>([
693 {
694 id: "vps-001",
695 name: "Production Web Server",
696 type: "dedicated",
697 status: "active",
698 cpu: 4,
699 ram: 8,
700 storage: 100,
701 bandwidth: 1000,
702 ip: "192.168.1.100",
703 location: "us-east-1",
704 os: "ubuntu-22.04",
705 createdAt: "2024-01-15",
706 lastUpdated: "2024-01-20",
707 monthlyPrice: 89.99,
708 customerId: "CUST-001",
709 customerName: "Acme Corp",
710 customerEmail: "admin@acme.com",
711 },
712 {
713 id: "vps-002",
714 name: "Development Server",
715 type: "shared",
716 status: "active",
717 cpu: 2,
718 ram: 4,
719 storage: 50,
720 bandwidth: 500,
721 ip: "192.168.1.101",
722 location: "us-west-2",
723 os: "ubuntu-20.04",
724 createdAt: "2024-01-10",
725 lastUpdated: "2024-01-18",
726 monthlyPrice: 29.99,
727 customerId: "CUST-002",
728 customerName: "TechStart Inc",
729 customerEmail: "dev@techstart.com",
730 },
731 ]);
732
733 const [search, setSearch] = useState("");
734 const [filterType, setFilterType] = useState<"all" | "shared" | "dedicated">(
735 "all"
736 );
737 const [filterStatus, setFilterStatus] = useState<
738 "all" | VPSInstance["status"]
739 >("all");
740 const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
741 const [editingInstance, setEditingInstance] = useState<VPSInstance | null>(
742 null
743 );
744 const [deletingInstance, setDeletingInstance] = useState<VPSInstance | null>(
745 null
746 );
747
748 const filteredInstances = instances.filter((instance) => {
749 const matchesSearch =
750 instance.name.toLowerCase().includes(search.toLowerCase()) ||
751 instance.customerName.toLowerCase().includes(search.toLowerCase()) ||
752 instance.ip.includes(search);
753 const matchesType = filterType === "all" || instance.type === filterType;
754 const matchesStatus =
755 filterStatus === "all" || instance.status === filterStatus;
756
757 return matchesSearch && matchesType && matchesStatus;
758 });
759
760 const handleCreateInstance = (data: VPSFormData) => {
761 const newInstance: VPSInstance = {
762 id: `vps-${Date.now()}`,
763 ...data,
764 ip: `192.168.1.${Math.floor(Math.random() * 255)}`,
765 status: "active",
766 createdAt: new Date().toISOString().split("T")[0],
767 lastUpdated: new Date().toISOString().split("T")[0],
768 };
769
770 setInstances((prev) => [...prev, newInstance]);
771 setIsCreateDialogOpen(false);
772 };
773
774 const handleEditInstance = (data: VPSFormData) => {
775 if (!editingInstance) return;
776
777 setInstances((prev) =>
778 prev.map((instance) =>
779 instance.id === editingInstance.id
780 ? {
781 ...instance,
782 ...data,
783 lastUpdated: new Date().toISOString().split("T")[0],
784 }
785 : instance
786 )
787 );
788 setEditingInstance(null);
789 };
790
791 const handleDeleteInstance = () => {
792 if (!deletingInstance) return;
793
794 setInstances((prev) =>
795 prev.filter((instance) => instance.id !== deletingInstance.id)
796 );
797 setDeletingInstance(null);
798 };
799
800 const stats = {
801 total: instances.length,
802 active: instances.filter((i) => i.status === "active").length,
803 shared: instances.filter((i) => i.type === "shared").length,
804 dedicated: instances.filter((i) => i.type === "dedicated").length,
805 };
806
807 return (
808 <div className="min-h-screen bg-background p-6">
809 <div className="max-w-7xl mx-auto space-y-6">
810 {/* Header */}
811 <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
812 <div>
813 <h1 className="text-3xl font-bold tracking-tight">
814 VPS Management
815 </h1>
816 <p className="text-muted-foreground">
817 Manage and monitor VPS instances for your customers
818 </p>
819 </div>
820 <Button
821 onClick={() => setIsCreateDialogOpen(true)}
822 className="flex items-center gap-2"
823 >
824 <Plus className="w-4 h-4" />
825 Add New Instance
826 </Button>
827 </div>
828
829 {/* Stats Cards */}
830 <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
831 <Card>
832 <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
833 <CardTitle className="text-sm font-medium">
834 Total Instances
835 </CardTitle>
836 <Server className="h-4 w-4 text-muted-foreground" />
837 </CardHeader>
838 <CardContent>
839 <div className="text-2xl font-bold">{stats.total}</div>
840 </CardContent>
841 </Card>
842
843 <Card>
844 <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
845 <CardTitle className="text-sm font-medium">Active</CardTitle>
846 <Activity className="h-4 w-4 text-green-600" />
847 </CardHeader>
848 <CardContent>
849 <div className="text-2xl font-bold text-green-600">
850 {stats.active}
851 </div>
852 </CardContent>
853 </Card>
854
855 <Card>
856 <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
857 <CardTitle className="text-sm font-medium">Shared VPS</CardTitle>
858 <Users className="h-4 w-4 text-blue-600" />
859 </CardHeader>
860 <CardContent>
861 <div className="text-2xl font-bold text-blue-600">
862 {stats.shared}
863 </div>
864 </CardContent>
865 </Card>
866
867 <Card>
868 <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
869 <CardTitle className="text-sm font-medium">
870 Dedicated VPS
871 </CardTitle>
872 <Shield className="h-4 w-4 text-purple-600" />
873 </CardHeader>
874 <CardContent>
875 <div className="text-2xl font-bold text-purple-600">
876 {stats.dedicated}
877 </div>
878 </CardContent>
879 </Card>
880 </div>
881
882 {/* Filters */}
883 <Card>
884 <CardContent className="pt-6">
885 <div className="flex flex-col sm:flex-row gap-4">
886 <div className="relative flex-1">
887 <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
888 <Input
889 placeholder="Search instances, customers, or IP addresses..."
890 value={search}
891 onChange={(e) => setSearch(e.target.value)}
892 className="pl-10"
893 />
894 </div>
895
896 <Select
897 value={filterType}
898 onChange={(e) => setFilterType(e.target.value as any)}
899 >
900 <option value="all">All Types</option>
901 <option value="shared">Shared VPS</option>
902 <option value="dedicated">Dedicated VPS</option>
903 </Select>
904
905 <Select
906 value={filterStatus}
907 onChange={(e) => setFilterStatus(e.target.value as any)}
908 >
909 <option value="all">All Status</option>
910 <option value="active">Active</option>
911 <option value="inactive">Inactive</option>
912 <option value="maintenance">Maintenance</option>
913 <option value="error">Error</option>
914 </Select>
915 </div>
916 </CardContent>
917 </Card>
918
919 {/* Instances Table */}
920 <Card>
921 <CardContent className="pt-6">
922 <div className="overflow-x-auto">
923 <table className="w-full">
924 <thead>
925 <tr className="border-b">
926 <th className="text-left py-3 px-4 font-medium">
927 Instance
928 </th>
929 <th className="text-left py-3 px-4 font-medium">
930 Customer
931 </th>
932 <th className="text-left py-3 px-4 font-medium">Type</th>
933 <th className="text-left py-3 px-4 font-medium">Status</th>
934 <th className="text-left py-3 px-4 font-medium">
935 Resources
936 </th>
937 <th className="text-left py-3 px-4 font-medium">
938 Location
939 </th>
940 <th className="text-left py-3 px-4 font-medium">Price</th>
941 <th className="text-left py-3 px-4 font-medium">Actions</th>
942 </tr>
943 </thead>
944 <tbody>
945 {filteredInstances.map((instance) => (
946 <tr
947 key={instance.id}
948 className="border-b hover:bg-muted/50"
949 >
950 <td className="py-3 px-4">
951 <div>
952 <div className="font-medium">{instance.name}</div>
953 <div className="text-sm text-muted-foreground">
954 {instance.ip}
955 </div>
956 </div>
957 </td>
958 <td className="py-3 px-4">
959 <div>
960 <div className="font-medium">
961 {instance.customerName}
962 </div>
963 <div className="text-sm text-muted-foreground">
964 {instance.customerEmail}
965 </div>
966 </div>
967 </td>
968 <td className="py-3 px-4">
969 <Badge
970 variant={
971 instance.type === "dedicated"
972 ? "default"
973 : "secondary"
974 }
975 >
976 {instance.type === "dedicated"
977 ? "Dedicated"
978 : "Shared"}
979 </Badge>
980 </td>
981 <td className="py-3 px-4">
982 <StatusBadge status={instance.status} />
983 </td>
984 <td className="py-3 px-4">
985 <div className="text-sm space-y-1">
986 <div className="flex items-center gap-1">
987 <Cpu className="w-3 h-3" />
988 {instance.cpu} cores
989 </div>
990 <div className="flex items-center gap-1">
991 <MemoryStick className="w-3 h-3" />
992 {instance.ram}GB RAM
993 </div>
994 <div className="flex items-center gap-1">
995 <HardDrive className="w-3 h-3" />
996 {instance.storage}GB
997 </div>
998 </div>
999 </td>
1000 <td className="py-3 px-4">
1001 <div className="flex items-center gap-1">
1002 <Globe className="w-3 h-3" />
1003 {instance.location}
1004 </div>
1005 </td>
1006 <td className="py-3 px-4">
1007 <div className="font-medium">
1008 ${instance.monthlyPrice}/mo
1009 </div>
1010 </td>
1011 <td className="py-3 px-4">
1012 <div className="flex items-center gap-2">
1013 <Button
1014 size="sm"
1015 variant="outline"
1016 onClick={() => setEditingInstance(instance)}
1017 >
1018 <Edit className="w-3 h-3" />
1019 </Button>
1020 <Button
1021 size="sm"
1022 variant="destructive"
1023 onClick={() => setDeletingInstance(instance)}
1024 >
1025 <Trash2 className="w-3 h-3" />
1026 </Button>
1027 </div>
1028 </td>
1029 </tr>
1030 ))}
1031 </tbody>
1032 </table>
1033
1034 {filteredInstances.length === 0 && (
1035 <div className="text-center py-12">
1036 <Server className="w-12 h-12 text-muted-foreground mx-auto mb-4" />
1037 <h3 className="text-lg font-medium mb-2">
1038 No instances found
1039 </h3>
1040 <p className="text-muted-foreground">
1041 Try adjusting your search or filters
1042 </p>
1043 </div>
1044 )}
1045 </div>
1046 </CardContent>
1047 </Card>
1048
1049 {/* Create Instance Dialog */}
1050 <Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
1051 <DialogContent className="max-w-4xl">
1052 <DialogHeader>
1053 <DialogTitle>Create New VPS Instance</DialogTitle>
1054 <DialogDescription>
1055 Configure a new VPS instance for your customer. Fill in all
1056 required details.
1057 </DialogDescription>
1058 </DialogHeader>
1059 <VPSForm
1060 onSubmit={handleCreateInstance}
1061 onCancel={() => setIsCreateDialogOpen(false)}
1062 />
1063 </DialogContent>
1064 </Dialog>
1065
1066 {/* Edit Instance Dialog */}
1067 <Dialog
1068 open={!!editingInstance}
1069 onOpenChange={() => setEditingInstance(null)}
1070 >
1071 <DialogContent className="max-w-4xl">
1072 <DialogHeader>
1073 <DialogTitle>Edit VPS Instance</DialogTitle>
1074 <DialogDescription>
1075 Update the configuration for {editingInstance?.name}.
1076 </DialogDescription>
1077 </DialogHeader>
1078 {editingInstance && (
1079 <VPSForm
1080 initialData={editingInstance}
1081 onSubmit={handleEditInstance}
1082 onCancel={() => setEditingInstance(null)}
1083 isEditing
1084 />
1085 )}
1086 </DialogContent>
1087 </Dialog>
1088
1089 {/* Delete Confirmation Dialog */}
1090 <AlertDialog
1091 open={!!deletingInstance}
1092 onOpenChange={() => setDeletingInstance(null)}
1093 >
1094 <AlertDialogContent>
1095 <AlertDialogHeader>
1096 <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
1097 <AlertDialogDescription>
1098 This action cannot be undone. This will permanently delete the
1099 VPS instance "{deletingInstance?.name}" and remove all
1100 associated data.
1101 </AlertDialogDescription>
1102 </AlertDialogHeader>
1103 <div className="flex justify-end gap-2">
1104 <AlertDialogCancel>Cancel</AlertDialogCancel>
1105 <AlertDialogAction
1106 onClick={handleDeleteInstance}
1107 className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
1108 >
1109 Delete Instance
1110 </AlertDialogAction>
1111 </div>
1112 </AlertDialogContent>
1113 </AlertDialog>
1114 </div>
1115 </div>
1116 );
1117};
1118
1119export default VPSAdminUI;
Dependencies
External Libraries
@radix-ui/react-alert-dialog@radix-ui/react-dialog@radix-ui/react-iconslucide-reactreact
Local Components
/lib/utils