finish configure server templates

This commit is contained in:
mbecker20
2024-05-05 19:08:47 -07:00
parent c44dc1c6f6
commit c12c74005e
4 changed files with 327 additions and 68 deletions
+19 -10
View File
@@ -3029,6 +3029,13 @@ export interface ServerHealth {
disks: Record<string, SeverityLevel>;
}
export enum AwsVolumeType {
Gp2 = "gp2",
Gp3 = "gp3",
Io1 = "io1",
Io2 = "io2",
}
/**
* For information on AWS volumes, see
* `<https://docs.aws.amazon.com/ebs/latest/userguide/ebs-volume-types.html>`.
@@ -3038,12 +3045,12 @@ export interface AwsVolume {
device_name: string;
/** The size of the volume in GB */
size_gb: number;
/** The type of volume, eg gp2, gp3, io1 */
volume_type?: string;
/** The iops of the volume, or AWS default. */
iops?: number;
/** The throughput of the volume, or AWS default. */
throughput?: number;
/** The type of volume. Options: gp2, gp3, io1, io2. */
volume_type: AwsVolumeType;
/** The iops of the volume, or 0 for AWS default. */
iops: number;
/** The throughput of the volume, or 0 for AWS default. */
throughput: number;
}
/** Aws EC2 instance config. */
@@ -3052,14 +3059,10 @@ export interface AwsServerTemplateConfig {
region: string;
/** The instance type to launch, eg. c5.2xlarge */
instance_type: string;
/** Specify the EBS volumes to attach. */
volumes: AwsVolume[];
/** Specify the ami id to use. Must be set up to start the periphery binary on startup. */
ami_id: string;
/** The subnet to assign to the instance. */
subnet_id: string;
/** The security groups to give to the instance. */
security_group_ids: string[];
/** The key pair name to give to the instance in case SSH access required. */
key_pair_name: string;
/**
@@ -3077,6 +3080,12 @@ export interface AwsServerTemplateConfig {
* Default: `8120`
*/
port: number;
/** The user data to deploy the instance with. */
user_data?: string;
/** The security groups to give to the instance. */
security_group_ids?: string[];
/** Specify the EBS volumes to attach. */
volumes: AwsVolume[];
}
export type AuthRequest =
@@ -1,58 +0,0 @@
import { Config } from "@components/config";
import { InputList } from "@components/config/util";
import { useRead, useWrite } from "@lib/hooks";
import { Types } from "@monitor/client";
import { useState } from "react";
export const ServerTemplateConfig = ({ id }: { id: string }) => {
const config = useRead("GetServerTemplate", { server_template: id }).data
?.config;
if (config?.type === "Aws") return <AwsServerTemplateConfig id={id} />;
};
export const AwsServerTemplateConfig = ({ id }: { id: string }) => {
const perms = useRead("GetPermissionLevel", {
target: { type: "ServerTemplate", id },
}).data;
const config = useRead("GetServerTemplate", { server_template: id }).data
?.config;
const [update, set] = useState<Partial<Types.AwsServerTemplateConfig>>({});
const { mutateAsync } = useWrite("UpdateServerTemplate");
if (!config) return null;
const disabled = perms !== Types.PermissionLevel.Write;
return (
<Config
disabled={disabled}
config={config.params as Types.AwsServerTemplateConfig}
update={update}
set={set}
onSave={async () => {
await mutateAsync({ id, config: { type: "Aws", params: update } });
}}
components={{
general: {
general: {
region: true,
instance_type: true,
ami_id: true,
subnet_id: true,
key_pair_name: true,
assign_public_ip: true,
use_public_ip: true,
security_group_ids: (values, set) => (
<InputList
field="security_group_ids"
values={values}
set={set}
disabled={disabled}
/>
),
port: true,
},
},
}}
/>
);
};
@@ -0,0 +1,300 @@
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 {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTrigger,
} from "@ui/dialog";
import { Input } from "@ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@ui/select";
import { MinusCircle } from "lucide-react";
import { useState } from "react";
export const AwsServerTemplateConfig = ({ id }: { id: string }) => {
const perms = useRead("GetPermissionLevel", {
target: { type: "ServerTemplate", id },
}).data;
const config = useRead("GetServerTemplate", { server_template: id }).data
?.config;
const [update, set] = useState<Partial<Types.AwsServerTemplateConfig>>({});
const { mutateAsync } = useWrite("UpdateServerTemplate");
if (!config) return null;
const disabled = perms !== Types.PermissionLevel.Write;
return (
<Config
disabled={disabled}
config={config.params as Types.AwsServerTemplateConfig}
update={update}
set={set}
onSave={async () => {
await mutateAsync({ id, config: { type: "Aws", params: update } });
}}
components={{
general: {
general: {
region: true,
instance_type: true,
ami_id: true,
subnet_id: true,
key_pair_name: true,
assign_public_ip: true,
use_public_ip: true,
port: true,
security_group_ids: (values, set) => (
<InputList
field="security_group_ids"
values={values!}
set={set}
disabled={disabled}
/>
),
volumes: (volumes, set) => {
return (
<ConfigItem label="EBS Volumes" className="items-start">
<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"
>
<AwsVolumeDialog
volumes={volumes}
index={index}
set={set}
disabled={disabled}
/>
{!disabled && (
<Button
variant="outline"
disabled={disabled}
onClick={() =>
set({
volumes: volumes.filter((_, i) => i !== index),
})
}
>
<MinusCircle className="w-4 h-4" />
</Button>
)}
</div>
))}
{!disabled && (
<Button
variant="outline"
onClick={() =>
set({
volumes: [...volumes, newVolume(volumes.length)],
})
}
>
Add Volume
</Button>
)}
</div>
</ConfigItem>
);
},
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]"
/>
</ConfigItem>
),
},
},
}}
/>
);
};
const AwsVolumeDialog = ({
volumes,
index,
set,
disabled,
}: {
volumes: Types.AwsVolume[];
index: number;
set: (value: Partial<Types.AwsServerTemplateConfig>) => 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.device_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.volume_type}
</div>
</div>
</Card>
</DialogTrigger>
<DialogContent>
<DialogHeader>AWS Volume</DialogHeader>
<div className="flex flex-col gap-4">
{/* Device Name */}
<div className="flex items-center justify-between w-fill">
<div className="text-nowrap">Device Name</div>
<Input
value={volume.device_name}
onChange={(e) =>
set({
volumes: volumes.map((volume, i) =>
i === index
? { ...volume, device_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 Type</div>
<Select
value={volume.volume_type}
onValueChange={(value) =>
set({
volumes: volumes.map((volume, i) =>
i === index
? { ...volume, volume_type: value as Types.AwsVolumeType }
: volume
),
})
}
>
<SelectTrigger className="w-[300px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
{Object.entries(Types.AwsVolumeType).map(([name, item]) => (
<SelectItem key={name} value={item}>
{name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* IOPS */}
<div className="flex items-center justify-between w-fill">
<div className="text-nowrap">Iops</div>
<Input
type="number"
placeholder="Set Custom Iops"
value={volume.iops ? volume.iops : undefined}
onChange={(e) =>
set({
volumes: volumes.map((volume, i) =>
i === index
? { ...volume, iops: Number(e.target.value || 0) }
: volume
),
})
}
disabled={disabled}
className="w-[300px]"
/>
</div>
{/* Throughput */}
<div className="flex items-center justify-between w-fill">
<div className="text-nowrap">Throughput</div>
<Input
type="number"
placeholder="Set Custom Throughput"
value={volume.throughput ? volume.throughput : undefined}
onChange={(e) =>
set({
volumes: volumes.map((volume, i) =>
i === index
? { ...volume, throughput: Number(e.target.value || 0) }
: volume
),
})
}
disabled={disabled}
className="w-[300px]"
/>
</div>
</div>
<DialogFooter>
<Button onClick={() => setOpen(false)}>Confirm</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
const device_letters = ["b", "c", "d", "e", "f"];
const newVolume: (index: number) => Types.AwsVolume = (index) => {
const device_name =
index === 0 ? "/dev/sda1" : `/dev/sd${device_letters[index - 1]}`;
return {
device_name,
size_gb: 20,
volume_type: Types.AwsVolumeType.Gp2,
iops: 0,
throughput: 0,
};
};
@@ -0,0 +1,8 @@
import { useRead } from "@lib/hooks";
import { AwsServerTemplateConfig } from "./aws";
export const ServerTemplateConfig = ({ id }: { id: string }) => {
const config = useRead("GetServerTemplate", { server_template: id }).data
?.config;
if (config?.type === "Aws") return <AwsServerTemplateConfig id={id} />;
};