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