ShadcnUI Vaults

Back to Blocks

VPS Resource Analytics Dashboard

Unknown Block

UnknownComponent

VPS Resource Analytics Dashboard

Advanced VPS analytics dashboard with expandable resource usage charts, customer details, and real-time monitoring for all VPS types.

Preview

Full width desktop view

Code

monitoring-9.tsx
1"use client";
2
3import React, { useState, useEffect } from "react";
4import { motion, AnimatePresence } from "framer-motion";
5import {
6  Server,
7  Plus,
8  Settings,
9  Activity,
10  Cpu,
11  HardDrive,
12  MemoryStick,
13  MapPin,
14  Users,
15  Monitor,
16  Power,
17  PowerOff,
18  Pause,
19  Play,
20  Trash2,
21  Edit,
22  Eye,
23  ChevronDown,
24  ChevronRight,
25  Filter,
26  Search,
27  MoreHorizontal,
28  AlertCircle,
29  CheckCircle,
30  Clock,
31  Database,
32  Globe,
33  Shield,
34  Zap,
35} from "lucide-react";
36import { Button } from "@/components/ui/button";
37import {
38  Card,
39  CardContent,
40  CardDescription,
41  CardHeader,
42  CardTitle,
43} from "@/components/ui/card";
44import { Badge } from "@/components/ui/badge";
45import { Input } from "@/components/ui/input";
46import { Label } from "@/components/ui/label";
47import {
48  Select,
49  SelectContent,
50  SelectItem,
51  SelectTrigger,
52  SelectValue,
53} from "@/components/ui/select";
54import {
55  Dialog,
56  DialogContent,
57  DialogDescription,
58  DialogFooter,
59  DialogHeader,
60  DialogTitle,
61  DialogTrigger,
62} from "@/components/ui/dialog";
63import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
64import { Progress } from "@/components/ui/progress";
65import { Separator } from "@/components/ui/separator";
66import {
67  DropdownMenu,
68  DropdownMenuContent,
69  DropdownMenuItem,
70  DropdownMenuTrigger,
71} from "@/components/ui/dropdown-menu";
72import {
73  ChartContainer,
74  ChartTooltip,
75  ChartTooltipContent,
76} from "@/components/ui/chart";
77import {
78  AreaChart,
79  Area,
80  XAxis,
81  YAxis,
82  ResponsiveContainer,
83  LineChart,
84  Line,
85} from "recharts";
86
87// Types
88interface VPSParent {
89  id: string;
90  name: string;
91  region: string;
92  cpu: number;
93  ram: number;
94  storage: number;
95  status: "online" | "offline" | "maintenance";
96  ipAddress: string;
97  createdAt: string;
98  usedCpu: number;
99  usedRam: number;
100  usedStorage: number;
101  sharedVPS: SharedVPS[];
102  dedicatedVPS: DedicatedVPS[];
103}
104
105interface SharedVPS {
106  id: string;
107  name: string;
108  parentId: string;
109  cpu: number;
110  ram: number;
111  storage: number;
112  status: "running" | "stopped" | "suspended";
113  customer: string;
114  domain?: string;
115  createdAt: string;
116  usedCpu: number;
117  usedRam: number;
118  usedStorage: number;
119}
120
121interface DedicatedVPS {
122  id: string;
123  name: string;
124  region: string;
125  cpu: number;
126  ram: number;
127  storage: number;
128  status: "running" | "stopped" | "suspended";
129  customer: string;
130  domain?: string;
131  ipAddress: string;
132  createdAt: string;
133  usedCpu: number;
134  usedRam: number;
135  usedStorage: number;
136}
137
138// Sample data
139const sampleVPSParents: VPSParent[] = [
140  {
141    id: "parent-1",
142    name: "US-East-1-Parent",
143    region: "US East (Virginia)",
144    cpu: 32,
145    ram: 128,
146    storage: 2000,
147    status: "online",
148    ipAddress: "192.168.1.100",
149    createdAt: "2024-01-15",
150    usedCpu: 18,
151    usedRam: 85,
152    usedStorage: 1200,
153    sharedVPS: [
154      {
155        id: "shared-1",
156        name: "WebStore-Alpha",
157        parentId: "parent-1",
158        cpu: 4,
159        ram: 16,
160        storage: 200,
161        status: "running",
162        customer: "TechCorp Inc",
163        domain: "techcorp-store.com",
164        createdAt: "2024-02-01",
165        usedCpu: 2.5,
166        usedRam: 12,
167        usedStorage: 150,
168      },
169      {
170        id: "shared-2",
171        name: "WebStore-Beta",
172        parentId: "parent-1",
173        cpu: 2,
174        ram: 8,
175        storage: 100,
176        status: "running",
177        customer: "StartupXYZ",
178        domain: "startupxyz.shop",
179        createdAt: "2024-02-10",
180        usedCpu: 1.2,
181        usedRam: 6,
182        usedStorage: 75,
183      },
184    ],
185    dedicatedVPS: [],
186  },
187  {
188    id: "parent-2",
189    name: "EU-West-1-Parent",
190    region: "EU West (Ireland)",
191    cpu: 24,
192    ram: 96,
193    storage: 1500,
194    status: "online",
195    ipAddress: "192.168.2.100",
196    createdAt: "2024-01-20",
197    usedCpu: 8,
198    usedRam: 32,
199    usedStorage: 600,
200    sharedVPS: [],
201    dedicatedVPS: [],
202  },
203];
204
205const sampleDedicatedVPS: DedicatedVPS[] = [
206  {
207    id: "dedicated-1",
208    name: "Enterprise-Store-1",
209    region: "US West (California)",
210    cpu: 16,
211    ram: 64,
212    storage: 1000,
213    status: "running",
214    customer: "Enterprise Corp",
215    domain: "enterprise-store.com",
216    ipAddress: "203.0.113.10",
217    createdAt: "2024-01-25",
218    usedCpu: 8.5,
219    usedRam: 45,
220    usedStorage: 650,
221  },
222  {
223    id: "dedicated-2",
224    name: "Premium-Shop-2",
225    region: "Asia Pacific (Singapore)",
226    cpu: 8,
227    ram: 32,
228    storage: 500,
229    status: "stopped",
230    customer: "Premium Retail",
231    domain: "premium-shop.asia",
232    ipAddress: "203.0.113.20",
233    createdAt: "2024-02-05",
234    usedCpu: 0,
235    usedRam: 0,
236    usedStorage: 200,
237  },
238];
239
240const regions = [
241  "US East (Virginia)",
242  "US West (California)",
243  "EU West (Ireland)",
244  "EU Central (Frankfurt)",
245  "Asia Pacific (Singapore)",
246  "Asia Pacific (Tokyo)",
247];
248
249// Chart data
250const chartConfig = {
251  cpu: { label: "CPU", color: "hsl(var(--chart-1))" },
252  ram: { label: "RAM", color: "hsl(var(--chart-2))" },
253  storage: { label: "Storage", color: "hsl(var(--chart-3))" },
254};
255
256const generateMetricsData = () => {
257  return Array.from({ length: 24 }, (_, i) => ({
258    time: `${i.toString().padStart(2, "0")}:00`,
259    cpu: Math.floor(Math.random() * 100),
260    ram: Math.floor(Math.random() * 100),
261    storage: Math.floor(Math.random() * 100),
262  }));
263};
264
265// Status badge component
266const StatusBadge = ({ status }: { status: string }) => {
267  const getStatusColor = (status: string) => {
268    switch (status) {
269      case "online":
270      case "running":
271        return "bg-green-500";
272      case "offline":
273      case "stopped":
274        return "bg-red-500";
275      case "maintenance":
276      case "suspended":
277        return "bg-yellow-500";
278      default:
279        return "bg-gray-500";
280    }
281  };
282
283  const getStatusIcon = (status: string) => {
284    switch (status) {
285      case "online":
286      case "running":
287        return <CheckCircle className="w-3 h-3" />;
288      case "offline":
289      case "stopped":
290        return <PowerOff className="w-3 h-3" />;
291      case "maintenance":
292      case "suspended":
293        return <Clock className="w-3 h-3" />;
294      default:
295        return <AlertCircle className="w-3 h-3" />;
296    }
297  };
298
299  return (
300    <Badge variant="secondary" className="flex items-center gap-1">
301      <div className={`w-2 h-2 rounded-full ${getStatusColor(status)}`} />
302      {getStatusIcon(status)}
303      {status}
304    </Badge>
305  );
306};
307
308// Resource usage component
309const ResourceUsage = ({
310  used,
311  total,
312  label,
313  unit,
314}: {
315  used: number;
316  total: number;
317  label: string;
318  unit: string;
319}) => {
320  const percentage = (used / total) * 100;
321
322  return (
323    <div className="space-y-2">
324      <div className="flex justify-between text-sm">
325        <span className="text-muted-foreground">{label}</span>
326        <span className="font-medium">
327          {used}
328          {unit} / {total}
329          {unit}
330        </span>
331      </div>
332      <Progress value={percentage} className="h-2" />
333      <div className="text-xs text-muted-foreground text-right">
334        {percentage.toFixed(1)}%
335      </div>
336    </div>
337  );
338};
339
340// VPS Parent Card
341const VPSParentCard = ({
342  parent,
343  onEdit,
344  onDelete,
345}: {
346  parent: VPSParent;
347  onEdit: () => void;
348  onDelete: () => void;
349}) => {
350  const [expanded, setExpanded] = useState(false);
351  const metricsData = generateMetricsData();
352
353  return (
354    <Card className="w-full">
355      <CardHeader className="pb-3">
356        <div className="flex items-center justify-between">
357          <div className="flex items-center gap-3">
358            <div className="p-2 bg-primary/10 rounded-lg">
359              <Server className="w-5 h-5 text-primary" />
360            </div>
361            <div>
362              <CardTitle className="text-lg">{parent.name}</CardTitle>
363              <CardDescription className="flex items-center gap-2">
364                <MapPin className="w-3 h-3" />
365                {parent.region}
366              </CardDescription>
367            </div>
368          </div>
369          <div className="flex items-center gap-2">
370            <StatusBadge status={parent.status} />
371            <DropdownMenu>
372              <DropdownMenuTrigger asChild>
373                <Button variant="ghost" size="sm">
374                  <MoreHorizontal className="w-4 h-4" />
375                </Button>
376              </DropdownMenuTrigger>
377              <DropdownMenuContent>
378                <DropdownMenuItem onClick={onEdit}>
379                  <Edit className="w-4 h-4 mr-2" />
380                  Edit
381                </DropdownMenuItem>
382                <DropdownMenuItem onClick={() => setExpanded(!expanded)}>
383                  <Eye className="w-4 h-4 mr-2" />
384                  {expanded ? "Collapse" : "Expand"}
385                </DropdownMenuItem>
386                <DropdownMenuItem
387                  onClick={onDelete}
388                  className="text-destructive"
389                >
390                  <Trash2 className="w-4 h-4 mr-2" />
391                  Delete
392                </DropdownMenuItem>
393              </DropdownMenuContent>
394            </DropdownMenu>
395            <Button
396              variant="ghost"
397              size="sm"
398              onClick={() => setExpanded(!expanded)}
399            >
400              {expanded ? (
401                <ChevronDown className="w-4 h-4" />
402              ) : (
403                <ChevronRight className="w-4 h-4" />
404              )}
405            </Button>
406          </div>
407        </div>
408      </CardHeader>
409
410      <CardContent className="space-y-4">
411        <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
412          <ResourceUsage
413            used={parent.usedCpu}
414            total={parent.cpu}
415            label="CPU"
416            unit=" cores"
417          />
418          <ResourceUsage
419            used={parent.usedRam}
420            total={parent.ram}
421            label="RAM"
422            unit=" GB"
423          />
424          <ResourceUsage
425            used={parent.usedStorage}
426            total={parent.storage}
427            label="Storage"
428            unit=" GB"
429          />
430        </div>
431
432        <AnimatePresence>
433          {expanded && (
434            <motion.div
435              initial={{ opacity: 0, height: 0 }}
436              animate={{ opacity: 1, height: "auto" }}
437              exit={{ opacity: 0, height: 0 }}
438              transition={{ duration: 0.3 }}
439              className="space-y-4"
440            >
441              <Separator />
442
443              <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
444                <div>
445                  <h4 className="font-medium mb-3 flex items-center gap-2">
446                    <Activity className="w-4 h-4" />
447                    Resource Usage (24h)
448                  </h4>
449                  <ChartContainer config={chartConfig} className="h-[200px]">
450                    <ResponsiveContainer width="100%" height="100%">
451                      <AreaChart data={metricsData}>
452                        <XAxis dataKey="time" />
453                        <YAxis />
454                        <ChartTooltip content={<ChartTooltipContent />} />
455                        <Area
456                          type="monotone"
457                          dataKey="cpu"
458                          stackId="1"
459                          stroke="var(--color-cpu)"
460                          fill="var(--color-cpu)"
461                          fillOpacity={0.6}
462                        />
463                        <Area
464                          type="monotone"
465                          dataKey="ram"
466                          stackId="1"
467                          stroke="var(--color-ram)"
468                          fill="var(--color-ram)"
469                          fillOpacity={0.6}
470                        />
471                      </AreaChart>
472                    </ResponsiveContainer>
473                  </ChartContainer>
474                </div>
475
476                <div className="space-y-3">
477                  <h4 className="font-medium flex items-center gap-2">
478                    <Database className="w-4 h-4" />
479                    Details
480                  </h4>
481                  <div className="space-y-2 text-sm">
482                    <div className="flex justify-between">
483                      <span className="text-muted-foreground">IP Address:</span>
484                      <span className="font-mono">{parent.ipAddress}</span>
485                    </div>
486                    <div className="flex justify-between">
487                      <span className="text-muted-foreground">Created:</span>
488                      <span>{parent.createdAt}</span>
489                    </div>
490                    <div className="flex justify-between">
491                      <span className="text-muted-foreground">Shared VPS:</span>
492                      <span>{parent.sharedVPS.length}</span>
493                    </div>
494                    <div className="flex justify-between">
495                      <span className="text-muted-foreground">
496                        Dedicated VPS:
497                      </span>
498                      <span>{parent.dedicatedVPS.length}</span>
499                    </div>
500                  </div>
501                </div>
502              </div>
503
504              {parent.sharedVPS.length > 0 && (
505                <div>
506                  <h4 className="font-medium mb-3 flex items-center gap-2">
507                    <Users className="w-4 h-4" />
508                    Shared VPS Instances
509                  </h4>
510                  <div className="grid gap-3">
511                    {parent.sharedVPS.map((vps) => (
512                      <div
513                        key={vps.id}
514                        className="p-3 border rounded-lg bg-muted/30"
515                      >
516                        <div className="flex items-center justify-between mb-2">
517                          <div className="flex items-center gap-2">
518                            <Monitor className="w-4 h-4 text-muted-foreground" />
519                            <span className="font-medium">{vps.name}</span>
520                            <StatusBadge status={vps.status} />
521                          </div>
522                          <div className="text-sm text-muted-foreground">
523                            {vps.customer}
524                          </div>
525                        </div>
526                        <div className="grid grid-cols-3 gap-4 text-sm">
527                          <div>
528                            <span className="text-muted-foreground">CPU: </span>
529                            <span>
530                              {vps.usedCpu}/{vps.cpu} cores
531                            </span>
532                          </div>
533                          <div>
534                            <span className="text-muted-foreground">RAM: </span>
535                            <span>
536                              {vps.usedRam}/{vps.ram} GB
537                            </span>
538                          </div>
539                          <div>
540                            <span className="text-muted-foreground">
541                              Storage:{" "}
542                            </span>
543                            <span>
544                              {vps.usedStorage}/{vps.storage} GB
545                            </span>
546                          </div>
547                        </div>
548                        {vps.domain && (
549                          <div className="mt-2 text-sm">
550                            <span className="text-muted-foreground">
551                              Domain:{" "}
552                            </span>
553                            <span className="font-mono">{vps.domain}</span>
554                          </div>
555                        )}
556                      </div>
557                    ))}
558                  </div>
559                </div>
560              )}
561            </motion.div>
562          )}
563        </AnimatePresence>
564      </CardContent>
565    </Card>
566  );
567};
568
569// Dedicated VPS Card
570const DedicatedVPSCard = ({
571  vps,
572  onEdit,
573  onDelete,
574}: {
575  vps: DedicatedVPS;
576  onEdit: () => void;
577  onDelete: () => void;
578}) => {
579  const [expanded, setExpanded] = useState(false);
580  const metricsData = generateMetricsData();
581
582  return (
583    <Card className="w-full">
584      <CardHeader className="pb-3">
585        <div className="flex items-center justify-between">
586          <div className="flex items-center gap-3">
587            <div className="p-2 bg-blue-500/10 rounded-lg">
588              <Shield className="w-5 h-5 text-blue-500" />
589            </div>
590            <div>
591              <CardTitle className="text-lg">{vps.name}</CardTitle>
592              <CardDescription className="flex items-center gap-2">
593                <MapPin className="w-3 h-3" />
594                {vps.region}
595              </CardDescription>
596            </div>
597          </div>
598          <div className="flex items-center gap-2">
599            <StatusBadge status={vps.status} />
600            <DropdownMenu>
601              <DropdownMenuTrigger asChild>
602                <Button variant="ghost" size="sm">
603                  <MoreHorizontal className="w-4 h-4" />
604                </Button>
605              </DropdownMenuTrigger>
606              <DropdownMenuContent>
607                <DropdownMenuItem onClick={onEdit}>
608                  <Edit className="w-4 h-4 mr-2" />
609                  Edit
610                </DropdownMenuItem>
611                <DropdownMenuItem onClick={() => setExpanded(!expanded)}>
612                  <Eye className="w-4 h-4 mr-2" />
613                  {expanded ? "Collapse" : "Expand"}
614                </DropdownMenuItem>
615                <DropdownMenuItem
616                  onClick={onDelete}
617                  className="text-destructive"
618                >
619                  <Trash2 className="w-4 h-4 mr-2" />
620                  Delete
621                </DropdownMenuItem>
622              </DropdownMenuContent>
623            </DropdownMenu>
624            <Button
625              variant="ghost"
626              size="sm"
627              onClick={() => setExpanded(!expanded)}
628            >
629              {expanded ? (
630                <ChevronDown className="w-4 h-4" />
631              ) : (
632                <ChevronRight className="w-4 h-4" />
633              )}
634            </Button>
635          </div>
636        </div>
637      </CardHeader>
638
639      <CardContent className="space-y-4">
640        <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
641          <ResourceUsage
642            used={vps.usedCpu}
643            total={vps.cpu}
644            label="CPU"
645            unit=" cores"
646          />
647          <ResourceUsage
648            used={vps.usedRam}
649            total={vps.ram}
650            label="RAM"
651            unit=" GB"
652          />
653          <ResourceUsage
654            used={vps.usedStorage}
655            total={vps.storage}
656            label="Storage"
657            unit=" GB"
658          />
659        </div>
660
661        <AnimatePresence>
662          {expanded && (
663            <motion.div
664              initial={{ opacity: 0, height: 0 }}
665              animate={{ opacity: 1, height: "auto" }}
666              exit={{ opacity: 0, height: 0 }}
667              transition={{ duration: 0.3 }}
668              className="space-y-4"
669            >
670              <Separator />
671
672              <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
673                <div>
674                  <h4 className="font-medium mb-3 flex items-center gap-2">
675                    <Activity className="w-4 h-4" />
676                    Resource Usage (24h)
677                  </h4>
678                  <ChartContainer config={chartConfig} className="h-[200px]">
679                    <ResponsiveContainer width="100%" height="100%">
680                      <LineChart data={metricsData}>
681                        <XAxis dataKey="time" />
682                        <YAxis />
683                        <ChartTooltip content={<ChartTooltipContent />} />
684                        <Line
685                          type="monotone"
686                          dataKey="cpu"
687                          stroke="var(--color-cpu)"
688                          strokeWidth={2}
689                        />
690                        <Line
691                          type="monotone"
692                          dataKey="ram"
693                          stroke="var(--color-ram)"
694                          strokeWidth={2}
695                        />
696                      </LineChart>
697                    </ResponsiveContainer>
698                  </ChartContainer>
699                </div>
700
701                <div className="space-y-3">
702                  <h4 className="font-medium flex items-center gap-2">
703                    <Database className="w-4 h-4" />
704                    Details
705                  </h4>
706                  <div className="space-y-2 text-sm">
707                    <div className="flex justify-between">
708                      <span className="text-muted-foreground">Customer:</span>
709                      <span>{vps.customer}</span>
710                    </div>
711                    <div className="flex justify-between">
712                      <span className="text-muted-foreground">IP Address:</span>
713                      <span className="font-mono">{vps.ipAddress}</span>
714                    </div>
715                    {vps.domain && (
716                      <div className="flex justify-between">
717                        <span className="text-muted-foreground">Domain:</span>
718                        <span className="font-mono">{vps.domain}</span>
719                      </div>
720                    )}
721                    <div className="flex justify-between">
722                      <span className="text-muted-foreground">Created:</span>
723                      <span>{vps.createdAt}</span>
724                    </div>
725                  </div>
726                </div>
727              </div>
728            </motion.div>
729          )}
730        </AnimatePresence>
731      </CardContent>
732    </Card>
733  );
734};
735
736// Add VPS Parent Dialog
737const AddVPSParentDialog = ({
738  open,
739  onOpenChange,
740  onAdd,
741}: {
742  open: boolean;
743  onOpenChange: (open: boolean) => void;
744  onAdd: (
745    parent: Omit<
746      VPSParent,
747      | "id"
748      | "usedCpu"
749      | "usedRam"
750      | "usedStorage"
751      | "sharedVPS"
752      | "dedicatedVPS"
753    >
754  ) => void;
755}) => {
756  const [formData, setFormData] = useState({
757    name: "",
758    region: "",
759    cpu: "",
760    ram: "",
761    storage: "",
762    ipAddress: "",
763  });
764
765  const handleSubmit = (e: React.FormEvent) => {
766    e.preventDefault();
767    onAdd({
768      name: formData.name,
769      region: formData.region,
770      cpu: parseInt(formData.cpu),
771      ram: parseInt(formData.ram),
772      storage: parseInt(formData.storage),
773      status: "online",
774      ipAddress: formData.ipAddress,
775      createdAt: new Date().toISOString().split("T")[0],
776    });
777    setFormData({
778      name: "",
779      region: "",
780      cpu: "",
781      ram: "",
782      storage: "",
783      ipAddress: "",
784    });
785    onOpenChange(false);
786  };
787
788  return (
789    <Dialog open={open} onOpenChange={onOpenChange}>
790      <DialogContent className="sm:max-w-[425px]">
791        <DialogHeader>
792          <DialogTitle>Add New VPS Parent</DialogTitle>
793          <DialogDescription>
794            Create a new VPS parent server that can host multiple shared VPS
795            instances.
796          </DialogDescription>
797        </DialogHeader>
798        <form onSubmit={handleSubmit} className="space-y-4">
799          <div className="space-y-2">
800            <Label htmlFor="name">Server Name</Label>
801            <Input
802              id="name"
803              value={formData.name}
804              onChange={(e) =>
805                setFormData({ ...formData, name: e.target.value })
806              }
807              placeholder="e.g., US-East-1-Parent"
808              required
809            />
810          </div>
811
812          <div className="space-y-2">
813            <Label htmlFor="region">Region</Label>
814            <Select
815              value={formData.region}
816              onValueChange={(value) =>
817                setFormData({ ...formData, region: value })
818              }
819            >
820              <SelectTrigger>
821                <SelectValue placeholder="Select region" />
822              </SelectTrigger>
823              <SelectContent>
824                {regions.map((region) => (
825                  <SelectItem key={region} value={region}>
826                    {region}
827                  </SelectItem>
828                ))}
829              </SelectContent>
830            </Select>
831          </div>
832
833          <div className="grid grid-cols-3 gap-4">
834            <div className="space-y-2">
835              <Label htmlFor="cpu">CPU Cores</Label>
836              <Input
837                id="cpu"
838                type="number"
839                value={formData.cpu}
840                onChange={(e) =>
841                  setFormData({ ...formData, cpu: e.target.value })
842                }
843                placeholder="32"
844                required
845              />
846            </div>
847            <div className="space-y-2">
848              <Label htmlFor="ram">RAM (GB)</Label>
849              <Input
850                id="ram"
851                type="number"
852                value={formData.ram}
853                onChange={(e) =>
854                  setFormData({ ...formData, ram: e.target.value })
855                }
856                placeholder="128"
857                required
858              />
859            </div>
860            <div className="space-y-2">
861              <Label htmlFor="storage">Storage (GB)</Label>
862              <Input
863                id="storage"
864                type="number"
865                value={formData.storage}
866                onChange={(e) =>
867                  setFormData({ ...formData, storage: e.target.value })
868                }
869                placeholder="2000"
870                required
871              />
872            </div>
873          </div>
874
875          <div className="space-y-2">
876            <Label htmlFor="ipAddress">IP Address</Label>
877            <Input
878              id="ipAddress"
879              value={formData.ipAddress}
880              onChange={(e) =>
881                setFormData({ ...formData, ipAddress: e.target.value })
882              }
883              placeholder="192.168.1.100"
884              required
885            />
886          </div>
887
888          <DialogFooter>
889            <Button
890              type="button"
891              variant="outline"
892              onClick={() => onOpenChange(false)}
893            >
894              Cancel
895            </Button>
896            <Button type="submit">Create VPS Parent</Button>
897          </DialogFooter>
898        </form>
899      </DialogContent>
900    </Dialog>
901  );
902};
903
904// Add Dedicated VPS Dialog
905const AddDedicatedVPSDialog = ({
906  open,
907  onOpenChange,
908  onAdd,
909}: {
910  open: boolean;
911  onOpenChange: (open: boolean) => void;
912  onAdd: (
913    vps: Omit<DedicatedVPS, "id" | "usedCpu" | "usedRam" | "usedStorage">
914  ) => void;
915}) => {
916  const [formData, setFormData] = useState({
917    name: "",
918    region: "",
919    cpu: "",
920    ram: "",
921    storage: "",
922    customer: "",
923    domain: "",
924    ipAddress: "",
925  });
926
927  const handleSubmit = (e: React.FormEvent) => {
928    e.preventDefault();
929    onAdd({
930      name: formData.name,
931      region: formData.region,
932      cpu: parseInt(formData.cpu),
933      ram: parseInt(formData.ram),
934      storage: parseInt(formData.storage),
935      status: "running",
936      customer: formData.customer,
937      domain: formData.domain || undefined,
938      ipAddress: formData.ipAddress,
939      createdAt: new Date().toISOString().split("T")[0],
940    });
941    setFormData({
942      name: "",
943      region: "",
944      cpu: "",
945      ram: "",
946      storage: "",
947      customer: "",
948      domain: "",
949      ipAddress: "",
950    });
951    onOpenChange(false);
952  };
953
954  return (
955    <Dialog open={open} onOpenChange={onOpenChange}>
956      <DialogContent className="sm:max-w-[425px]">
957        <DialogHeader>
958          <DialogTitle>Add Dedicated VPS</DialogTitle>
959          <DialogDescription>
960            Create a new dedicated VPS instance for a customer.
961          </DialogDescription>
962        </DialogHeader>
963        <form onSubmit={handleSubmit} className="space-y-4">
964          <div className="space-y-2">
965            <Label htmlFor="name">VPS Name</Label>
966            <Input
967              id="name"
968              value={formData.name}
969              onChange={(e) =>
970                setFormData({ ...formData, name: e.target.value })
971              }
972              placeholder="e.g., Enterprise-Store-1"
973              required
974            />
975          </div>
976
977          <div className="space-y-2">
978            <Label htmlFor="customer">Customer</Label>
979            <Input
980              id="customer"
981              value={formData.customer}
982              onChange={(e) =>
983                setFormData({ ...formData, customer: e.target.value })
984              }
985              placeholder="Customer name"
986              required
987            />
988          </div>
989
990          <div className="space-y-2">
991            <Label htmlFor="region">Region</Label>
992            <Select
993              value={formData.region}
994              onValueChange={(value) =>
995                setFormData({ ...formData, region: value })
996              }
997            >
998              <SelectTrigger>
999                <SelectValue placeholder="Select region" />
1000              </SelectTrigger>
1001              <SelectContent>
1002                {regions.map((region) => (
1003                  <SelectItem key={region} value={region}>
1004                    {region}
1005                  </SelectItem>
1006                ))}
1007              </SelectContent>
1008            </Select>
1009          </div>
1010
1011          <div className="grid grid-cols-3 gap-4">
1012            <div className="space-y-2">
1013              <Label htmlFor="cpu">CPU Cores</Label>
1014              <Input
1015                id="cpu"
1016                type="number"
1017                value={formData.cpu}
1018                onChange={(e) =>
1019                  setFormData({ ...formData, cpu: e.target.value })
1020                }
1021                placeholder="16"
1022                required
1023              />
1024            </div>
1025            <div className="space-y-2">
1026              <Label htmlFor="ram">RAM (GB)</Label>
1027              <Input
1028                id="ram"
1029                type="number"
1030                value={formData.ram}
1031                onChange={(e) =>
1032                  setFormData({ ...formData, ram: e.target.value })
1033                }
1034                placeholder="64"
1035                required
1036              />
1037            </div>
1038            <div className="space-y-2">
1039              <Label htmlFor="storage">Storage (GB)</Label>
1040              <Input
1041                id="storage"
1042                type="number"
1043                value={formData.storage}
1044                onChange={(e) =>
1045                  setFormData({ ...formData, storage: e.target.value })
1046                }
1047                placeholder="1000"
1048                required
1049              />
1050            </div>
1051          </div>
1052
1053          <div className="space-y-2">
1054            <Label htmlFor="domain">Domain (Optional)</Label>
1055            <Input
1056              id="domain"
1057              value={formData.domain}
1058              onChange={(e) =>
1059                setFormData({ ...formData, domain: e.target.value })
1060              }
1061              placeholder="example.com"
1062            />
1063          </div>
1064
1065          <div className="space-y-2">
1066            <Label htmlFor="ipAddress">IP Address</Label>
1067            <Input
1068              id="ipAddress"
1069              value={formData.ipAddress}
1070              onChange={(e) =>
1071                setFormData({ ...formData, ipAddress: e.target.value })
1072              }
1073              placeholder="203.0.113.10"
1074              required
1075            />
1076          </div>
1077
1078          <DialogFooter>
1079            <Button
1080              type="button"
1081              variant="outline"
1082              onClick={() => onOpenChange(false)}
1083            >
1084              Cancel
1085            </Button>
1086            <Button type="submit">Create Dedicated VPS</Button>
1087          </DialogFooter>
1088        </form>
1089      </DialogContent>
1090    </Dialog>
1091  );
1092};
1093
1094// Main Admin UI Component
1095const VPSAdminUI = () => {
1096  const [vpsParents, setVpsParents] = useState<VPSParent[]>(sampleVPSParents);
1097  const [dedicatedVPS, setDedicatedVPS] =
1098    useState<DedicatedVPS[]>(sampleDedicatedVPS);
1099  const [searchTerm, setSearchTerm] = useState("");
1100  const [activeTab, setActiveTab] = useState("parents");
1101  const [showAddParentDialog, setShowAddParentDialog] = useState(false);
1102  const [showAddDedicatedDialog, setShowAddDedicatedDialog] = useState(false);
1103
1104  const handleAddVPSParent = (
1105    parentData: Omit<
1106      VPSParent,
1107      | "id"
1108      | "usedCpu"
1109      | "usedRam"
1110      | "usedStorage"
1111      | "sharedVPS"
1112      | "dedicatedVPS"
1113    >
1114  ) => {
1115    const newParent: VPSParent = {
1116      ...parentData,
1117      id: `parent-${Date.now()}`,
1118      usedCpu: 0,
1119      usedRam: 0,
1120      usedStorage: 0,
1121      sharedVPS: [],
1122      dedicatedVPS: [],
1123    };
1124    setVpsParents([...vpsParents, newParent]);
1125  };
1126
1127  const handleAddDedicatedVPS = (
1128    vpsData: Omit<DedicatedVPS, "id" | "usedCpu" | "usedRam" | "usedStorage">
1129  ) => {
1130    const newVPS: DedicatedVPS = {
1131      ...vpsData,
1132      id: `dedicated-${Date.now()}`,
1133      usedCpu: 0,
1134      usedRam: 0,
1135      usedStorage: 0,
1136    };
1137    setDedicatedVPS([...dedicatedVPS, newVPS]);
1138  };
1139
1140  const filteredParents = vpsParents.filter(
1141    (parent) =>
1142      parent.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
1143      parent.region.toLowerCase().includes(searchTerm.toLowerCase())
1144  );
1145
1146  const filteredDedicated = dedicatedVPS.filter(
1147    (vps) =>
1148      vps.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
1149      vps.customer.toLowerCase().includes(searchTerm.toLowerCase()) ||
1150      vps.region.toLowerCase().includes(searchTerm.toLowerCase())
1151  );
1152
1153  const totalStats = {
1154    parents: vpsParents.length,
1155    dedicated: dedicatedVPS.length,
1156    shared: vpsParents.reduce(
1157      (acc, parent) => acc + parent.sharedVPS.length,
1158      0
1159    ),
1160    totalCpu:
1161      vpsParents.reduce((acc, parent) => acc + parent.cpu, 0) +
1162      dedicatedVPS.reduce((acc, vps) => acc + vps.cpu, 0),
1163    totalRam:
1164      vpsParents.reduce((acc, parent) => acc + parent.ram, 0) +
1165      dedicatedVPS.reduce((acc, vps) => acc + vps.ram, 0),
1166    totalStorage:
1167      vpsParents.reduce((acc, parent) => acc + parent.storage, 0) +
1168      dedicatedVPS.reduce((acc, vps) => acc + vps.storage, 0),
1169  };
1170
1171  return (
1172    <div className="min-h-screen bg-background p-6">
1173      <div className="max-w-7xl mx-auto space-y-6">
1174        {/* Header */}
1175        <div className="flex items-center justify-between">
1176          <div>
1177            <h1 className="text-3xl font-bold tracking-tight">
1178              VPS Management
1179            </h1>
1180            <p className="text-muted-foreground">
1181              Manage and monitor your VPS infrastructure for webstore
1182              deployments
1183            </p>
1184          </div>
1185          <div className="flex items-center gap-3">
1186            <Button variant="outline" size="sm">
1187              <Settings className="w-4 h-4 mr-2" />
1188              Settings
1189            </Button>
1190          </div>
1191        </div>
1192
1193        {/* Stats Overview */}
1194        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-4">
1195          <Card>
1196            <CardContent className="p-4">
1197              <div className="flex items-center gap-2">
1198                <Server className="w-4 h-4 text-primary" />
1199                <div>
1200                  <p className="text-sm text-muted-foreground">VPS Parents</p>
1201                  <p className="text-2xl font-bold">{totalStats.parents}</p>
1202                </div>
1203              </div>
1204            </CardContent>
1205          </Card>
1206
1207          <Card>
1208            <CardContent className="p-4">
1209              <div className="flex items-center gap-2">
1210                <Shield className="w-4 h-4 text-blue-500" />
1211                <div>
1212                  <p className="text-sm text-muted-foreground">Dedicated VPS</p>
1213                  <p className="text-2xl font-bold">{totalStats.dedicated}</p>
1214                </div>
1215              </div>
1216            </CardContent>
1217          </Card>
1218
1219          <Card>
1220            <CardContent className="p-4">
1221              <div className="flex items-center gap-2">
1222                <Users className="w-4 h-4 text-green-500" />
1223                <div>
1224                  <p className="text-sm text-muted-foreground">Shared VPS</p>
1225                  <p className="text-2xl font-bold">{totalStats.shared}</p>
1226                </div>
1227              </div>
1228            </CardContent>
1229          </Card>
1230
1231          <Card>
1232            <CardContent className="p-4">
1233              <div className="flex items-center gap-2">
1234                <Cpu className="w-4 h-4 text-orange-500" />
1235                <div>
1236                  <p className="text-sm text-muted-foreground">Total CPU</p>
1237                  <p className="text-2xl font-bold">{totalStats.totalCpu}</p>
1238                </div>
1239              </div>
1240            </CardContent>
1241          </Card>
1242
1243          <Card>
1244            <CardContent className="p-4">
1245              <div className="flex items-center gap-2">
1246                <MemoryStick className="w-4 h-4 text-purple-500" />
1247                <div>
1248                  <p className="text-sm text-muted-foreground">Total RAM</p>
1249                  <p className="text-2xl font-bold">{totalStats.totalRam}GB</p>
1250                </div>
1251              </div>
1252            </CardContent>
1253          </Card>
1254
1255          <Card>
1256            <CardContent className="p-4">
1257              <div className="flex items-center gap-2">
1258                <HardDrive className="w-4 h-4 text-red-500" />
1259                <div>
1260                  <p className="text-sm text-muted-foreground">Total Storage</p>
1261                  <p className="text-2xl font-bold">
1262                    {totalStats.totalStorage}GB
1263                  </p>
1264                </div>
1265              </div>
1266            </CardContent>
1267          </Card>
1268        </div>
1269
1270        {/* Controls */}
1271        <div className="flex items-center justify-between">
1272          <div className="flex items-center gap-3">
1273            <div className="relative">
1274              <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
1275              <Input
1276                placeholder="Search VPS instances..."
1277                value={searchTerm}
1278                onChange={(e) => setSearchTerm(e.target.value)}
1279                className="pl-10 w-80"
1280              />
1281            </div>
1282            <Button variant="outline" size="sm">
1283              <Filter className="w-4 h-4 mr-2" />
1284              Filter
1285            </Button>
1286          </div>
1287        </div>
1288
1289        {/* Main Content */}
1290        <Tabs value={activeTab} onValueChange={setActiveTab}>
1291          <div className="flex items-center justify-between">
1292            <TabsList>
1293              <TabsTrigger value="parents" className="flex items-center gap-2">
1294                <Server className="w-4 h-4" />
1295                VPS Parents
1296              </TabsTrigger>
1297              <TabsTrigger
1298                value="dedicated"
1299                className="flex items-center gap-2"
1300              >
1301                <Shield className="w-4 h-4" />
1302                Dedicated VPS
1303              </TabsTrigger>
1304              <TabsTrigger value="shared" className="flex items-center gap-2">
1305                <Users className="w-4 h-4" />
1306                Shared VPS
1307              </TabsTrigger>
1308            </TabsList>
1309
1310            <div className="flex gap-2">
1311              {activeTab === "parents" && (
1312                <Button onClick={() => setShowAddParentDialog(true)}>
1313                  <Plus className="w-4 h-4 mr-2" />
1314                  Add VPS Parent
1315                </Button>
1316              )}
1317              {activeTab === "dedicated" && (
1318                <Button onClick={() => setShowAddDedicatedDialog(true)}>
1319                  <Plus className="w-4 h-4 mr-2" />
1320                  Add Dedicated VPS
1321                </Button>
1322              )}
1323            </div>
1324          </div>
1325
1326          <TabsContent value="parents" className="space-y-4">
1327            <div className="grid gap-4">
1328              {filteredParents.map((parent) => (
1329                <VPSParentCard
1330                  key={parent.id}
1331                  parent={parent}
1332                  onEdit={() => {}}
1333                  onDelete={() => {}}
1334                />
1335              ))}
1336            </div>
1337          </TabsContent>
1338
1339          <TabsContent value="dedicated" className="space-y-4">
1340            <div className="grid gap-4">
1341              {filteredDedicated.map((vps) => (
1342                <DedicatedVPSCard
1343                  key={vps.id}
1344                  vps={vps}
1345                  onEdit={() => {}}
1346                  onDelete={() => {}}
1347                />
1348              ))}
1349            </div>
1350          </TabsContent>
1351
1352          <TabsContent value="shared" className="space-y-4">
1353            <div className="grid gap-4">
1354              {vpsParents.map((parent) =>
1355                parent.sharedVPS.map((vps) => (
1356                  <Card key={vps.id}>
1357                    <CardHeader>
1358                      <div className="flex items-center justify-between">
1359                        <div className="flex items-center gap-3">
1360                          <div className="p-2 bg-green-500/10 rounded-lg">
1361                            <Monitor className="w-5 h-5 text-green-500" />
1362                          </div>
1363                          <div>
1364                            <CardTitle className="text-lg">
1365                              {vps.name}
1366                            </CardTitle>
1367                            <CardDescription>
1368                              Parent: {parent.name} • Customer: {vps.customer}
1369                            </CardDescription>
1370                          </div>
1371                        </div>
1372                        <StatusBadge status={vps.status} />
1373                      </div>
1374                    </CardHeader>
1375                    <CardContent>
1376                      <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
1377                        <ResourceUsage
1378                          used={vps.usedCpu}
1379                          total={vps.cpu}
1380                          label="CPU"
1381                          unit=" cores"
1382                        />
1383                        <ResourceUsage
1384                          used={vps.usedRam}
1385                          total={vps.ram}
1386                          label="RAM"
1387                          unit=" GB"
1388                        />
1389                        <ResourceUsage
1390                          used={vps.usedStorage}
1391                          total={vps.storage}
1392                          label="Storage"
1393                          unit=" GB"
1394                        />
1395                      </div>
1396                    </CardContent>
1397                  </Card>
1398                ))
1399              )}
1400            </div>
1401          </TabsContent>
1402        </Tabs>
1403
1404        {/* Dialogs */}
1405        <AddVPSParentDialog
1406          open={showAddParentDialog}
1407          onOpenChange={setShowAddParentDialog}
1408          onAdd={handleAddVPSParent}
1409        />
1410
1411        <AddDedicatedVPSDialog
1412          open={showAddDedicatedDialog}
1413          onOpenChange={setShowAddDedicatedDialog}
1414          onAdd={handleAddDedicatedVPS}
1415        />
1416      </div>
1417    </div>
1418  );
1419};
1420
1421export default VPSAdminUI;

Dependencies

External Libraries

framer-motionlucide-reactreactrecharts

Shadcn/UI Components

badgebuttoncardchartdialogdropdown-menuinputlabelprogressselectseparatortabs

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.