sp/src/components/BuyServices/NewHetznerCloudInstance.jsx

374 lines
13 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";
export default function NewHetznerInstance() {
const { showToast } = useToast();
const [isLoading, setIsLoading] = useState(false);
const [isFetchingData, setIsFetchingData] = useState(true);
const [serverTypes, setServerTypes] = useState([]);
const [locations, setLocations] = useState([]);
const [images, setImages] = useState([]);
const [sshKeys, setSshKeys] = useState([]);
const [showAddSshKey, setShowAddSshKey] = useState(false);
const PUBLIC_HETZNER_API_KEY = import.meta.env.PUBLIC_HETZNER_API_KEY;
const [formData, setFormData] = useState({
name: "",
server_type: "",
image: "",
location: "nbg1", // Default to Nuremberg
ssh_keys: [],
user_data: "",
backups: false,
start_after_create: true
});
useEffect(() => {
const fetchData = async () => {
try {
// Fetch all required data in parallel
const [serverTypesResponse, locationsResponse, imagesResponse, sshKeysResponse] = await Promise.all([
fetch("https://api.hetzner.cloud/v1/server_types", {
headers: { "Authorization": `Bearer ${PUBLIC_HETZNER_API_KEY}` }
}),
fetch("https://api.hetzner.cloud/v1/locations", {
headers: { "Authorization": `Bearer ${PUBLIC_HETZNER_API_KEY}` }
}),
fetch("https://api.hetzner.cloud/v1/images", {
headers: { "Authorization": `Bearer ${PUBLIC_HETZNER_API_KEY}` }
}),
fetch("https://api.hetzner.cloud/v1/ssh_keys", {
headers: { "Authorization": `Bearer ${PUBLIC_HETZNER_API_KEY}` }
})
]);
const serverTypesData = await serverTypesResponse.json();
const locationsData = await locationsResponse.json();
const imagesData = await imagesResponse.json();
const sshKeysData = await sshKeysResponse.json();
setServerTypes(serverTypesData.server_types || []);
setLocations(locationsData.locations || []);
setImages(imagesData.images.filter(img => img.type === "system") || []);
setSshKeys(sshKeysData.ssh_keys || []);
} 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 = {
name: formData.name,
server_type: formData.server_type,
image: formData.image,
location: formData.location,
backups: formData.backups,
ssh_keys: formData.ssh_keys,
start_after_create: formData.start_after_create
};
// Add user_data if provided
if (formData.user_data) {
payload.user_data = formData.user_data;
}
const response = await fetch("https://api.hetzner.cloud/v1/servers", {
method: "POST",
headers: {
"Authorization": `Bearer ${PUBLIC_HETZNER_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
});
const data = await response.json();
if (data.server) {
showToast({
title: "Deployment Started",
description: `Server ${data.server.name} is being deployed`,
variant: "success"
});
// Reset form after successful deployment
setFormData({
name: "",
server_type: "",
image: "",
location: "nbg1",
ssh_keys: [],
user_data: "",
backups: false,
start_after_create: true
});
} else {
throw new Error(data.message || "Failed to deploy instance");
}
} catch (error) {
showToast({
title: "Deployment Failed",
description: error.message,
variant: "destructive"
});
} 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.hetzner.cloud/v1/ssh_keys', {
method: "POST",
headers: {
"Authorization": `Bearer ${PUBLIC_HETZNER_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
name: formData.newSshKeyName,
public_key: formData.newSshKeyValue
})
});
const data = await response.json();
if (data.ssh_key) {
// Refresh SSH keys list
const sshResponse = await fetch("https://api.hetzner.cloud/v1/ssh_keys", {
headers: { "Authorization": `Bearer ${PUBLIC_HETZNER_API_KEY}` }
});
const sshData = await sshResponse.json();
setSshKeys(sshData.ssh_keys || []);
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"
});
}
};
if (isFetchingData) {
return <Loader />;
}
return (
<Card className="w-full max-w-2xl mx-auto my-4">
<CardHeader>
<CardTitle>Deploy New Hetzner Cloud Server</CardTitle>
<CardDescription>Configure your cloud server with essential parameters</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name">Server Name *</Label>
<Input
id="name"
name="name"
value={formData.name}
onChange={handleChange}
placeholder="my-hetzner-server"
required
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="location">Location *</Label>
<Select
name="location"
value={formData.location}
onValueChange={(value) => setFormData({...formData, location: value})}
>
<SelectTrigger>
<SelectValue placeholder="Select location" />
</SelectTrigger>
<SelectContent>
{locations.map(location => (
<SelectItem key={location.name} value={location.name}>
{`${location.city} (${location.name.toUpperCase()})`}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="server_type">Server Type *</Label>
<Select
name="server_type"
value={formData.server_type}
onValueChange={(value) => setFormData({...formData, server_type: value})}
required
>
<SelectTrigger>
<SelectValue placeholder="Select a server type" />
</SelectTrigger>
<SelectContent>
{serverTypes.map(type => (
<SelectItem key={type.name} value={type.name}>
{`${type.name} - ${type.cores} vCPU, ${type.memory}GB RAM`}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="image">Operating System *</Label>
<Select
name="image"
value={formData.image}
onValueChange={(value) => setFormData({...formData, image: value})}
required
>
<SelectTrigger>
<SelectValue placeholder="Select OS" />
</SelectTrigger>
<SelectContent>
{images.map(image => (
<SelectItem key={image.id} value={image.id.toString()}>
{image.description}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-4">
<h3 className="text-lg font-medium">SSH Keys</h3>
<div className="space-y-2">
<Label>Select SSH Keys</Label>
<div className="flex gap-2">
<Select
value={formData.ssh_keys[0] || ""}
onValueChange={(value) => setFormData({...formData, ssh_keys: [value]})}
>
<SelectTrigger>
<SelectValue placeholder="Select SSH key" />
</SelectTrigger>
<SelectContent>
{sshKeys.map(key => (
<SelectItem key={key.id} value={key.id.toString()}>{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>
)}
</div>
<div className="space-y-2">
<Label htmlFor="user_data">Cloud-Init User Data (Optional)</Label>
<Textarea
id="user_data"
name="user_data"
value={formData.user_data}
onChange={handleChange}
placeholder="#cloud-config\nwrite_files:\n - content: |\n Hello World\n path: /tmp/hello.txt"
rows={6}
/>
</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="backups"
checked={formData.backups}
onCheckedChange={(checked) => setFormData({...formData, backups: checked})}
/>
<Label htmlFor="backups">Enable Backups (+20% cost)</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 Server"}
</Button>
</div>
</form>
</CardContent>
</Card>
);
}