configure hetzner template working

This commit is contained in:
mbecker20
2024-05-25 03:09:31 -07:00
parent 84dc29b77f
commit 99ccffbc38
3 changed files with 515 additions and 2 deletions
@@ -0,0 +1,507 @@
import { Config } from "@components/config";
import { ConfigItem, InputList } from "@components/config/util";
import { TextUpdateMenu } from "@components/util";
import { useRead, useWrite } from "@lib/hooks";
import { cn } from "@lib/utils";
import { Types } from "@monitor/client";
import { Button } from "@ui/button";
import { Card } from "@ui/card";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@ui/command";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTrigger,
} from "@ui/dialog";
import { Input } from "@ui/input";
import { Popover, PopoverContent, PopoverTrigger } from "@ui/popover";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@ui/select";
import { ChevronsUpDown, MinusCircle, PlusCircle, SearchX } from "lucide-react";
import { useState } from "react";
export const HetznerServerTemplateConfig = ({ id }: { id: string }) => {
const perms = useRead("GetPermissionLevel", {
target: { type: "ServerTemplate", id },
}).data;
const config = useRead("GetServerTemplate", { server_template: id }).data
?.config.params as Types.HetznerServerTemplateConfig;
const [update, set] = useState<Partial<Types.HetznerServerTemplateConfig>>(
{}
);
const { mutateAsync } = useWrite("UpdateServerTemplate");
if (!config) return null;
const disabled = perms !== Types.PermissionLevel.Write;
return (
<Config
disabled={disabled}
config={config}
update={update}
set={set}
onSave={async () => {
await mutateAsync({ id, config: { type: "Hetzner", params: update } });
}}
components={{
general: [
{
label: "General",
components: {
datacenter: (datacenter, set) => (
<DatacenterSelector
selected={datacenter}
set={set}
disabled={disabled}
/>
),
server_type: (server_type, set) => (
<ServerTypeSelector
selected={server_type}
set={set}
disabled={disabled}
/>
),
image: true,
automount: true,
port: true,
enable_public_ipv4: true,
enable_public_ipv6: true,
use_public_ip:
update.enable_public_ipv4 ?? config.enable_public_ipv4,
},
},
{
label: "Private Network Ids",
contentHidden:
(update.private_network_ids ?? config.private_network_ids)
?.length === 0,
actions: !disabled && (
<Button
variant="secondary"
onClick={() =>
set({
...update,
private_network_ids: [
...(update.private_network_ids ??
config.private_network_ids ??
[]),
0,
],
})
}
className="flex items-center gap-2 w-[200px]"
>
<PlusCircle className="w-4 h-4" />
Add Network Id
</Button>
),
components: {
private_network_ids: (values, set) => (
<InputList
field="private_network_ids"
values={values?.map((val) => val.toString()) ?? []}
set={({ private_network_ids }) =>
set({
private_network_ids: (
private_network_ids as string[]
).map((id) => Number(id)),
})
}
disabled={disabled}
placeholder="Private Network Id"
/>
),
},
},
{
label: "Firewall Ids",
contentHidden:
(update.firewall_ids ?? config.firewall_ids)?.length === 0,
actions: !disabled && (
<Button
variant="secondary"
onClick={() =>
set({
...update,
firewall_ids: [
...(update.firewall_ids ?? config.firewall_ids ?? []),
0,
],
})
}
className="flex items-center gap-2 w-[200px]"
>
<PlusCircle className="w-4 h-4" />
Add Firewall Id
</Button>
),
components: {
firewall_ids: (values, set) => (
<InputList
field="firewall_ids"
values={values?.map((val) => val.toString()) ?? []}
set={({ firewall_ids }) =>
set({
firewall_ids: (firewall_ids as string[]).map((id) =>
Number(id)
),
})
}
disabled={disabled}
placeholder="Firewall Id"
/>
),
},
},
{
label: "Volumes",
contentHidden:
((update.volumes ?? config.volumes)?.length ?? 0) === 0,
actions: !disabled && (
<Button
variant="secondary"
onClick={() =>
set({
...update,
volumes: [
...(update.volumes ?? config.volumes ?? []),
newVolume(
(update.volumes ?? config.volumes)?.length ?? 0
),
],
})
}
className="flex items-center gap-2 w-[200px]"
>
<PlusCircle className="w-4 h-4" />
Add Volume
</Button>
),
components: {
volumes: (volumes, set) => {
return (
<div className="w-full flex justify-end">
<div className="flex flex-col gap-4 w-full max-w-[400px]">
{volumes?.map((_, index) => (
<div
key={index}
className="flex items-center justify-between gap-4"
>
<HetznerVolumeDialog
volumes={volumes}
index={index}
set={set}
disabled={disabled}
/>
{!disabled && (
<Button
variant="secondary"
disabled={disabled}
onClick={() =>
set({
volumes: volumes.filter(
(_, i) => i !== index
),
})
}
>
<MinusCircle className="w-4 h-4" />
</Button>
)}
</div>
))}
</div>
</div>
);
},
},
},
{
label: "User Data",
labelHidden: true,
components: {
user_data: (user_data, set) => (
<ConfigItem label="User Data">
<TextUpdateMenu
title="Update User Data"
placeholder="Set User Data"
value={user_data}
onUpdate={(user_data) => set({ user_data })}
triggerClassName="min-w-[300px] max-w-[400px]"
disabled={disabled}
/>
</ConfigItem>
),
},
},
{
label: "SSH Keys",
contentHidden: (update.ssh_keys ?? config.ssh_keys)?.length === 0,
actions: !disabled && (
<Button
variant="secondary"
onClick={() =>
set({
...update,
ssh_keys: [
...(update.ssh_keys ?? config.ssh_keys ?? []),
"",
],
})
}
className="flex items-center gap-2 w-[200px]"
>
<PlusCircle className="w-4 h-4" />
Add SSH Key
</Button>
),
components: {
ssh_keys: (values, set) => (
<InputList
field="ssh_keys"
values={values?.map((val) => val.toString()) ?? []}
set={set}
disabled={disabled}
placeholder="SSH Key ID or Name"
/>
),
},
},
],
}}
/>
);
};
const DatacenterSelector = ({
disabled,
selected,
set,
}: {
disabled: boolean;
selected: Types.HetznerDatacenter | undefined;
set: (value: Partial<Types.HetznerServerTemplateConfig>) => void;
}) => {
return (
<ConfigItem label="Datacenter">
<Select
value={selected}
onValueChange={(value) =>
set({ datacenter: value as Types.HetznerDatacenter })
}
disabled={disabled}
>
<SelectTrigger
className="w-full lg:w-[200px] max-w-[50%]"
disabled={disabled}
>
<SelectValue />
</SelectTrigger>
<SelectContent>
{Object.values(Types.HetznerDatacenter).map((datacenter) => (
<SelectItem key={datacenter} value={datacenter}>
{datacenter}
</SelectItem>
))}
</SelectContent>
</Select>
</ConfigItem>
);
};
const ServerTypeSelector = ({
disabled,
selected,
set,
}: {
disabled: boolean;
selected: Types.HetznerServerType | undefined;
set: (value: Partial<Types.HetznerServerTemplateConfig>) => void;
}) => {
const [open, setOpen] = useState(false);
const [input, setInput] = useState("");
return (
<ConfigItem label="Server Type">
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="secondary"
className="flex gap-2"
disabled={disabled}
>
{selected ?? "Select Server Type"}
{!disabled && <ChevronsUpDown className="w-3 h-3" />}
</Button>
</PopoverTrigger>
<PopoverContent className="w-[300px] max-h-[200px] p-0" align="end">
<Command>
<CommandInput
placeholder="Search Server Types"
className="h-9"
value={input}
onValueChange={setInput}
/>
<CommandList>
<CommandEmpty className="flex justify-evenly items-center">
No Server Types Found
<SearchX className="w-3 h-3" />
</CommandEmpty>
<CommandGroup>
{Object.values(Types.HetznerServerType).map((server_type) => (
<CommandItem
key={server_type}
onSelect={() => {
set({ server_type });
setOpen(false);
}}
className="flex items-center justify-between cursor-pointer"
>
<div className="p-1">{server_type}</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</ConfigItem>
);
};
const HetznerVolumeDialog = ({
volumes,
index,
set,
disabled,
}: {
volumes: Types.HetznerVolumeSpecs[];
index: number;
set: (value: Partial<Types.HetznerServerTemplateConfig>) => void;
disabled?: boolean;
}) => {
const volume = volumes[index];
const [open, setOpen] = useState(false);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Card className="px-3 py-2 hover:bg-accent/50 transition-colors cursor-pointer w-full">
<div
className={cn(
"flex gap-2 text-sm text-nowrap overflow-hidden overflow-ellipsis"
)}
>
<div className="flex gap-2">
<div className="text-muted-foreground">device:</div> {volume.name}
</div>
<div className="flex gap-2">
<div className="text-muted-foreground">size:</div>{" "}
{volume.size_gb} GB
</div>
<div className="flex gap-2">
<div className="text-muted-foreground">type:</div> {volume.format}
</div>
</div>
</Card>
</DialogTrigger>
<DialogContent>
<DialogHeader>Hetzner Volume</DialogHeader>
<div className="flex flex-col gap-4">
{/* Volume Name */}
<div className="flex items-center justify-between w-fill">
<div className="text-nowrap">Volume Name</div>
<Input
value={volume.name}
onChange={(e) =>
set({
volumes: volumes.map((volume, i) =>
i === index ? { ...volume, name: e.target.value } : volume
),
})
}
disabled={disabled}
className="w-[300px]"
/>
</div>
{/* Size GB */}
<div className="flex items-center justify-between w-fill">
<div className="text-nowrap">Size (GB)</div>
<Input
type="number"
value={volume.size_gb}
onChange={(e) =>
set({
volumes: volumes.map((volume, i) =>
i === index
? { ...volume, size_gb: Number(e.target.value || 0) }
: volume
),
})
}
disabled={disabled}
className="w-[300px]"
/>
</div>
{/* Volume Type */}
<div className="flex items-center justify-between w-fill">
<div className="text-nowrap">Volume Format</div>
<Select
value={volume.format}
onValueChange={(value) =>
set({
volumes: volumes.map((volume, i) =>
i === index
? {
...volume,
format: value as Types.HetznerVolumeFormat,
}
: volume
),
})
}
>
<SelectTrigger className="w-[300px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
{Object.values(Types.HetznerVolumeFormat).map((format) => (
<SelectItem key={format} value={format}>
{format}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button onClick={() => setOpen(false)}>Confirm</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
const newVolume: (index: number) => Types.HetznerVolumeSpecs = (index) => {
return {
name: `volume-${index}`,
size_gb: 20,
format: Types.HetznerVolumeFormat.Xfs,
labels: {},
};
};
@@ -1,8 +1,13 @@
import { useRead } from "@lib/hooks";
import { AwsServerTemplateConfig } from "./aws";
import { HetznerServerTemplateConfig } from "./hetzner";
export const ServerTemplateConfig = ({ id }: { id: string }) => {
const config = useRead("GetServerTemplate", { server_template: id }).data
?.config;
if (config?.type === "Aws") return <AwsServerTemplateConfig id={id} />;
};
if (config?.type === "Aws") {
return <AwsServerTemplateConfig id={id} />;
} else if (config?.type === "Hetzner") {
return <HetznerServerTemplateConfig id={id} />;
}
};
@@ -82,6 +82,7 @@ export const ServerTemplateComponents: RequiredResourceComponents = {
<SelectContent>
<SelectGroup>
<SelectItem value="Aws">Aws</SelectItem>
<SelectItem value="Hetzner">Hetzner</SelectItem>
</SelectGroup>
</SelectContent>
</Select>