ShadcnUI Vaults

Back to Blocks

VPS Instance CRUD Dashboard

Unknown Block

UnknownComponent

VPS Instance CRUD Dashboard

Modern dashboard for creating, editing, and deleting VPS instances with customer assignment, resource allocation, and confirmation dialogs.

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

LICENSE

MIT License

Copyright (c) 2025 Aldhaneka

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Shadcn Vaults Project (CC BY-NC 4.0 with Internal Use Exception)

All user-submitted components in the '/blocks' directory are licensed under the Creative Commons Attribution-NonCommercial 4.0 International License (CC BY-NC 4.0),
with the following clarification and exception:

You are free to:
- Share — copy and redistribute the material in any medium or format
- Adapt — remix, transform, and build upon the material

Under these conditions:
- Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made.
- NonCommercial — You may NOT use the material for commercial redistribution, resale, or monetization.

🚫 You MAY NOT:
- Sell or redistribute the components individually or as part of a product (e.g. a UI kit, template marketplace, SaaS component library)
- Offer the components or derivative works in any paid tool, theme pack, or design system

✅ You MAY:
- Use the components in internal company tools, dashboards, or applications that are not sold as products
- Remix or adapt components for private or enterprise projects
- Use them in open-source non-commercial projects

This license encourages sharing, learning, and internal innovation — but prohibits using these components as a basis for monetized products.

Full license text: https://creativecommons.org/licenses/by-nc/4.0/

By submitting a component, contributors agree to these terms.

Contributors

Ramiro Godoy@milogodoy

Review Form Block

Aldhaneka@Aldhanekaa

Project Creator

For questions about licensing, please contact the project maintainers.