ShadCN Vaults

Back to Blocks

VPS Instance Management UI

Unknown Block

UnknownComponent

VPS Instance Management UI

Interactive UI for managing VPS instances, including add/edit dialogs, resource and customer details, and advanced filtering and search.

Preview

Full width desktop view

Code

monitoring-13.tsx
1"use client";
2
3import * as React from "react";
4import { useState } from "react";
5import { cn } from "@/lib/utils";
6import {
7  Plus,
8  Server,
9  HardDrive,
10  Cpu,
11  MemoryStick,
12  Globe,
13  Activity,
14  Edit,
15  Trash2,
16  Eye,
17  Search,
18  Users,
19  Database,
20  Network,
21} from "lucide-react";
22import { Button } from "@/components/ui/button";
23import { Input } from "@/components/ui/input";
24import { Label } from "@/components/ui/label";
25import { Card } from "@/components/ui/card";
26import { Badge } from "@/components/ui/badge";
27import {
28  Dialog,
29  DialogContent,
30  DialogHeader,
31  DialogTitle,
32  DialogTrigger,
33  DialogFooter,
34} from "@/components/ui/dialog";
35
36import {
37  Select,
38  SelectContent,
39  SelectItem,
40  SelectTrigger,
41  SelectValue,
42} from "@/components/ui/select";
43
44interface VPSInstance {
45  id: string;
46  name: string;
47  type: "shared" | "dedicated";
48  status: "running" | "stopped" | "maintenance" | "error";
49  cpu: string;
50  ram: string;
51  storage: string;
52  bandwidth: string;
53  ipAddress: string;
54  location: string;
55  os: string;
56  customer: string;
57  createdAt: string;
58  monthlyPrice: number;
59  webstoreUrl?: string;
60}
61
62interface VPSFormData {
63  name: string;
64  type: "shared" | "dedicated";
65  cpu: string;
66  ram: string;
67  storage: string;
68  bandwidth: string;
69  location: string;
70  os: string;
71  customer: string;
72  monthlyPrice: string;
73  webstoreUrl: string;
74}
75
76const VPSAdminUI = () => {
77  const [instances, setInstances] = useState<VPSInstance[]>([
78    {
79      id: "1",
80      name: "WebStore-Pro-001",
81      type: "dedicated",
82      status: "running",
83      cpu: "4 vCPU",
84      ram: "8 GB",
85      storage: "200 GB SSD",
86      bandwidth: "10 TB",
87      ipAddress: "192.168.1.100",
88      location: "US East",
89      os: "Ubuntu 22.04",
90      customer: "Acme Corp",
91      createdAt: "2024-01-15",
92      monthlyPrice: 89.99,
93      webstoreUrl: "https://acme-store.com",
94    },
95    {
96      id: "2",
97      name: "Shared-Store-002",
98      type: "shared",
99      status: "running",
100      cpu: "2 vCPU",
101      ram: "4 GB",
102      storage: "50 GB SSD",
103      bandwidth: "2 TB",
104      ipAddress: "192.168.1.101",
105      location: "EU West",
106      os: "CentOS 8",
107      customer: "TechStart Ltd",
108      createdAt: "2024-01-20",
109      monthlyPrice: 29.99,
110    },
111  ]);
112
113  const [searchTerm, setSearchTerm] = useState("");
114  const [filterType, setFilterType] = useState<"all" | "shared" | "dedicated">(
115    "all"
116  );
117  const [filterStatus, setFilterStatus] = useState<
118    "all" | "running" | "stopped" | "maintenance" | "error"
119  >("all");
120  const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
121  const [editingInstance, setEditingInstance] = useState<VPSInstance | null>(
122    null
123  );
124  const [formData, setFormData] = useState<VPSFormData>({
125    name: "",
126    type: "shared",
127    cpu: "",
128    ram: "",
129    storage: "",
130    bandwidth: "",
131    location: "",
132    os: "",
133    customer: "",
134    monthlyPrice: "",
135    webstoreUrl: "",
136  });
137
138  const resetForm = () => {
139    setFormData({
140      name: "",
141      type: "shared",
142      cpu: "",
143      ram: "",
144      storage: "",
145      bandwidth: "",
146      location: "",
147      os: "",
148      customer: "",
149      monthlyPrice: "",
150      webstoreUrl: "",
151    });
152  };
153
154  const handleAddInstance = () => {
155    const newInstance: VPSInstance = {
156      id: Date.now().toString(),
157      name: formData.name,
158      type: formData.type,
159      status: "running",
160      cpu: formData.cpu,
161      ram: formData.ram,
162      storage: formData.storage,
163      bandwidth: formData.bandwidth,
164      ipAddress: `192.168.1.${Math.floor(Math.random() * 255)}`,
165      location: formData.location,
166      os: formData.os,
167      customer: formData.customer,
168      createdAt: new Date().toISOString().split("T")[0],
169      monthlyPrice: parseFloat(formData.monthlyPrice),
170      webstoreUrl: formData.webstoreUrl || undefined,
171    };
172
173    setInstances([...instances, newInstance]);
174    setIsAddDialogOpen(false);
175    resetForm();
176  };
177
178  const handleEditInstance = () => {
179    if (!editingInstance) return;
180
181    const updatedInstances = instances.map((instance) =>
182      instance.id === editingInstance.id
183        ? {
184            ...instance,
185            name: formData.name,
186            type: formData.type,
187            cpu: formData.cpu,
188            ram: formData.ram,
189            storage: formData.storage,
190            bandwidth: formData.bandwidth,
191            location: formData.location,
192            os: formData.os,
193            customer: formData.customer,
194            monthlyPrice: parseFloat(formData.monthlyPrice),
195            webstoreUrl: formData.webstoreUrl || undefined,
196          }
197        : instance
198    );
199
200    setInstances(updatedInstances);
201    setEditingInstance(null);
202    resetForm();
203  };
204
205  const handleDeleteInstance = (id: string) => {
206    setInstances(instances.filter((instance) => instance.id !== id));
207  };
208
209  const openEditDialog = (instance: VPSInstance) => {
210    setEditingInstance(instance);
211    setFormData({
212      name: instance.name,
213      type: instance.type,
214      cpu: instance.cpu,
215      ram: instance.ram,
216      storage: instance.storage,
217      bandwidth: instance.bandwidth,
218      location: instance.location,
219      os: instance.os,
220      customer: instance.customer,
221      monthlyPrice: instance.monthlyPrice.toString(),
222      webstoreUrl: instance.webstoreUrl || "",
223    });
224  };
225
226  const getStatusColor = (status: string) => {
227    switch (status) {
228      case "running":
229        return "bg-green-500";
230      case "stopped":
231        return "bg-red-500";
232      case "maintenance":
233        return "bg-yellow-500";
234      case "error":
235        return "bg-red-600";
236      default:
237        return "bg-gray-500";
238    }
239  };
240
241  const getStatusBadgeVariant = (status: string) => {
242    switch (status) {
243      case "running":
244        return "default";
245      case "stopped":
246        return "destructive";
247      case "maintenance":
248        return "secondary";
249      case "error":
250        return "destructive";
251      default:
252        return "secondary";
253    }
254  };
255
256  const filteredInstances = instances.filter((instance) => {
257    const matchesSearch =
258      instance.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
259      instance.customer.toLowerCase().includes(searchTerm.toLowerCase());
260    const matchesType = filterType === "all" || instance.type === filterType;
261    const matchesStatus =
262      filterStatus === "all" || instance.status === filterStatus;
263
264    return matchesSearch && matchesType && matchesStatus;
265  });
266
267  const stats = {
268    total: instances.length,
269    running: instances.filter((i) => i.status === "running").length,
270    shared: instances.filter((i) => i.type === "shared").length,
271    dedicated: instances.filter((i) => i.type === "dedicated").length,
272  };
273
274  return (
275    <div className="min-h-screen bg-background p-6">
276      <div className="max-w-7xl mx-auto space-y-6">
277        {/* Header */}
278        <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
279          <div>
280            <h1 className="text-3xl font-bold text-foreground">
281              VPS Instance Management
282            </h1>
283            <p className="text-muted-foreground">
284              Manage and monitor VPS instances for webstore deployments
285            </p>
286          </div>
287
288          <Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
289            <DialogTrigger asChild>
290              <Button className="flex items-center gap-2">
291                <Plus size={16} />
292                Add New VPS
293              </Button>
294            </DialogTrigger>
295            <DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
296              <DialogHeader>
297                <DialogTitle>Add New VPS Instance</DialogTitle>
298              </DialogHeader>
299
300              <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
301                <div className="space-y-2">
302                  <Label htmlFor="name">Instance Name</Label>
303                  <Input
304                    id="name"
305                    value={formData.name}
306                    onChange={(e) =>
307                      setFormData({ ...formData, name: e.target.value })
308                    }
309                    placeholder="e.g., WebStore-Pro-001"
310                  />
311                </div>
312
313                <div className="space-y-2">
314                  <Label htmlFor="type">VPS Type</Label>
315                  <Select
316                    value={formData.type}
317                    onValueChange={(value: "shared" | "dedicated") =>
318                      setFormData({ ...formData, type: value })
319                    }
320                  >
321                    <SelectTrigger>
322                      <SelectValue />
323                    </SelectTrigger>
324                    <SelectContent>
325                      <SelectItem value="shared">Shared VPS</SelectItem>
326                      <SelectItem value="dedicated">Dedicated VPS</SelectItem>
327                    </SelectContent>
328                  </Select>
329                </div>
330
331                <div className="space-y-2">
332                  <Label htmlFor="cpu">CPU</Label>
333                  <Input
334                    id="cpu"
335                    value={formData.cpu}
336                    onChange={(e) =>
337                      setFormData({ ...formData, cpu: e.target.value })
338                    }
339                    placeholder="e.g., 4 vCPU"
340                  />
341                </div>
342
343                <div className="space-y-2">
344                  <Label htmlFor="ram">RAM</Label>
345                  <Input
346                    id="ram"
347                    value={formData.ram}
348                    onChange={(e) =>
349                      setFormData({ ...formData, ram: e.target.value })
350                    }
351                    placeholder="e.g., 8 GB"
352                  />
353                </div>
354
355                <div className="space-y-2">
356                  <Label htmlFor="storage">Storage</Label>
357                  <Input
358                    id="storage"
359                    value={formData.storage}
360                    onChange={(e) =>
361                      setFormData({ ...formData, storage: e.target.value })
362                    }
363                    placeholder="e.g., 200 GB SSD"
364                  />
365                </div>
366
367                <div className="space-y-2">
368                  <Label htmlFor="bandwidth">Bandwidth</Label>
369                  <Input
370                    id="bandwidth"
371                    value={formData.bandwidth}
372                    onChange={(e) =>
373                      setFormData({ ...formData, bandwidth: e.target.value })
374                    }
375                    placeholder="e.g., 10 TB"
376                  />
377                </div>
378
379                <div className="space-y-2">
380                  <Label htmlFor="location">Location</Label>
381                  <Select
382                    value={formData.location}
383                    onValueChange={(value) =>
384                      setFormData({ ...formData, location: value })
385                    }
386                  >
387                    <SelectTrigger>
388                      <SelectValue placeholder="Select location" />
389                    </SelectTrigger>
390                    <SelectContent>
391                      <SelectItem value="US East">US East</SelectItem>
392                      <SelectItem value="US West">US West</SelectItem>
393                      <SelectItem value="EU West">EU West</SelectItem>
394                      <SelectItem value="EU Central">EU Central</SelectItem>
395                      <SelectItem value="Asia Pacific">Asia Pacific</SelectItem>
396                    </SelectContent>
397                  </Select>
398                </div>
399
400                <div className="space-y-2">
401                  <Label htmlFor="os">Operating System</Label>
402                  <Select
403                    value={formData.os}
404                    onValueChange={(value) =>
405                      setFormData({ ...formData, os: value })
406                    }
407                  >
408                    <SelectTrigger>
409                      <SelectValue placeholder="Select OS" />
410                    </SelectTrigger>
411                    <SelectContent>
412                      <SelectItem value="Ubuntu 22.04">Ubuntu 22.04</SelectItem>
413                      <SelectItem value="Ubuntu 20.04">Ubuntu 20.04</SelectItem>
414                      <SelectItem value="CentOS 8">CentOS 8</SelectItem>
415                      <SelectItem value="Debian 11">Debian 11</SelectItem>
416                      <SelectItem value="Windows Server 2022">
417                        Windows Server 2022
418                      </SelectItem>
419                    </SelectContent>
420                  </Select>
421                </div>
422
423                <div className="space-y-2">
424                  <Label htmlFor="customer">Customer</Label>
425                  <Input
426                    id="customer"
427                    value={formData.customer}
428                    onChange={(e) =>
429                      setFormData({ ...formData, customer: e.target.value })
430                    }
431                    placeholder="Customer name"
432                  />
433                </div>
434
435                <div className="space-y-2">
436                  <Label htmlFor="price">Monthly Price ($)</Label>
437                  <Input
438                    id="price"
439                    type="number"
440                    step="0.01"
441                    value={formData.monthlyPrice}
442                    onChange={(e) =>
443                      setFormData({ ...formData, monthlyPrice: e.target.value })
444                    }
445                    placeholder="29.99"
446                  />
447                </div>
448
449                <div className="space-y-2 md:col-span-2">
450                  <Label htmlFor="webstore">Webstore URL (Optional)</Label>
451                  <Input
452                    id="webstore"
453                    value={formData.webstoreUrl}
454                    onChange={(e) =>
455                      setFormData({ ...formData, webstoreUrl: e.target.value })
456                    }
457                    placeholder="https://example-store.com"
458                  />
459                </div>
460              </div>
461
462              <DialogFooter>
463                <Button
464                  variant="outline"
465                  onClick={() => setIsAddDialogOpen(false)}
466                >
467                  Cancel
468                </Button>
469                <Button onClick={handleAddInstance}>Create VPS Instance</Button>
470              </DialogFooter>
471            </DialogContent>
472          </Dialog>
473        </div>
474
475        {/* Stats Cards */}
476        <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
477          <Card className="p-6">
478            <div className="flex items-center justify-between">
479              <div>
480                <p className="text-sm font-medium text-muted-foreground">
481                  Total Instances
482                </p>
483                <p className="text-2xl font-bold text-foreground">
484                  {stats.total}
485                </p>
486              </div>
487              <Server className="h-8 w-8 text-blue-500" />
488            </div>
489          </Card>
490
491          <Card className="p-6">
492            <div className="flex items-center justify-between">
493              <div>
494                <p className="text-sm font-medium text-muted-foreground">
495                  Running
496                </p>
497                <p className="text-2xl font-bold text-green-600">
498                  {stats.running}
499                </p>
500              </div>
501              <Activity className="h-8 w-8 text-green-500" />
502            </div>
503          </Card>
504
505          <Card className="p-6">
506            <div className="flex items-center justify-between">
507              <div>
508                <p className="text-sm font-medium text-muted-foreground">
509                  Shared VPS
510                </p>
511                <p className="text-2xl font-bold text-foreground">
512                  {stats.shared}
513                </p>
514              </div>
515              <Users className="h-8 w-8 text-orange-500" />
516            </div>
517          </Card>
518
519          <Card className="p-6">
520            <div className="flex items-center justify-between">
521              <div>
522                <p className="text-sm font-medium text-muted-foreground">
523                  Dedicated VPS
524                </p>
525                <p className="text-2xl font-bold text-foreground">
526                  {stats.dedicated}
527                </p>
528              </div>
529              <Database className="h-8 w-8 text-purple-500" />
530            </div>
531          </Card>
532        </div>
533
534        {/* Filters and Search */}
535        <Card className="p-6">
536          <div className="flex flex-col sm:flex-row gap-4">
537            <div className="flex-1">
538              <div className="relative">
539                <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
540                <Input
541                  placeholder="Search instances or customers..."
542                  value={searchTerm}
543                  onChange={(e) => setSearchTerm(e.target.value)}
544                  className="pl-10"
545                />
546              </div>
547            </div>
548
549            <Select
550              value={filterType}
551              onValueChange={(value: any) => setFilterType(value)}
552            >
553              <SelectTrigger className="w-full sm:w-[180px]">
554                <SelectValue />
555              </SelectTrigger>
556              <SelectContent>
557                <SelectItem value="all">All Types</SelectItem>
558                <SelectItem value="shared">Shared VPS</SelectItem>
559                <SelectItem value="dedicated">Dedicated VPS</SelectItem>
560              </SelectContent>
561            </Select>
562
563            <Select
564              value={filterStatus}
565              onValueChange={(value: any) => setFilterStatus(value)}
566            >
567              <SelectTrigger className="w-full sm:w-[180px]">
568                <SelectValue />
569              </SelectTrigger>
570              <SelectContent>
571                <SelectItem value="all">All Status</SelectItem>
572                <SelectItem value="running">Running</SelectItem>
573                <SelectItem value="stopped">Stopped</SelectItem>
574                <SelectItem value="maintenance">Maintenance</SelectItem>
575                <SelectItem value="error">Error</SelectItem>
576              </SelectContent>
577            </Select>
578          </div>
579        </Card>
580
581        {/* Instances List */}
582        <div className="space-y-4">
583          {filteredInstances.length === 0 ? (
584            <Card className="p-12 text-center">
585              <Server className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
586              <h3 className="text-lg font-semibold text-foreground mb-2">
587                No VPS instances found
588              </h3>
589              <p className="text-muted-foreground">
590                Create your first VPS instance to get started.
591              </p>
592            </Card>
593          ) : (
594            filteredInstances.map((instance) => (
595              <Card key={instance.id} className="p-6">
596                <div className="flex flex-col lg:flex-row lg:items-center justify-between gap-4">
597                  <div className="flex-1 space-y-4">
598                    <div className="flex items-center gap-3">
599                      <div
600                        className={cn(
601                          "w-3 h-3 rounded-full",
602                          getStatusColor(instance.status)
603                        )}
604                      />
605                      <h3 className="text-lg font-semibold text-foreground">
606                        {instance.name}
607                      </h3>
608                      <Badge
609                        variant={
610                          instance.type === "dedicated"
611                            ? "default"
612                            : "secondary"
613                        }
614                      >
615                        {instance.type === "dedicated" ? "Dedicated" : "Shared"}
616                      </Badge>
617                      <Badge variant={getStatusBadgeVariant(instance.status)}>
618                        {instance.status}
619                      </Badge>
620                    </div>
621
622                    <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 text-sm">
623                      <div className="flex items-center gap-2">
624                        <Cpu className="h-4 w-4 text-muted-foreground" />
625                        <span className="text-muted-foreground">CPU:</span>
626                        <span className="font-medium">{instance.cpu}</span>
627                      </div>
628                      <div className="flex items-center gap-2">
629                        <MemoryStick className="h-4 w-4 text-muted-foreground" />
630                        <span className="text-muted-foreground">RAM:</span>
631                        <span className="font-medium">{instance.ram}</span>
632                      </div>
633                      <div className="flex items-center gap-2">
634                        <HardDrive className="h-4 w-4 text-muted-foreground" />
635                        <span className="text-muted-foreground">Storage:</span>
636                        <span className="font-medium">{instance.storage}</span>
637                      </div>
638                      <div className="flex items-center gap-2">
639                        <Network className="h-4 w-4 text-muted-foreground" />
640                        <span className="text-muted-foreground">
641                          Bandwidth:
642                        </span>
643                        <span className="font-medium">
644                          {instance.bandwidth}
645                        </span>
646                      </div>
647                    </div>
648
649                    <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 text-sm">
650                      <div>
651                        <span className="text-muted-foreground">Customer:</span>
652                        <span className="font-medium ml-2">
653                          {instance.customer}
654                        </span>
655                      </div>
656                      <div>
657                        <span className="text-muted-foreground">Location:</span>
658                        <span className="font-medium ml-2">
659                          {instance.location}
660                        </span>
661                      </div>
662                      <div>
663                        <span className="text-muted-foreground">IP:</span>
664                        <span className="font-medium ml-2">
665                          {instance.ipAddress}
666                        </span>
667                      </div>
668                      <div>
669                        <span className="text-muted-foreground">OS:</span>
670                        <span className="font-medium ml-2">{instance.os}</span>
671                      </div>
672                      <div>
673                        <span className="text-muted-foreground">Created:</span>
674                        <span className="font-medium ml-2">
675                          {instance.createdAt}
676                        </span>
677                      </div>
678                      <div>
679                        <span className="text-muted-foreground">Price:</span>
680                        <span className="font-medium ml-2">
681                          ${instance.monthlyPrice}/mo
682                        </span>
683                      </div>
684                    </div>
685
686                    {instance.webstoreUrl && (
687                      <div className="flex items-center gap-2 text-sm">
688                        <Globe className="h-4 w-4 text-muted-foreground" />
689                        <span className="text-muted-foreground">Webstore:</span>
690                        <a
691                          href={instance.webstoreUrl}
692                          target="_blank"
693                          rel="noopener noreferrer"
694                          className="text-blue-600 hover:text-blue-800 underline"
695                        >
696                          {instance.webstoreUrl}
697                        </a>
698                      </div>
699                    )}
700                  </div>
701
702                  <div className="flex items-center gap-2">
703                    <Button variant="outline" size="sm">
704                      <Eye className="h-4 w-4" />
705                    </Button>
706
707                    <Dialog
708                      open={editingInstance?.id === instance.id}
709                      onOpenChange={(open) => !open && setEditingInstance(null)}
710                    >
711                      <DialogTrigger asChild>
712                        <Button
713                          variant="outline"
714                          size="sm"
715                          onClick={() => openEditDialog(instance)}
716                        >
717                          <Edit className="h-4 w-4" />
718                        </Button>
719                      </DialogTrigger>
720                      <DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
721                        <DialogHeader>
722                          <DialogTitle>Edit VPS Instance</DialogTitle>
723                        </DialogHeader>
724
725                        <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
726                          <div className="space-y-2">
727                            <Label htmlFor="edit-name">Instance Name</Label>
728                            <Input
729                              id="edit-name"
730                              value={formData.name}
731                              onChange={(e) =>
732                                setFormData({
733                                  ...formData,
734                                  name: e.target.value,
735                                })
736                              }
737                            />
738                          </div>
739
740                          <div className="space-y-2">
741                            <Label htmlFor="edit-type">VPS Type</Label>
742                            <Select
743                              value={formData.type}
744                              onValueChange={(value: "shared" | "dedicated") =>
745                                setFormData({ ...formData, type: value })
746                              }
747                            >
748                              <SelectTrigger>
749                                <SelectValue />
750                              </SelectTrigger>
751                              <SelectContent>
752                                <SelectItem value="shared">
753                                  Shared VPS
754                                </SelectItem>
755                                <SelectItem value="dedicated">
756                                  Dedicated VPS
757                                </SelectItem>
758                              </SelectContent>
759                            </Select>
760                          </div>
761
762                          <div className="space-y-2">
763                            <Label htmlFor="edit-cpu">CPU</Label>
764                            <Input
765                              id="edit-cpu"
766                              value={formData.cpu}
767                              onChange={(e) =>
768                                setFormData({
769                                  ...formData,
770                                  cpu: e.target.value,
771                                })
772                              }
773                            />
774                          </div>
775
776                          <div className="space-y-2">
777                            <Label htmlFor="edit-ram">RAM</Label>
778                            <Input
779                              id="edit-ram"
780                              value={formData.ram}
781                              onChange={(e) =>
782                                setFormData({
783                                  ...formData,
784                                  ram: e.target.value,
785                                })
786                              }
787                            />
788                          </div>
789
790                          <div className="space-y-2">
791                            <Label htmlFor="edit-storage">Storage</Label>
792                            <Input
793                              id="edit-storage"
794                              value={formData.storage}
795                              onChange={(e) =>
796                                setFormData({
797                                  ...formData,
798                                  storage: e.target.value,
799                                })
800                              }
801                            />
802                          </div>
803
804                          <div className="space-y-2">
805                            <Label htmlFor="edit-bandwidth">Bandwidth</Label>
806                            <Input
807                              id="edit-bandwidth"
808                              value={formData.bandwidth}
809                              onChange={(e) =>
810                                setFormData({
811                                  ...formData,
812                                  bandwidth: e.target.value,
813                                })
814                              }
815                            />
816                          </div>
817
818                          <div className="space-y-2">
819                            <Label htmlFor="edit-location">Location</Label>
820                            <Select
821                              value={formData.location}
822                              onValueChange={(value) =>
823                                setFormData({ ...formData, location: value })
824                              }
825                            >
826                              <SelectTrigger>
827                                <SelectValue />
828                              </SelectTrigger>
829                              <SelectContent>
830                                <SelectItem value="US East">US East</SelectItem>
831                                <SelectItem value="US West">US West</SelectItem>
832                                <SelectItem value="EU West">EU West</SelectItem>
833                                <SelectItem value="EU Central">
834                                  EU Central
835                                </SelectItem>
836                                <SelectItem value="Asia Pacific">
837                                  Asia Pacific
838                                </SelectItem>
839                              </SelectContent>
840                            </Select>
841                          </div>
842
843                          <div className="space-y-2">
844                            <Label htmlFor="edit-os">Operating System</Label>
845                            <Select
846                              value={formData.os}
847                              onValueChange={(value) =>
848                                setFormData({ ...formData, os: value })
849                              }
850                            >
851                              <SelectTrigger>
852                                <SelectValue />
853                              </SelectTrigger>
854                              <SelectContent>
855                                <SelectItem value="Ubuntu 22.04">
856                                  Ubuntu 22.04
857                                </SelectItem>
858                                <SelectItem value="Ubuntu 20.04">
859                                  Ubuntu 20.04
860                                </SelectItem>
861                                <SelectItem value="CentOS 8">
862                                  CentOS 8
863                                </SelectItem>
864                                <SelectItem value="Debian 11">
865                                  Debian 11
866                                </SelectItem>
867                                <SelectItem value="Windows Server 2022">
868                                  Windows Server 2022
869                                </SelectItem>
870                              </SelectContent>
871                            </Select>
872                          </div>
873
874                          <div className="space-y-2">
875                            <Label htmlFor="edit-customer">Customer</Label>
876                            <Input
877                              id="edit-customer"
878                              value={formData.customer}
879                              onChange={(e) =>
880                                setFormData({
881                                  ...formData,
882                                  customer: e.target.value,
883                                })
884                              }
885                            />
886                          </div>
887
888                          <div className="space-y-2">
889                            <Label htmlFor="edit-price">
890                              Monthly Price ($)
891                            </Label>
892                            <Input
893                              id="edit-price"
894                              type="number"
895                              step="0.01"
896                              value={formData.monthlyPrice}
897                              onChange={(e) =>
898                                setFormData({
899                                  ...formData,
900                                  monthlyPrice: e.target.value,
901                                })
902                              }
903                            />
904                          </div>
905
906                          <div className="space-y-2 md:col-span-2">
907                            <Label htmlFor="edit-webstore">
908                              Webstore URL (Optional)
909                            </Label>
910                            <Input
911                              id="edit-webstore"
912                              value={formData.webstoreUrl}
913                              onChange={(e) =>
914                                setFormData({
915                                  ...formData,
916                                  webstoreUrl: e.target.value,
917                                })
918                              }
919                            />
920                          </div>
921                        </div>
922
923                        <DialogFooter>
924                          <Button
925                            variant="outline"
926                            onClick={() => setEditingInstance(null)}
927                          >
928                            Cancel
929                          </Button>
930                          <Button onClick={handleEditInstance}>
931                            Update Instance
932                          </Button>
933                        </DialogFooter>
934                      </DialogContent>
935                    </Dialog>
936
937                    <Button
938                      variant="outline"
939                      size="sm"
940                      onClick={() => handleDeleteInstance(instance.id)}
941                      className="text-red-600 hover:text-red-700"
942                    >
943                      <Trash2 className="h-4 w-4" />
944                    </Button>
945                  </div>
946                </div>
947              </Card>
948            ))
949          )}
950        </div>
951      </div>
952    </div>
953  );
954};
955
956export default VPSAdminUI;

Dependencies

External Libraries

lucide-reactreact

Shadcn/UI Components

badgebuttoncarddialoginputlabelselect

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.