ShadcnUI Vaults

Back to Blocks

Animated VPS Management Suite

Unknown Block

UnknownComponent

Animated VPS Management Suite

Animated and modern VPS management suite with advanced stats, customer analytics, revenue tracking, and editable VPS instance table.

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

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.