fix markdown editor css and change mobile bottom nav menu

This commit is contained in:
suvodip ghosh
2025-06-16 15:42:05 +00:00
parent 22fade091d
commit f6857712a8
9 changed files with 1632 additions and 187 deletions

View File

@@ -14,21 +14,24 @@ export default function NewKubernetesService() {
const [isFetchingData, setIsFetchingData] = useState(true);
const [plans, setPlans] = useState([]);
const [vpcs, setVpcs] = useState([]);
const [subnets, setSubnets] = useState([]);
const UTHO_API_KEY = "Bearer IoNXhkRJsQPyOEqFMceSfzuKaDLrpxUCATgZjiVdvYlBHbwWmGtn";
const [formData, setFormData] = useState({
datacenter: "mumbai",
plan: "",
name: "",
k8s_version: "1.27",
nodes: 1,
storage: "50",
public_ip: true,
auto_scaling: false,
min_nodes: 1,
max_nodes: 3,
dcslug: "innoida",
cluster_version: "1.24",
cluster_label: "",
nodepools: [{
label: "default-pool",
size: "",
count: 1,
maxCount: 1
}],
firewall: "",
vpc: "",
subnet: ""
subnet: "",
network_type: "public",
cpumodel: "intel"
});
useEffect(() => {
@@ -52,19 +55,19 @@ export default function NewKubernetesService() {
const plansData = await plansResponse.json();
const vpcsData = await vpcsResponse.json();
console.log('vpcsData.vpc', vpcsData.vpc)
setPlans(plansData.plans || []);
setVpcs(vpcsData.vpc || []);
// Set default VPC and subnet if available
// Set default VPC if available
if (vpcsData.vpc?.length > 0) {
const firstVpc = vpcsData.vpc[0];
const firstSubnet = firstVpc.subnets?.[0]?.id || "";
setFormData(prev => ({
...prev,
vpc: firstVpc.id,
subnet: firstSubnet
vpc: firstVpc.id
}));
// Fetch subnets for the default VPC
await fetchSubnets(firstVpc.id);
}
} catch (error) {
console.error("Initial data fetch error:", error);
@@ -81,19 +84,49 @@ console.log('vpcsData.vpc', vpcsData.vpc)
fetchInitialData();
}, []);
const handleVpcChange = (vpcId) => {
const selectedVpc = vpcs.find(vpc => vpc.id === vpcId);
const firstSubnet = selectedVpc?.subnet?.[0]?.id || "";
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": 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: firstSubnet
subnet: ""
}));
};
const getCurrentSubnets = () => {
const selectedVpc = vpcs.find(vpc => vpc.id === formData.vpc);
return selectedVpc?.subnets || [];
await fetchSubnets(vpcId);
};
const handleSubmit = async (e) => {
@@ -101,27 +134,29 @@ console.log('vpcsData.vpc', vpcsData.vpc)
setIsLoading(true);
try {
if (!formData.vpc || !formData.subnet) {
throw new Error("Please select both VPC and Subnet to proceed");
// 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 = {
datacenter: formData.datacenter,
plan: formData.plan,
name: formData.name,
k8s_version: formData.k8s_version,
nodes: formData.nodes,
storage: formData.storage,
public_ip: formData.public_ip,
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
subnet: formData.subnet, // Now required based on API behavior
network_type: formData.network_type,
cpumodel: formData.cpumodel
};
if (formData.auto_scaling) {
payload.auto_scaling = true;
payload.min_nodes = formData.min_nodes;
payload.max_nodes = formData.max_nodes;
}
console.log("Deployment payload:", payload);
const response = await fetch("https://api.utho.com/v2/kubernetes/deploy", {
method: "POST",
@@ -144,11 +179,18 @@ console.log('vpcsData.vpc', vpcsData.vpc)
description: `Cluster ${data.cluster_id || data.id} is being deployed`,
variant: "success"
});
// Reset form
setFormData(prev => ({
...prev,
name: "",
plan: "",
nodes: 1
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");
@@ -173,6 +215,15 @@ console.log('vpcsData.vpc', vpcsData.vpc)
}));
};
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">
@@ -182,8 +233,6 @@ console.log('vpcsData.vpc', vpcsData.vpc)
);
}
const currentSubnets = getCurrentSubnets();
return (
<Card className="w-full max-w-2xl mx-auto my-4">
<CardHeader>
@@ -194,97 +243,164 @@ console.log('vpcsData.vpc', vpcsData.vpc)
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
{/* Name, Datacenter, and Kubernetes Version fields */}
{/* Cluster Label */}
<div className="space-y-2">
<Label htmlFor="name">Cluster Name *</Label>
<Label htmlFor="cluster_label">Cluster Label *</Label>
<Input
id="name"
name="name"
value={formData.name}
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="datacenter">Data Center *</Label>
<Label htmlFor="dcslug">Data Center *</Label>
<Select
name="datacenter"
value={formData.datacenter}
onValueChange={(value) => setFormData({...formData, datacenter: value})}
name="dcslug"
value={formData.dcslug}
onValueChange={(value) => setFormData({...formData, dcslug: value})}
required
>
<SelectTrigger>
<SelectValue placeholder="Select location" />
</SelectTrigger>
<SelectContent>
<SelectItem value="mumbai">Mumbai</SelectItem>
<SelectItem value="noida">Noida</SelectItem>
<SelectItem value="delhi">Delhi</SelectItem>
<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="k8s_version">Kubernetes Version *</Label>
<Label htmlFor="cluster_version">Kubernetes Version *</Label>
<Select
name="k8s_version"
value={formData.k8s_version}
onValueChange={(value) => setFormData({...formData, k8s_version: value})}
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.27">1.27 (Latest)</SelectItem>
<SelectItem value="1.26">1.26</SelectItem>
<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>
{/* Node Plan and Count */}
{/* Network Type and CPU Model */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="plan">Node Plan *</Label>
<Label htmlFor="network_type">Network Type *</Label>
<Select
name="plan"
value={formData.plan}
onValueChange={(value) => setFormData({...formData, plan: value})}
name="network_type"
value={formData.network_type}
onValueChange={(value) => setFormData({...formData, network_type: value})}
required
>
<SelectTrigger>
<SelectValue placeholder="Select a plan" />
<SelectValue placeholder="Select network type" />
</SelectTrigger>
<SelectContent>
{plans.map(plan => (
<SelectItem key={plan.id} value={plan.id}>
{`${plan.cpu} vCPU, ${Math.floor(parseInt(plan.ram)/1024)}GB RAM, ${plan.disk}GB Disk`}
</SelectItem>
))}
<SelectItem value="public">Public</SelectItem>
<SelectItem value="private">Private</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="nodes">Initial Node Count *</Label>
<Input
id="nodes"
name="nodes"
type="number"
min="1"
max="10"
value={formData.nodes}
onChange={handleChange}
<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>
{/* VPC and Subnet Selection */}
{/* 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>
@@ -314,22 +430,22 @@ console.log('vpcsData.vpc', vpcsData.vpc)
value={formData.subnet}
onValueChange={(value) => setFormData({...formData, subnet: value})}
required
disabled={!formData.vpc || currentSubnets.length === 0}
disabled={!formData.vpc || subnets.length === 0}
>
<SelectTrigger>
<SelectValue placeholder={currentSubnets.length === 0 ? "No subnets available" : "Select subnet"}>
{formData.subnet || (currentSubnets.length === 0 ? "No subnets available" : "Select subnet")}
<SelectValue placeholder={subnets.length === 0 ? "No subnets available" : "Select subnet"}>
{formData.subnet || (subnets.length === 0 ? "No subnets available" : "Select subnet")}
</SelectValue>
</SelectTrigger>
<SelectContent>
{currentSubnets.map(subnet => (
{subnets.map(subnet => (
<SelectItem key={subnet.id} value={subnet.id}>
{subnet.name || subnet.id} ({subnet.network}/{subnet.size})
</SelectItem>
))}
</SelectContent>
</Select>
{currentSubnets.length === 0 && formData.vpc && (
{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>
@@ -337,85 +453,22 @@ console.log('vpcsData.vpc', vpcsData.vpc)
</div>
</div>
{/* Storage and Advanced Options */}
{/* Firewall (Optional) */}
<div className="space-y-2">
<Label htmlFor="storage">Storage per Node (GB) *</Label>
<Select
name="storage"
value={formData.storage}
onValueChange={(value) => setFormData({...formData, storage: value})}
required
>
<SelectTrigger>
<SelectValue placeholder="Select storage" />
</SelectTrigger>
<SelectContent>
<SelectItem value="50">50 GB</SelectItem>
<SelectItem value="100">100 GB</SelectItem>
<SelectItem value="200">200 GB</SelectItem>
<SelectItem value="500">500 GB</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-4">
<h3 className="text-lg font-medium">Advanced Options</h3>
<div className="flex flex-col gap-4">
<div className="flex items-center space-x-2">
<Switch
id="public_ip"
checked={formData.public_ip}
onCheckedChange={(checked) => setFormData({...formData, public_ip: checked})}
/>
<Label htmlFor="public_ip">Enable Public IP for Nodes</Label>
</div>
<div className="flex items-center space-x-2">
<Switch
id="auto_scaling"
checked={formData.auto_scaling}
onCheckedChange={(checked) => setFormData({...formData, auto_scaling: checked})}
/>
<Label htmlFor="auto_scaling">Enable Auto Scaling</Label>
</div>
{formData.auto_scaling && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 p-4 border rounded-lg">
<div className="space-y-2">
<Label htmlFor="min_nodes">Minimum Nodes</Label>
<Input
id="min_nodes"
name="min_nodes"
type="number"
min="1"
max="10"
value={formData.min_nodes}
onChange={handleChange}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="max_nodes">Maximum Nodes</Label>
<Input
id="max_nodes"
name="max_nodes"
type="number"
min={formData.min_nodes}
max="20"
value={formData.max_nodes}
onChange={handleChange}
required
/>
</div>
</div>
)}
</div>
<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.vpc || !formData.subnet}
disabled={isLoading || !formData.cluster_label || !formData.nodepools[0].size || !formData.vpc || !formData.subnet}
className="w-full md:w-auto"
>
{isLoading ? (