UI: migrate flash installer page

This commit is contained in:
Johannes Kirschbauer
2024-12-20 18:14:14 +01:00
parent cad55e01bf
commit 2e2df3f09b

View File

@@ -2,10 +2,12 @@ import { callApi } from "@/src/api";
import { Button } from "@/src/components/button"; import { Button } from "@/src/components/button";
import { FileInput } from "@/src/components/FileInput"; import { FileInput } from "@/src/components/FileInput";
import Icon from "@/src/components/icon"; import Icon from "@/src/components/icon";
import { SelectInput } from "@/src/components/SelectInput";
import { TextInput } from "@/src/Form/fields/TextInput";
import { Typography } from "@/src/components/Typography"; import { Typography } from "@/src/components/Typography";
import { Header } from "@/src/layout/header"; import { Header } from "@/src/layout/header";
import { SelectInput } from "@/src/Form/fields/Select";
import { TextInput } from "@/src/Form/fields/TextInput";
import { import {
createForm, createForm,
required, required,
@@ -16,6 +18,8 @@ import {
import { createQuery } from "@tanstack/solid-query"; import { createQuery } from "@tanstack/solid-query";
import { createEffect, createSignal, For, Show } from "solid-js"; import { createEffect, createSignal, For, Show } from "solid-js";
import toast from "solid-toast"; import toast from "solid-toast";
import { FieldLayout } from "@/src/Form/fields/layout";
import { InputLabel } from "@/src/components/inputBase";
interface Wifi extends FieldValues { interface Wifi extends FieldValues {
ssid: string; ssid: string;
@@ -89,9 +93,7 @@ export const Flash = () => {
const deviceQuery = createQuery(() => ({ const deviceQuery = createQuery(() => ({
queryKey: ["block_devices"], queryKey: ["block_devices"],
queryFn: async () => { queryFn: async () => {
const result = await callApi("show_block_devices", { const result = await callApi("show_block_devices", {});
options: {},
});
if (result.status === "error") throw new Error("Failed to fetch data"); if (result.status === "error") throw new Error("Failed to fetch data");
return result.data; return result.data;
}, },
@@ -145,41 +147,43 @@ export const Flash = () => {
const handleSubmit = async (values: FlashFormValues) => { const handleSubmit = async (values: FlashFormValues) => {
console.log("Submit:", values); console.log("Submit:", values);
try { toast.error("Not fully implemented yet");
await callApi("flash_machine", { // Disabled for now. To prevent accidental flashing of local disks
machine: { // try {
name: values.machine.devicePath, // await callApi("flash_machine", {
flake: { // machine: {
loc: values.machine.flake, // name: values.machine.devicePath,
}, // flake: {
}, // loc: values.machine.flake,
mode: "format", // },
disks: [{ name: "main", device: values.disk }], // },
system_config: { // mode: "format",
language: values.language, // disks: [{ name: "main", device: values.disk }],
keymap: values.keymap, // system_config: {
ssh_keys_path: values.sshKeys.map((file) => file.name), // language: values.language,
}, // keymap: values.keymap,
dry_run: false, // ssh_keys_path: values.sshKeys.map((file) => file.name),
write_efi_boot_entries: false, // },
debug: false, // dry_run: false,
}); // write_efi_boot_entries: false,
} catch (error) { // debug: false,
toast.error(`Error could not flash disk: ${error}`); // });
console.error("Error submitting form:", error); // } catch (error) {
} // toast.error(`Error could not flash disk: ${error}`);
// console.error("Error submitting form:", error);
// }
}; };
return ( return (
<> <>
<Header title="Flash installer" /> <Header title="Flash installer" />
<div class="p-4"> <div class="p-4">
<Typography tag="p" hierarchy="body" size="default" color="secondary"> <Typography tag="p" hierarchy="body" size="default" color="primary">
USB Utility image. USB Utility image.
</Typography> </Typography>
<Typography tag="p" hierarchy="body" size="default" color="secondary"> <Typography tag="p" hierarchy="body" size="default" color="secondary">
This will make bootstrapping a new machine easier by providing secure Will make bootstrapping new machines easier by providing secure remote
remote connection to any machine when plugged in. connection to any machine when plugged in.
</Typography> </Typography>
<Form onSubmit={handleSubmit}> <Form onSubmit={handleSubmit}>
<div class="my-4"> <div class="my-4">
@@ -220,37 +224,30 @@ export const Flash = () => {
<Field name="disk" validate={[required("This field is required")]}> <Field name="disk" validate={[required("This field is required")]}>
{(field, props) => ( {(field, props) => (
<SelectInput <SelectInput
topRightLabel={ loading={deviceQuery.isFetching}
<Button
size="s"
variant="light"
onClick={(e) => {
e.preventDefault();
deviceQuery.refetch();
}}
startIcon={<Icon icon="Update" />}
></Button>
}
formStore={formStore}
selectProps={props} selectProps={props}
label="Flash Disk" label="Flash Disk"
value={String(field.value)} labelProps={{
labelAction: (
<Button
class="ml-auto"
variant="ghost"
size="s"
type="button"
startIcon={<Icon icon="Update" />}
onClick={() => deviceQuery.refetch()}
/>
),
}}
value={field.value || ""}
error={field.error} error={field.error}
required required
placeholder="Select a thing where the installer will be flashed to"
options={ options={
<> deviceQuery.data?.blockdevices.map((d) => ({
<option value="" disabled> value: d.path,
Select a disk where the installer will be flashed to label: `${d.path} -- ${d.size} bytes`,
</option> })) || []
<For each={deviceQuery.data?.blockdevices}>
{(device) => (
<option value={device.path}>
{device.path} -- {device.size} bytes
</option>
)}
</For>
</>
} }
/> />
)} )}
@@ -258,94 +255,100 @@ export const Flash = () => {
{/* WiFi Networks */} {/* WiFi Networks */}
<div class="my-4 py-2"> <div class="my-4 py-2">
<h3 class="mb-2 text-lg font-semibold">WiFi Networks</h3> <FieldLayout
<span class="mb-2 text-sm">Add preconfigured networks</span> label={<InputLabel class="mb-4">Networks</InputLabel>}
field={
<>
<Button
type="button"
size="s"
variant="light"
onClick={addWifiNetwork}
startIcon={<Icon icon="Plus" />}
>
WiFi Network
</Button>
</>
}
/>
<For each={wifiNetworks()}> <For each={wifiNetworks()}>
{(network, index) => ( {(network, index) => (
<div class="mb-2 grid grid-cols-7 gap-2"> <div class="flex w-full gap-2">
<Field <div class="mb-2 grid w-full grid-cols-6 gap-2 align-middle">
name={`wifi.${index()}.ssid`} <Field
validate={[required("SSID is required")]} name={`wifi.${index()}.ssid`}
> validate={[required("SSID is required")]}
{(field, props) => ( >
<TextInput {(field, props) => (
inputProps={props}
label="SSID"
value={field.value ?? ""}
error={field.error}
class="col-span-3"
required
/>
)}
</Field>
<Field
name={`wifi.${index()}.password`}
validate={[required("Password is required")]}
>
{(field, props) => (
<div class="relative col-span-3 w-full">
<TextInput <TextInput
inputProps={props} inputProps={props}
type={ label="SSID"
passwordVisibility()[index()] ? "text" : "password"
}
label="Password"
value={field.value ?? ""} value={field.value ?? ""}
error={field.error} error={field.error}
adornment={{ class="col-span-3"
position: "end",
content: (
<Button
variant="light"
type="button"
class="flex justify-center opacity-70"
onClick={() =>
togglePasswordVisibility(index())
}
startIcon={
passwordVisibility()[index()] ? (
<Icon icon="EyeClose" />
) : (
<Icon icon="EyeOpen" />
)
}
></Button>
),
}}
required required
/> />
</div> )}
)} </Field>
</Field> <Field
<div class="col-span-1 self-end"> name={`wifi.${index()}.password`}
<Button validate={[required("Password is required")]}
type="button" >
variant="light" {(field, props) => (
class="h-12" <div class="relative col-span-3 w-full">
onClick={() => removeWifiNetwork(index())} <TextInput
startIcon={<Icon icon="Trash" />} inputProps={{
></Button> ...props,
type: passwordVisibility()[index()]
? "text"
: "password",
}}
label="Password"
value={field.value ?? ""}
error={field.error}
// adornment={{
// position: "end",
// content: (
// <Button
// variant="light"
// type="button"
// class="flex justify-center opacity-70"
// onClick={() =>
// togglePasswordVisibility(index())
// }
// startIcon={
// passwordVisibility()[index()] ? (
// <Icon icon="EyeClose" />
// ) : (
// <Icon icon="EyeOpen" />
// )
// }
// ></Button>
// ),
// }}
required
/>
</div>
)}
</Field>
</div> </div>
<Button
type="button"
variant="light"
class="h-10"
size="s"
onClick={() => removeWifiNetwork(index())}
startIcon={<Icon icon="Trash" />}
></Button>
</div> </div>
)} )}
</For> </For>
<div class="">
<Button
type="button"
size="s"
variant="light"
onClick={addWifiNetwork}
startIcon={<Icon icon="Plus" />}
>
Add WiFi Network
</Button>
</div>
</div> </div>
<div class="collapse collapse-arrow" tabindex="0"> <div class="collapse collapse-arrow" tabindex="0">
<input type="checkbox" /> <input type="checkbox" />
<div class="collapse-title link font-medium "> <div class="collapse-title px-0">
Advanced Settings <InputLabel class="mb-4">Advanced</InputLabel>
</div> </div>
<div class="collapse-content"> <div class="collapse-content">
<Field <Field
@@ -358,9 +361,6 @@ export const Flash = () => {
inputProps={props} inputProps={props}
label="Source (flake URL)" label="Source (flake URL)"
value={String(field.value)} value={String(field.value)}
inlineLabel={
<span class="material-icons">file_download</span>
}
error={field.error} error={field.error}
required required
/> />
@@ -377,21 +377,26 @@ export const Flash = () => {
inputProps={props} inputProps={props}
label="Image Name (attribute name)" label="Image Name (attribute name)"
value={String(field.value)} value={String(field.value)}
inlineLabel={<span class="material-icons">devices</span>}
error={field.error} error={field.error}
required required
/> />
</> </>
)} )}
</Field> </Field>
<div class="my-2 py-2"> <FieldLayout
<span class="text-sm text-neutral-600">Source URL: </span> label={
<span class="text-sm text-neutral-600"> <InputLabel help="Computed reference">Source Url</InputLabel>
{getValue(formStore, "machine.flake") + }
"#" + field={
getValue(formStore, "machine.devicePath")} <InputLabel>
</span> {getValue(formStore, "machine.flake") +
</div> "#" +
getValue(formStore, "machine.devicePath")}
</InputLabel>
}
/>
<hr class="mb-6"></hr>
<Field <Field
name="language" name="language"
validate={[required("This field is required")]} validate={[required("This field is required")]}
@@ -399,22 +404,22 @@ export const Flash = () => {
{(field, props) => ( {(field, props) => (
<> <>
<SelectInput <SelectInput
formStore={formStore}
selectProps={props} selectProps={props}
label="Language" label="Language"
value={String(field.value)} value={String(field.value)}
error={field.error} error={field.error}
required required
options={ loading={langQuery.isLoading}
<> options={[
<option value={"en_US.UTF-8"}>{"en_US.UTF-8"}</option> {
<For each={langQuery.data}> label: "en_US.UTF-8",
{(language) => ( value: "en_US.UTF-8",
<option value={language}>{language}</option> },
)} ...(langQuery.data?.map((lang) => ({
</For> label: lang,
</> value: lang,
} })) || []),
]}
/> />
</> </>
)} )}
@@ -427,22 +432,22 @@ export const Flash = () => {
{(field, props) => ( {(field, props) => (
<> <>
<SelectInput <SelectInput
formStore={formStore}
selectProps={props} selectProps={props}
label="Keymap" label="Keymap"
value={String(field.value)} value={String(field.value)}
error={field.error} error={field.error}
required required
options={ loading={keymapQuery.isLoading}
<> options={[
<option value={"en"}>{"en"}</option> {
<For each={keymapQuery.data}> label: "en",
{(keymap) => ( value: "en",
<option value={keymap}>{keymap}</option> },
)} ...(keymapQuery.data?.map((keymap) => ({
</For> label: keymap,
</> value: keymap,
} })) || []),
]}
/> />
</> </>
)} )}