486 lines
16 KiB
JavaScript
486 lines
16 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 { Switch } from "../ui/switch";
|
|
|
|
export default function NewKubernetesService() {
|
|
const { showToast } = useToast();
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [isFetchingData, setIsFetchingData] = useState(true);
|
|
const [plans, setPlans] = useState([]);
|
|
const [vpcs, setVpcs] = useState([]);
|
|
const [subnets, setSubnets] = useState([]);
|
|
const PUBLIC_UTHO_API_KEY = import.meta.env.PUBLIC_UTHO_API_KEY;
|
|
|
|
const [formData, setFormData] = useState({
|
|
dcslug: "innoida",
|
|
cluster_version: "1.24",
|
|
cluster_label: "",
|
|
nodepools: [{
|
|
label: "default-pool",
|
|
size: "",
|
|
count: 1,
|
|
maxCount: 1
|
|
}],
|
|
firewall: "",
|
|
vpc: "",
|
|
subnet: "",
|
|
network_type: "public",
|
|
cpumodel: "intel"
|
|
});
|
|
|
|
useEffect(() => {
|
|
const fetchInitialData = async () => {
|
|
setIsFetchingData(true);
|
|
try {
|
|
const [plansResponse, vpcsResponse] = await Promise.all([
|
|
fetch("https://api.utho.com/v2/plans", {
|
|
headers: {
|
|
"Authorization": `Bearer ${PUBLIC_UTHO_API_KEY}`,
|
|
"Content-Type": "application/json"
|
|
}
|
|
}),
|
|
fetch("https://api.utho.com/v2/vpc", {
|
|
headers: {
|
|
"Authorization": `Bearer ${PUBLIC_UTHO_API_KEY}`,
|
|
"Content-Type": "application/json"
|
|
}
|
|
})
|
|
]);
|
|
|
|
const plansData = await plansResponse.json();
|
|
const vpcsData = await vpcsResponse.json();
|
|
|
|
setPlans(plansData.plans || []);
|
|
setVpcs(vpcsData.vpc || []);
|
|
|
|
// Set default VPC if available
|
|
if (vpcsData.vpc?.length > 0) {
|
|
const firstVpc = vpcsData.vpc[0];
|
|
setFormData(prev => ({
|
|
...prev,
|
|
vpc: firstVpc.id
|
|
}));
|
|
// Fetch subnets for the default VPC
|
|
await fetchSubnets(firstVpc.id);
|
|
}
|
|
} catch (error) {
|
|
console.error("Initial data fetch error:", error);
|
|
showToast({
|
|
title: "Error",
|
|
description: "Failed to load initial configuration data",
|
|
variant: "destructive"
|
|
});
|
|
} finally {
|
|
setIsFetchingData(false);
|
|
}
|
|
};
|
|
|
|
fetchInitialData();
|
|
}, []);
|
|
|
|
const fetchSubnets = async (vpcId) => {
|
|
try {
|
|
// First try to get subnets from the VPC data
|
|
const selectedVpc = vpcs.find(vpc => vpc.id === vpcId);
|
|
if (selectedVpc?.subnet?.length > 0) {
|
|
setSubnets(selectedVpc.subnet);
|
|
setFormData(prev => ({
|
|
...prev,
|
|
subnet: selectedVpc.subnet[0].id
|
|
}));
|
|
return;
|
|
}
|
|
|
|
// If no subnets in VPC data, try the subnets endpoint
|
|
const response = await fetch(`https://api.utho.com/v2/vpc/${vpcId}/subnets`, {
|
|
headers: {
|
|
"Authorization": `Bearer ${PUBLIC_UTHO_API_KEY}`,
|
|
"Content-Type": "application/json"
|
|
}
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
setSubnets(data.subnets || []);
|
|
if (data.subnets?.length > 0) {
|
|
setFormData(prev => ({
|
|
...prev,
|
|
subnet: data.subnets[0].id
|
|
}));
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("Error fetching subnets:", error);
|
|
}
|
|
};
|
|
|
|
const handleVpcChange = async (vpcId) => {
|
|
setFormData(prev => ({
|
|
...prev,
|
|
vpc: vpcId,
|
|
subnet: ""
|
|
}));
|
|
await fetchSubnets(vpcId);
|
|
};
|
|
|
|
const handleSubmit = async (e) => {
|
|
e.preventDefault();
|
|
setIsLoading(true);
|
|
|
|
try {
|
|
// Validate required fields including subnet
|
|
if (!formData.cluster_label || !formData.nodepools[0].size || !formData.vpc || !formData.subnet) {
|
|
throw new Error("Please fill all required fields including VPC and Subnet");
|
|
}
|
|
|
|
const payload = {
|
|
dcslug: formData.dcslug,
|
|
cluster_version: formData.cluster_version,
|
|
cluster_label: formData.cluster_label,
|
|
nodepools: formData.nodepools.map(pool => ({
|
|
label: pool.label,
|
|
size: pool.size,
|
|
count: pool.count.toString(),
|
|
maxCount: pool.maxCount.toString()
|
|
})),
|
|
firewall: formData.firewall || undefined,
|
|
vpc: formData.vpc,
|
|
subnet: formData.subnet, // Now required based on API behavior
|
|
network_type: formData.network_type,
|
|
cpumodel: formData.cpumodel
|
|
};
|
|
|
|
console.log("Deployment payload:", payload);
|
|
|
|
const response = await fetch("https://api.utho.com/v2/kubernetes/deploy", {
|
|
method: "POST",
|
|
headers: {
|
|
"Authorization": `Bearer ${PUBLIC_UTHO_API_KEY}`,
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (!response.ok) {
|
|
throw new Error(data.message || "Failed to deploy Kubernetes cluster");
|
|
}
|
|
|
|
if (data.status === "success") {
|
|
showToast({
|
|
title: "Success",
|
|
description: `Cluster ${data.cluster_id || data.id} is being deployed`,
|
|
variant: "success"
|
|
});
|
|
// Reset form
|
|
setFormData(prev => ({
|
|
...prev,
|
|
cluster_label: "",
|
|
nodepools: [{
|
|
label: "default-pool",
|
|
size: "",
|
|
count: 1,
|
|
maxCount: 1
|
|
}],
|
|
vpc: vpcs[0]?.id || "",
|
|
subnet: subnets[0]?.id || ""
|
|
}));
|
|
} else {
|
|
throw new Error(data.message || "Failed to deploy Kubernetes cluster");
|
|
}
|
|
} catch (error) {
|
|
showToast({
|
|
title: "Deployment Failed",
|
|
description: error.message,
|
|
variant: "destructive"
|
|
});
|
|
console.error("Deployment error:", error);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleChange = (e) => {
|
|
const { name, value, type, checked } = e.target;
|
|
setFormData(prev => ({
|
|
...prev,
|
|
[name]: type === "checkbox" ? checked : value
|
|
}));
|
|
};
|
|
|
|
const handleNodePoolChange = (index, field, value) => {
|
|
const updatedNodePools = [...formData.nodepools];
|
|
updatedNodePools[index][field] = value;
|
|
setFormData(prev => ({
|
|
...prev,
|
|
nodepools: updatedNodePools
|
|
}));
|
|
};
|
|
|
|
if (isFetchingData) {
|
|
return (
|
|
<div className="flex items-center justify-center h-64">
|
|
<Loader />
|
|
<span className="ml-2">Loading configuration...</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Card className="w-full max-w-2xl mx-auto my-4">
|
|
<CardHeader>
|
|
<CardTitle>Deploy New Kubernetes Cluster</CardTitle>
|
|
<CardDescription>
|
|
Configure your Kubernetes cluster with the required parameters
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
{/* Cluster Label */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="cluster_label">Cluster Label *</Label>
|
|
<Input
|
|
id="cluster_label"
|
|
name="cluster_label"
|
|
value={formData.cluster_label}
|
|
onChange={handleChange}
|
|
placeholder="my-cluster"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
{/* Data Center and Version */}
|
|
<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})}
|
|
required
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select location" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="innoida">Noida</SelectItem>
|
|
<SelectItem value="inmumbaizone2">Mumbai Zone 2</SelectItem>
|
|
<SelectItem value="indelhi">Delhi</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="cluster_version">Kubernetes Version *</Label>
|
|
<Select
|
|
name="cluster_version"
|
|
value={formData.cluster_version}
|
|
onValueChange={(value) => setFormData({...formData, cluster_version: value})}
|
|
required
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select version" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="1.24">1.24</SelectItem>
|
|
<SelectItem value="1.25">1.25</SelectItem>
|
|
<SelectItem value="1.26">1.26</SelectItem>
|
|
<SelectItem value="1.27">1.27</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Network Type and CPU Model */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="network_type">Network Type *</Label>
|
|
<Select
|
|
name="network_type"
|
|
value={formData.network_type}
|
|
onValueChange={(value) => setFormData({...formData, network_type: value})}
|
|
required
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select network type" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="public">Public</SelectItem>
|
|
<SelectItem value="private">Private</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="cpumodel">CPU Model *</Label>
|
|
<Select
|
|
name="cpumodel"
|
|
value={formData.cpumodel}
|
|
onValueChange={(value) => setFormData({...formData, cpumodel: value})}
|
|
required
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select CPU model" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="intel">Intel</SelectItem>
|
|
<SelectItem value="amd">AMD</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Node Pool Configuration */}
|
|
<div className="space-y-4">
|
|
<h3 className="text-lg font-medium">Node Pool Configuration</h3>
|
|
{formData.nodepools.map((pool, index) => (
|
|
<div key={index} className="p-4 border rounded-lg space-y-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor={`pool-label-${index}`}>Pool Label *</Label>
|
|
<Input
|
|
id={`pool-label-${index}`}
|
|
value={pool.label}
|
|
onChange={(e) => handleNodePoolChange(index, 'label', e.target.value)}
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor={`pool-size-${index}`}>Node Size *</Label>
|
|
<Select
|
|
value={pool.size}
|
|
onValueChange={(value) => handleNodePoolChange(index, 'size', value)}
|
|
required
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select node size" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{plans.map(plan => (
|
|
<SelectItem key={plan.id} value={plan.id}>
|
|
{`${plan.cpu} vCPU, ${Math.floor(parseInt(plan.ram)/1024)}GB RAM`}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor={`pool-count-${index}`}>Node Count *</Label>
|
|
<Input
|
|
id={`pool-count-${index}`}
|
|
type="number"
|
|
min="1"
|
|
value={pool.count}
|
|
onChange={(e) => handleNodePoolChange(index, 'count', parseInt(e.target.value))}
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor={`pool-maxCount-${index}`}>Max Nodes</Label>
|
|
<Input
|
|
id={`pool-maxCount-${index}`}
|
|
type="number"
|
|
min={pool.count}
|
|
value={pool.maxCount}
|
|
onChange={(e) => handleNodePoolChange(index, 'maxCount', parseInt(e.target.value))}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* VPC and Subnet */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="vpc">VPC *</Label>
|
|
<Select
|
|
name="vpc"
|
|
value={formData.vpc}
|
|
onValueChange={handleVpcChange}
|
|
required
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select VPC" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{vpcs.map(vpc => (
|
|
<SelectItem key={vpc.id} value={vpc.id}>
|
|
{vpc.name || vpc.id}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="subnet">Subnet *</Label>
|
|
<Select
|
|
name="subnet"
|
|
value={formData.subnet}
|
|
onValueChange={(value) => setFormData({...formData, subnet: value})}
|
|
required
|
|
disabled={!formData.vpc || subnets.length === 0}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder={subnets.length === 0 ? "No subnets available" : "Select subnet"}>
|
|
{formData.subnet || (subnets.length === 0 ? "No subnets available" : "Select subnet")}
|
|
</SelectValue>
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{subnets.map(subnet => (
|
|
<SelectItem key={subnet.id} value={subnet.id}>
|
|
{subnet.name || subnet.id} ({subnet.network}/{subnet.size})
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
{subnets.length === 0 && formData.vpc && (
|
|
<p className="text-sm text-muted-foreground mt-1">
|
|
No subnets found in this VPC. Please create a subnet first.
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Firewall (Optional) */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="firewall">Firewall ID (Optional)</Label>
|
|
<Input
|
|
id="firewall"
|
|
name="firewall"
|
|
value={formData.firewall}
|
|
onChange={handleChange}
|
|
placeholder="Firewall ID"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex justify-end pt-4">
|
|
<Button
|
|
type="submit"
|
|
disabled={isLoading || !formData.cluster_label || !formData.nodepools[0].size || !formData.vpc || !formData.subnet}
|
|
className="w-full md:w-auto"
|
|
>
|
|
{isLoading ? (
|
|
<>
|
|
<Loader className="mr-2 h-4 w-4" />
|
|
Deploying...
|
|
</>
|
|
) : "Deploy Kubernetes Cluster"}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
} |