flash install: fixes form layout

This commit is contained in:
Timo
2025-05-06 14:33:38 +02:00
committed by Johannes Kirschbauer
parent 698a39fafb
commit 7d39d49b30
6 changed files with 169 additions and 241 deletions

View File

@@ -142,91 +142,87 @@ export function SelectInput(props: SelectInputpProps) {
</InputLabel>
}
field={
<>
<InputBase
error={!!props.error}
disabled={props.disabled}
required={props.required}
class="!justify-start"
divRef={setReference}
inputElem={
<button
// TODO: Keyboard acessibililty
// Currently the popover only opens with onClick
// Options are not selectable with keyboard
tabIndex={-1}
disabled={props.disabled}
onClick={() => {
const popover = document.getElementById(_id);
if (popover) {
popover.togglePopover(); // Show or hide the popover
}
}}
type="button"
class="flex w-full items-center gap-2"
formnovalidate
// TODO: Use native popover once Webkit supports it within <form>
// popovertarget={_id}
// popovertargetaction="toggle"
<InputBase
error={!!props.error}
disabled={props.disabled}
required={props.required}
class="!justify-start"
divRef={setReference}
inputElem={
<button
// TODO: Keyboard acessibililty
// Currently the popover only opens with onClick
// Options are not selectable with keyboard
tabIndex={-1}
disabled={props.disabled}
onClick={() => {
const popover = document.getElementById(_id);
if (popover) {
popover.togglePopover(); // Show or hide the popover
}
}}
type="button"
class="flex w-full items-center gap-2"
formnovalidate
// TODO: Use native popover once Webkit supports it within <form>
// popovertarget={_id}
// popovertargetaction="toggle"
>
<Show
when={props.adornment && props.adornment.position === "start"}
>
{props.adornment?.content}
</Show>
{props.inlineLabel}
<div class="flex cursor-default flex-row gap-2">
<Show
when={
props.adornment && props.adornment.position === "start"
getValues() &&
getValues.length !== 1 &&
getValues()[0] !== ""
}
fallback={props.placeholder}
>
{props.adornment?.content}
<For each={getValues()} fallback={"Select"}>
{(item) => (
<div class="rounded-xl bg-slate-800 px-4 py-1 text-sm text-white">
{item}
<Show when={props.multiple}>
<button
class=""
type="button"
onClick={(_e) => {
// @ts-expect-error: fieldName is not known ahead of time
props.selectProps.onInput({
currentTarget: {
options: getValues()
.filter((o) => o !== item)
.map((value) => ({
value,
selected: true,
disabled: false,
})),
},
});
}}
>
X
</button>
</Show>
</div>
)}
</For>
</Show>
{props.inlineLabel}
<div class="flex cursor-default flex-row gap-2">
<Show
when={
getValues() &&
getValues.length !== 1 &&
getValues()[0] !== ""
}
fallback={props.placeholder}
>
<For each={getValues()} fallback={"Select"}>
{(item) => (
<div class="rounded-xl bg-slate-800 px-4 py-1 text-sm text-white">
{item}
<Show when={props.multiple}>
<button
class=""
type="button"
onClick={(_e) => {
// @ts-expect-error: fieldName is not known ahead of time
props.selectProps.onInput({
currentTarget: {
options: getValues()
.filter((o) => o !== item)
.map((value) => ({
value,
selected: true,
disabled: false,
})),
},
});
}}
>
X
</button>
</Show>
</div>
)}
</For>
</Show>
</div>
<Show
when={props.adornment && props.adornment.position === "end"}
>
{props.adornment?.content}
</Show>
<Icon icon="CaretDown" class="ml-auto mr-2"></Icon>
</button>
}
/>
</>
</div>
<Show
when={props.adornment && props.adornment.position === "end"}
>
{props.adornment?.content}
</Show>
<Icon size={12} icon="CaretDown" class="ml-auto mr-2" />
</button>
}
/>
}
/>

View File

@@ -18,7 +18,7 @@ export const FieldLayout = (props: LayoutProps) => {
class={cx("grid grid-cols-10 items-center", intern.class)}
{...divProps}
>
<label class="col-span-5">{props.label}</label>
<div class="col-span-5 flex items-center">{props.label}</div>
<div class="col-span-5">{props.field}</div>
{props.error && <span class="col-span-full">{props.error}</span>}
</div>

View File

@@ -21,7 +21,7 @@ export default function Accordion(props: AccordionProps) {
fallback={
<Button
endIcon={<Icon size={12} icon={"CaretDown"} />}
variant="light"
variant="ghost"
size="s"
>
{props.title}
@@ -30,7 +30,7 @@ export default function Accordion(props: AccordionProps) {
>
<Button
endIcon={<Icon size={12} icon={"CaretUp"} />}
variant="dark"
variant="ghost"
size="s"
>
{props.title}

View File

@@ -22,6 +22,8 @@ import toast from "solid-toast";
import { FieldLayout } from "@/src/Form/fields/layout";
import { InputLabel } from "@/src/components/inputBase";
import { Modal } from "@/src/components/modal";
import Fieldset from "@/src/Form/fieldset";
import Accordion from "@/src/components/accordion";
interface Wifi extends FieldValues {
ssid: string;
@@ -233,16 +235,23 @@ export const Flash = () => {
</div>
</div>
</Modal>
<div class="p-4">
<Typography tag="p" hierarchy="body" size="default" color="primary">
<div class="w-full self-stretch p-8">
{/* <Typography tag="p" hierarchy="body" size="default" color="primary">
USB Utility image.
</Typography>
<Typography tag="p" hierarchy="body" size="default" color="secondary">
Will make bootstrapping new machines easier by providing secure remote
connection to any machine when plugged in.
</Typography>
<Form onSubmit={handleSubmit}>
<div class="my-4">
</Typography> */}
<Form
onSubmit={handleSubmit}
class="mx-auto flex w-full max-w-2xl flex-col gap-y-6"
>
<Fieldset legend="Authorized SSH Keys">
<Typography hierarchy="body" size="s" weight="medium">
Provide your SSH public key. For secure and passwordless SSH
connections.
</Typography>
<Field name="sshKeys" type="File[]">
{(field, props) => (
<>
@@ -267,146 +276,72 @@ export const Flash = () => {
}}
value={field.value}
error={field.error}
helperText="Provide your SSH public key. For secure and passwordless SSH connections."
label="Authorized SSH Keys"
//helperText="Provide your SSH public key. For secure and passwordless SSH connections."
//label="Authorized SSH Keys"
multiple
required
/>
</>
)}
</Field>
</div>
<Field name="disk" validate={[required("This field is required")]}>
{(field, props) => (
<SelectInput
loading={deviceQuery.isFetching}
selectProps={props}
label="Flash Disk"
labelProps={{
labelAction: (
<Button
disabled={isFlashing()}
class="ml-auto"
variant="ghost"
size="s"
type="button"
startIcon={<Icon icon="Update" />}
onClick={() => deviceQuery.refetch()}
/>
),
}}
value={field.value || ""}
error={field.error}
required
placeholder="Select a drive where the clan-installer will be flashed to"
options={
deviceQuery.data?.blockdevices.map((d) => ({
value: d.path,
label: `${d.path} -- ${d.size} bytes`,
})) || []
}
/>
)}
</Field>
{/* WiFi Networks */}
<div class="my-4 py-2">
<FieldLayout
label={<InputLabel class="mb-4">Networks</InputLabel>}
field={
</Fieldset>
<Fieldset legend="General">
<Field name="disk" validate={[required("This field is required")]}>
{(field, props) => (
<>
<SelectInput
loading={deviceQuery.isFetching}
selectProps={props}
label="Flash Disk"
labelProps={{
labelAction: (
<Button
disabled={isFlashing()}
class="ml-auto"
variant="ghost"
size="s"
type="button"
startIcon={<Icon icon="Update" />}
onClick={() => deviceQuery.refetch()}
/>
),
}}
value={field.value || ""}
error={field.error}
required
placeholder="Select a drive"
options={
deviceQuery.data?.blockdevices.map((d) => ({
value: d.path,
label: `${d.path} -- ${d.size} bytes`,
})) || []
}
/>
</>
)}
</Field>
</Fieldset>
<Fieldset legend="Network Settings">
<FieldLayout
label={<InputLabel>Networks</InputLabel>}
field={
<div class="w-full flex justify-end">
<Button
type="button"
size="s"
variant="light"
onClick={addWifiNetwork}
startIcon={<Icon icon="Plus" />}
startIcon={<Icon size={12} icon="Plus" />}
>
WiFi Network
</Button>
</>
</div>
}
/>
<For each={wifiNetworks()}>
{(network, index) => (
<div class="flex w-full gap-2">
<div class="mb-2 grid w-full grid-cols-6 gap-2 align-middle">
<Field
name={`wifi.${index()}.ssid`}
validate={[required("SSID is required")]}
>
{(field, props) => (
<TextInput
inputProps={props}
label="SSID"
value={field.value ?? ""}
error={field.error}
class="col-span-full "
required
/>
)}
</Field>
<Field
name={`wifi.${index()}.password`}
validate={[required("Password is required")]}
>
{(field, props) => (
<TextInput
class="col-span-full"
inputProps={{
...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
/>
)}
</Field>
</div>
<Button
type="button"
variant="light"
class="h-10"
size="s"
onClick={() => removeWifiNetwork(index())}
startIcon={<Icon icon="Trash" />}
></Button>
</div>
)}
</For>
</div>
</Fieldset>
<div class=" " tabindex="0">
<input type="checkbox" />
<div class=" px-0">
<InputLabel class="mb-4">Advanced</InputLabel>
</div>
<div class="">
<Accordion title="Advanced">
<Fieldset>
<Field
name="machine.flake"
validate={[required("This field is required")]}
@@ -508,10 +443,8 @@ export const Flash = () => {
</>
)}
</Field>
</div>
</div>
<hr></hr>
</Fieldset>
</Accordion>
<div class="mt-2 flex justify-end pt-2">
<Button
class="self-end"

View File

@@ -11,7 +11,6 @@ import { Match, Switch } from "solid-js";
import toast from "solid-toast";
import { MachineAvatar } from "./avatar";
import { DynForm } from "@/src/Form/form";
import { Typography } from "@/src/components/Typography";
import Fieldset from "@/src/Form/fieldset";
import Accordion from "@/src/components/accordion";

View File

@@ -1,9 +1,4 @@
import { callApi, SuccessData } from "@/src/api";
import { activeURI } from "@/src/App";
import { Button } from "@/src/components/button";
import Icon from "@/src/components/icon";
import { TextInput } from "@/src/Form/fields/TextInput";
import {
createForm,
FieldValues,
@@ -14,6 +9,12 @@ import {
import { useNavigate, useParams, useSearchParams } from "@solidjs/router";
import { createQuery, useQueryClient } from "@tanstack/solid-query";
import { createEffect, createSignal, For, Match, Show, Switch } from "solid-js";
import { activeURI } from "@/src/App";
import { Button } from "@/src/components/button";
import Icon from "@/src/components/icon";
import { TextInput } from "@/src/Form/fields/TextInput";
import Accordion from "@/src/components/accordion";
import toast from "solid-toast";
import { MachineAvatar } from "./avatar";
import { Header } from "@/src/layout/header";
@@ -550,7 +551,7 @@ const MachineForm = (props: MachineDetailsProps) => {
onClick={() => {
setInstallModalOpen(true);
}}
endIcon={<Icon icon="Flash" />}
endIcon={<Icon size={14} icon="Flash" />}
>
Install
</Button>
@@ -559,7 +560,7 @@ const MachineForm = (props: MachineDetailsProps) => {
class="w-full"
size="s"
onClick={() => handleUpdate()}
endIcon={<Icon icon="Update" />}
endIcon={<Icon size={12} icon="Update" />}
>
Update
</Button>
@@ -636,6 +637,7 @@ const MachineForm = (props: MachineDetailsProps) => {
/>
)}
</Field>
<hr />
<Field name="disk_schema.schema_name">
{(field, props) => (
<>
@@ -646,27 +648,25 @@ const MachineForm = (props: MachineDetailsProps) => {
</>
)}
</Field>
<div class=" col-span-full" tabindex="0">
<input type="checkbox" />
<div class=" px-0 text-xl ">Connection Settings</div>
<div class="">
<Field name="machine.deploy.targetHost">
{(field, props) => (
<TextInput
inputProps={props}
label="Target Host"
value={field.value ?? ""}
error={field.error}
class="col-span-2"
required
/>
)}
</Field>
</div>
</div>
</Fieldset>
<Accordion title="Connection Settings">
<Fieldset>
<Field name="machine.deploy.targetHost">
{(field, props) => (
<TextInput
inputProps={props}
label="Target Host"
value={field.value ?? ""}
error={field.error}
class="col-span-2"
required
/>
)}
</Field>
</Fieldset>
</Accordion>
{
<footer class="flex justify-end gap-y-3 border-t border-secondary-200 pt-5">
<Button