698 lines
22 KiB
JavaScript
698 lines
22 KiB
JavaScript
import React, { useState, useEffect } from "react";
|
|
import { Button } from '../ui/button';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card";
|
|
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "../ui/select";
|
|
import { Input } from "../ui/input";
|
|
import { Label } from "../ui/label";
|
|
import Loader from "../ui/loader";
|
|
import { useToast } from "../ui/toast";
|
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from "../ui/tabs";
|
|
import { Textarea } from "../ui/textarea";
|
|
import { Switch } from "../ui/switch";
|
|
import { useIsLoggedIn } from '../../lib/isLoggedIn';
|
|
|
|
export default function NewCloudInstance() {
|
|
const PUBLIC_UTHO_API_KEY = import.meta.env.PUBLIC_UTHO_API_KEY;
|
|
const { isLoggedIn, loading, error, sessionData } = useIsLoggedIn();
|
|
const { showToast } = useToast();
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [isFetchingData, setIsFetchingData] = useState(true);
|
|
const [deployError, setDeployError] = useState()
|
|
const [deployStatus, setDeployStatus] = useState({});
|
|
const [copied, setCopied] = useState(false);
|
|
const [plans, setPlans] = useState([
|
|
{
|
|
"id": "10027",
|
|
"type": "cloud",
|
|
"slug": "dedicated-cpu",
|
|
"disk": "80",
|
|
"ram": "4096",
|
|
"cpu": "2",
|
|
"bandwidth": "1000",
|
|
"dedicated_vcore": "1",
|
|
"is_available": "YES",
|
|
"price": 26.83,
|
|
"price_currency": 26.83,
|
|
"price_cur": "$26.8"
|
|
},
|
|
{
|
|
"id": "10028",
|
|
"type": "cloud",
|
|
"slug": "dedicated-cpu",
|
|
"disk": "160",
|
|
"ram": "8192",
|
|
"cpu": "4",
|
|
"bandwidth": "2000",
|
|
"dedicated_vcore": "1",
|
|
"is_available": "YES",
|
|
"price": 53.72,
|
|
"price_currency": 53.72,
|
|
"price_cur": "$53.7"
|
|
},
|
|
{
|
|
"id": "10029",
|
|
"type": "cloud",
|
|
"slug": "dedicated-cpu",
|
|
"disk": "480",
|
|
"ram": "32768",
|
|
"cpu": "8",
|
|
"bandwidth": "3000",
|
|
"dedicated_vcore": "1",
|
|
"is_available": "YES",
|
|
"price": 151.91,
|
|
"price_currency": 151.91,
|
|
"price_cur": "$151.9"
|
|
},
|
|
{
|
|
"id": "10030",
|
|
"type": "cloud",
|
|
"slug": "dedicated-cpu",
|
|
"disk": "960",
|
|
"ram": "65536",
|
|
"cpu": "16",
|
|
"bandwidth": "4000",
|
|
"dedicated_vcore": "1",
|
|
"is_available": "YES",
|
|
"price": 292.19,
|
|
"price_currency": 292.19,
|
|
"price_cur": "$292.2"
|
|
},
|
|
]);
|
|
// const [sshKeys, setSshKeys] = useState([]);
|
|
const [showAddSshKey, setShowAddSshKey] = useState(false);
|
|
const [vpcs, setVpcs] = useState();
|
|
const [images, setImages] = useState([
|
|
{
|
|
"distro": null,
|
|
"distribution": "Alma Linux",
|
|
"version": "9.2 x86_64",
|
|
"image": "almalinux-9.2-x86_64",
|
|
"cost": 0
|
|
},
|
|
{
|
|
"distro": null,
|
|
"distribution": "Alma Linux",
|
|
"version": "8.4 x86_64",
|
|
"image": "AlmaLinux_8.4_x86_64",
|
|
"cost": 0
|
|
},
|
|
{
|
|
"distro": null,
|
|
"distribution": "Centos",
|
|
"version": "7.3 x86_64",
|
|
"image": "centos-7.3-x86_64",
|
|
"cost": 0
|
|
},
|
|
{
|
|
"distro": null,
|
|
"distribution": "Centos",
|
|
"version": "8 x86_64",
|
|
"image": "centos-8-x86_64",
|
|
"cost": 0
|
|
},
|
|
{
|
|
"distro": null,
|
|
"distribution": "Centos",
|
|
"version": "7.9 x86_64",
|
|
"image": "centos-7.9-x86_64",
|
|
"cost": 0
|
|
},
|
|
{
|
|
"distro": null,
|
|
"distribution": "Centos",
|
|
"version": "Ameyo x86_64",
|
|
"image": "AmeyoCentos7",
|
|
"cost": 0
|
|
},
|
|
{
|
|
"distro": null,
|
|
"distribution": "Debian",
|
|
"version": "9.4 x86_64",
|
|
"image": "debian-9.4-x86_64",
|
|
"cost": 0
|
|
},
|
|
{
|
|
"distro": null,
|
|
"distribution": "Debian",
|
|
"version": "9.13 x86_64",
|
|
"image": "debian-9.13-x86_64",
|
|
"cost": 0
|
|
},
|
|
{
|
|
"distro": null,
|
|
"distribution": "Debian",
|
|
"version": "10 x86_64",
|
|
"image": "debian-10-x86_64",
|
|
"cost": 0
|
|
},
|
|
{
|
|
"distro": null,
|
|
"distribution": "Debian",
|
|
"version": "11 x86_64",
|
|
"image": "debian-11-x86_64",
|
|
"cost": 0
|
|
},
|
|
{
|
|
"distro": null,
|
|
"distribution": "Debian",
|
|
"version": "12 x86_64",
|
|
"image": "debian-12-x86_64",
|
|
"cost": 0
|
|
},
|
|
{
|
|
"distro": null,
|
|
"distribution": "Fedora",
|
|
"version": "34 x86_64",
|
|
"image": "fedora-34-x86_64",
|
|
"cost": 0
|
|
},
|
|
{
|
|
"distro": null,
|
|
"distribution": "Fedora",
|
|
"version": "32 x86_64",
|
|
"image": "fedora-32-x86_64",
|
|
"cost": 0
|
|
},
|
|
{
|
|
"distro": null,
|
|
"distribution": "Fedora",
|
|
"version": "33 x86_64",
|
|
"image": "fedora-33-x86_64",
|
|
"cost": 0
|
|
},
|
|
{
|
|
"distro": null,
|
|
"distribution": "FortiOS",
|
|
"version": "7.6.2 ",
|
|
"image": "fortios-762",
|
|
"cost": 0
|
|
},
|
|
{
|
|
"distro": null,
|
|
"distribution": "Rocky Linux",
|
|
"version": "8.7 x86_64",
|
|
"image": "rocky-8.7-86_64",
|
|
"cost": 0
|
|
},
|
|
{
|
|
"distro": null,
|
|
"distribution": "Rocky Linux",
|
|
"version": "8.8 x86_64",
|
|
"image": "rocky-8.8-x86_64",
|
|
"cost": 0
|
|
},
|
|
{
|
|
"distro": null,
|
|
"distribution": "Rocky Linux",
|
|
"version": "9.1 x86_64",
|
|
"image": "rocky-9.1-x86_64",
|
|
"cost": 0
|
|
},
|
|
{
|
|
"distro": null,
|
|
"distribution": "Rocky Linux",
|
|
"version": "9.2 x86_64",
|
|
"image": "rocky-9.2-x86_64",
|
|
"cost": 0
|
|
},
|
|
{
|
|
"distro": null,
|
|
"distribution": "Ubuntu",
|
|
"version": "20.04 x86_64",
|
|
"image": "ubuntu-20.04-x86_64",
|
|
"cost": 0
|
|
},
|
|
{
|
|
"distro": null,
|
|
"distribution": "Ubuntu",
|
|
"version": "18.04 x86_64",
|
|
"image": "ubuntu-18.10-x86_64",
|
|
"cost": 0
|
|
},
|
|
{
|
|
"distro": null,
|
|
"distribution": "Ubuntu",
|
|
"version": "22.04 x86_64",
|
|
"image": "ubuntu-22.04-x86_64",
|
|
"cost": 0
|
|
},
|
|
{
|
|
"distro": null,
|
|
"distribution": "Windows",
|
|
"version": "Server 2012R2 x86_64",
|
|
"image": "windows-server2012r2-x86_64",
|
|
"cost": 1200
|
|
},
|
|
{
|
|
"distro": null,
|
|
"distribution": "Windows",
|
|
"version": "Server 2016 x86_64",
|
|
"image": "windows-2016",
|
|
"cost": 1200
|
|
},
|
|
{
|
|
"distro": null,
|
|
"distribution": "Windows",
|
|
"version": "Server 2019 x86_64",
|
|
"image": "windows2019-10nov2020",
|
|
"cost": 1200
|
|
},
|
|
{
|
|
"distro": null,
|
|
"distribution": "Windows",
|
|
"version": "Server 2022 x86_64",
|
|
"image": "windows-2022",
|
|
"cost": 1200
|
|
}
|
|
])
|
|
const [formData, setFormData] = useState({
|
|
dcslug: '',
|
|
planid: '',
|
|
billingcycle: "hourly",
|
|
auth: "option2",
|
|
enable_publicip: '',
|
|
subnetRequired: '',
|
|
firewall: "23434645",
|
|
cpumodel: "intel",
|
|
enablebackup: '',
|
|
root_password: '',
|
|
support: "unmanaged",
|
|
vpc: '',
|
|
cloud: [
|
|
{
|
|
hostname: ''
|
|
}
|
|
],
|
|
image: '',
|
|
sshkeys: ''
|
|
});
|
|
|
|
useEffect(() => {
|
|
const fetchData = async () => {
|
|
try {
|
|
const vpcResponse = await fetch('https://api.utho.com/v2/vpc', {
|
|
headers: { "Authorization": `Bearer ${PUBLIC_UTHO_API_KEY}` }
|
|
});
|
|
|
|
const vpcsData = await vpcResponse.json();
|
|
|
|
setVpcs(vpcsData.vpc || []);
|
|
} catch (error) {
|
|
showToast({
|
|
title: "Error",
|
|
description: "Failed to load configuration data",
|
|
variant: "destructive"
|
|
});
|
|
} finally {
|
|
setIsFetchingData(false);
|
|
}
|
|
};
|
|
|
|
fetchData();
|
|
}, []);
|
|
|
|
|
|
const handleSubmit = async (e) => {
|
|
e.preventDefault();
|
|
setIsLoading(true);
|
|
|
|
try {
|
|
const payload = {
|
|
dcslug: formData.dcslug,
|
|
planid: formData.planid,
|
|
billingcycle: "hourly",
|
|
auth: "option2",
|
|
enable_publicip: formData.enable_publicip ? true : false,
|
|
subnetRequired: false,
|
|
firewall: "23434645",
|
|
cpumodel: "intel",
|
|
enablebackup: formData.enablebackup ? true : false,
|
|
root_password: formData.rootPassword,
|
|
support: "unmanaged",
|
|
vpc: formData.dcslug === 'inmumbaizone2' ? '68c36d65-bda8-43cc-94b5-ae28bb2c3306' : formData.dcslug === 'inbangalore' ? 'c17032c9-3cfd-4028-8f2a-f3f5aa8c2976' : '',
|
|
cloud: [
|
|
{
|
|
hostname: formData.hostname
|
|
}
|
|
],
|
|
image: formData.image,
|
|
sshkeys: formData.sshkeys
|
|
};
|
|
// inbangalore = c17032c9-3cfd-4028-8f2a-f3f5aa8c2976
|
|
// inmumbai = 68c36d65-bda8-43cc-94b5-ae28bb2c3306
|
|
// Add authentication based on selected method
|
|
if (formData.auth === "ssh_key") {
|
|
payload.sshkeys = formData.sshkeys;
|
|
} else {
|
|
payload.password = formData.password;
|
|
}
|
|
console.log('payload', payload)
|
|
const response = await fetch("https://host-api.cs1.hz.siliconpin.com/v1/vps/?query=deploy&source=chanel_1", {
|
|
credentials: "include",
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.status === "success") {
|
|
setDeployStatus({status : data.status, ip : data.ipv4})
|
|
// showToast({
|
|
// title: "Deployment Started",
|
|
// description: `Server ${data.cloudid} is being deployed with IP ${data.ipv4}`,
|
|
// variant: "success"
|
|
// });
|
|
// Reset form after successful deployment
|
|
setFormData(prev => ({
|
|
...prev,
|
|
hostname: "",
|
|
planid: ""
|
|
}));
|
|
} else {
|
|
throw new Error(data.message || "Failed to deploy instance");
|
|
}
|
|
} catch (error) {
|
|
setDeployError(error.message)
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleChange = (e) => {
|
|
const { name, value, type, checked } = e.target;
|
|
setFormData(prev => ({
|
|
...prev,
|
|
[name]: type === "checkbox" ? checked : value
|
|
}));
|
|
};
|
|
|
|
const handleAddSshKey = async () => {
|
|
try {
|
|
const response = await fetch('https://api.utho.com/v2/key/import', {
|
|
method: "POST",
|
|
headers: {
|
|
"Authorization": `Bearer ${PUBLIC_UTHO_API_KEY}`,
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify({
|
|
name: formData.newSshKeyName,
|
|
sshkey: formData.newSshKeyValue
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.status === 'success') {
|
|
// Refresh SSH keys list
|
|
const sshResponse = await fetch("https://api.utho.com/v2/key", {
|
|
headers: { "Authorization": `Bearer ${PUBLIC_UTHO_API_KEY}` }
|
|
});
|
|
const sshData = await sshResponse.json();
|
|
setSshKeys(sshData.key || []);
|
|
|
|
showToast({
|
|
title: "SSH Key Added",
|
|
description: "Your SSH key has been successfully added",
|
|
variant: "success"
|
|
});
|
|
|
|
setShowAddSshKey(false);
|
|
}
|
|
} catch (error) {
|
|
showToast({
|
|
title: "Error",
|
|
description: "Failed to add SSH key",
|
|
variant: "destructive"
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleCopy = (text) => {
|
|
navigator.clipboard.writeText(text);
|
|
setCopied(true);
|
|
setTimeout(() => setCopied(false), 1000); // revert after 1 second
|
|
};
|
|
|
|
|
|
if (loading || (isLoggedIn && isFetchingData)) {
|
|
return <Loader />;
|
|
}
|
|
|
|
if (deployError) return <p>Error: {deployError?.message}</p>;
|
|
|
|
if (!isLoggedIn) {
|
|
return <p className="text-center mt-8">
|
|
You are not Logged in. <a href="/login" className="text-[#6d9e37]">Click Here</a> to login first then you can add this service.
|
|
</p>;
|
|
}
|
|
|
|
|
|
|
|
if(deployStatus.status === 'success'){
|
|
return (
|
|
<>
|
|
<Card className="w-full max-w-2xl mx-auto my-4">
|
|
<CardHeader>
|
|
<CardTitle>Create New Vertual Private Server (VPS)</CardTitle>
|
|
<CardDescription>Provide the root password then you can access vps using the password. <br /> later on you can setup ssh key</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p>Your New VPS</p>
|
|
<div className="flex flex-col my-2">
|
|
<Label>IP Address:</Label>
|
|
<div className="flex">
|
|
<Input className="rounded-l-md rounded-r-none text-xl font-bold border-[2px] border-[#6d9e37]" type="text" readOnly value={deployStatus.ip} />
|
|
<Button className="rounded-l-none rounded-r-md" onClick={() => handleCopy(deployStatus.ip)}>{copied ? 'Copied!' : 'Copy' }</Button>
|
|
</div>
|
|
</div>
|
|
<div className="flex justify-center place-items-center">
|
|
<Button className="" onClick={() => window.location.href = '/services/cloud-instance'}>Back</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</>
|
|
)
|
|
}
|
|
return (
|
|
<Card className="w-full max-w-2xl mx-auto my-4">
|
|
<CardHeader>
|
|
<CardTitle>Create New Vertual Private Server (VPS)</CardTitle>
|
|
<CardDescription>Provide the root password then you can access vps using the password. <br /> later on you can setup ssh key</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="hostname">Hostname *</Label>
|
|
<Input
|
|
id="hostname"
|
|
name="hostname"
|
|
value={formData.hostname}
|
|
onChange={handleChange}
|
|
placeholder="server1.example.com"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="dcslug">Data Center *</Label>
|
|
<Select
|
|
name="dcslug"
|
|
value={formData.dcslug}
|
|
onValueChange={(value) => setFormData({...formData, dcslug: value})}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select location" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="inmumbaizone2">Mumbai</SelectItem>
|
|
<SelectItem value="inbangalore">Bangalore</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="planid">Plan *</Label>
|
|
<Select
|
|
name="planid"
|
|
value={formData.planid}
|
|
onValueChange={(value) => setFormData({...formData, planid: value})}
|
|
required
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select a plan" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{plans.map(plan => (
|
|
<SelectItem key={plan.id} value={plan.id}>
|
|
{`${plan.cpu} vCPU, ${plan.ram}MB RAM - ${plan.price_cur}/mo`}
|
|
</SelectItem>
|
|
))}
|
|
{/* <SelectItem key={plans[0].id} value={plans[0].id}>
|
|
{`${plans[0].cpu} vCPU, ${plans[0].ram}MB RAM - ${plans[0].price_cur}/mo`}
|
|
</SelectItem> */}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="image">Operating System *</Label>
|
|
<Select
|
|
name="image"
|
|
value={formData.image}
|
|
onValueChange={(value) => setFormData({...formData, image: value})}>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select OS" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{
|
|
images.map((image, i) => (
|
|
<SelectItem key={i} value={image.image}>{image.image.split('-x86_64')[0]}</SelectItem>
|
|
))
|
|
}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="password">Root Password *</Label>
|
|
<Input
|
|
id="password"
|
|
name="password"
|
|
type="password"
|
|
value={formData.password}
|
|
onChange={handleChange}
|
|
placeholder="Enter root password"
|
|
required={formData.auth === "password"}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* <div className="space-y-4">
|
|
<h3 className="text-lg font-medium">Authentication</h3>
|
|
<Tabs
|
|
value={formData.auth}
|
|
onValueChange={(value) => setFormData({...formData, auth: value})}
|
|
className="w-full">
|
|
<TabsList className="grid w-full grid-cols-2 gap-4">
|
|
<TabsTrigger className={`p-2 rounded-md ${formData.auth === 'ssh_key' ? 'bg-[#6d9e37] text-[#FFF]' : 'border border-[#6d9e37]'}`} value="ssh_key">SSH Key</TabsTrigger>
|
|
<TabsTrigger className={`p-2 rounded-md ${formData.auth === 'password' ? 'bg-[#6d9e37] text-[#FFF]' : 'border border-[#6d9e37]'}`} value="password">Password</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="ssh_key" className="space-y-4">
|
|
<div className="space-y-2">
|
|
<Label>Select SSH Key</Label>
|
|
<div className="flex gap-2">
|
|
<Select
|
|
value={formData.sshkeys}
|
|
onValueChange={(value) => setFormData({...formData, sshkeys: value})}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select SSH key" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{sshKeys.map(key => (
|
|
<SelectItem key={key.id} value={key.id}>{key.name}</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
className="whitespace-nowrap"
|
|
onClick={() => setShowAddSshKey(!showAddSshKey)}
|
|
>
|
|
{showAddSshKey ? "Cancel" : "Add New"}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{showAddSshKey && (
|
|
<div className="space-y-4 p-4 border rounded-lg">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="newSshKeyName">Key Name</Label>
|
|
<Input
|
|
id="newSshKeyName"
|
|
name="newSshKeyName"
|
|
value={formData.newSshKeyName}
|
|
onChange={handleChange}
|
|
placeholder="My Laptop Key"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="newSshKeyValue">Public Key</Label>
|
|
<Textarea
|
|
id="newSshKeyValue"
|
|
name="newSshKeyValue"
|
|
value={formData.newSshKeyValue}
|
|
onChange={handleChange}
|
|
placeholder="ssh-rsa AAAAB3NzaC1yc2E..."
|
|
rows={4}
|
|
/>
|
|
</div>
|
|
<Button
|
|
type="button"
|
|
onClick={handleAddSshKey}
|
|
disabled={!formData.newSshKeyName || !formData.newSshKeyValue}
|
|
>
|
|
Add SSH Key
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</TabsContent>
|
|
|
|
<TabsContent value="password" className="space-y-2">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="password">Root Password *</Label>
|
|
<Input
|
|
id="password"
|
|
name="password"
|
|
type="password"
|
|
value={formData.password}
|
|
onChange={handleChange}
|
|
placeholder="Enter root password"
|
|
required={formData.auth === "password"}
|
|
/>
|
|
</div>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div> */}
|
|
|
|
<div className="space-y-4">
|
|
<h3 className="text-lg font-medium">Options</h3>
|
|
<div className="flex flex-col gap-4">
|
|
<div className="flex items-center space-x-2">
|
|
<Switch
|
|
id="enable_publicip"
|
|
checked={formData.enable_publicip}
|
|
onCheckedChange={(checked) => setFormData({...formData, enable_publicip: checked})}
|
|
/>
|
|
<Label htmlFor="enable_publicip">Enable Public IP</Label>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-2">
|
|
<Switch
|
|
id="enablebackup"
|
|
checked={formData.enablebackup}
|
|
onCheckedChange={(checked) => setFormData({...formData, enablebackup: checked})}
|
|
/>
|
|
<Label htmlFor="enablebackup">Enable Weekly Backups (+20%)</Label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end pt-4">
|
|
<Button type="submit" disabled={isLoading} className="w-full md:w-auto">
|
|
{isLoading ? "Deploying..." : "Deploy Instance"}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
} |