Clan-app: Flash improve form & file input
This commit is contained in:
91
pkgs/webview-ui/app/src/components/FileInput.tsx
Normal file
91
pkgs/webview-ui/app/src/components/FileInput.tsx
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import cx from "classnames";
|
||||||
|
import { createMemo, JSX, Show, splitProps } from "solid-js";
|
||||||
|
|
||||||
|
interface FileInputProps {
|
||||||
|
ref: (element: HTMLInputElement) => void;
|
||||||
|
name: string;
|
||||||
|
value?: File[] | File;
|
||||||
|
onInput: JSX.EventHandler<HTMLInputElement, InputEvent>;
|
||||||
|
onClick: JSX.EventHandler<HTMLInputElement, Event>;
|
||||||
|
onChange: JSX.EventHandler<HTMLInputElement, Event>;
|
||||||
|
onBlur: JSX.EventHandler<HTMLInputElement, FocusEvent>;
|
||||||
|
accept?: string;
|
||||||
|
required?: boolean;
|
||||||
|
multiple?: boolean;
|
||||||
|
class?: string;
|
||||||
|
label?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* File input field that users can click or drag files into. Various
|
||||||
|
* decorations can be displayed in or around the field to communicate the entry
|
||||||
|
* requirements.
|
||||||
|
*/
|
||||||
|
export function FileInput(props: FileInputProps) {
|
||||||
|
// Split input element props
|
||||||
|
const [, inputProps] = splitProps(props, [
|
||||||
|
"class",
|
||||||
|
"value",
|
||||||
|
"label",
|
||||||
|
"error",
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create file list
|
||||||
|
const getFiles = createMemo(() =>
|
||||||
|
props.value
|
||||||
|
? Array.isArray(props.value)
|
||||||
|
? props.value
|
||||||
|
: [props.value]
|
||||||
|
: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class={cx("form-control w-full", props.class)}>
|
||||||
|
<div class="label">
|
||||||
|
<span
|
||||||
|
class="label-text block"
|
||||||
|
classList={{
|
||||||
|
"after:ml-0.5 after:text-primary after:content-['*']":
|
||||||
|
props.required,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class={cx(
|
||||||
|
"relative flex min-h-[96px] w-full items-center justify-center rounded-2xl border-[3px] border-dashed p-8 text-center focus-within:ring-4 md:min-h-[112px] md:text-lg lg:min-h-[128px] lg:p-10 lg:text-xl",
|
||||||
|
!getFiles().length && "text-slate-500",
|
||||||
|
props.error
|
||||||
|
? "border-red-500/25 focus-within:border-red-500/50 focus-within:ring-red-500/10 hover:border-red-500/40 dark:border-red-400/25 dark:focus-within:border-red-400/50 dark:focus-within:ring-red-400/10 dark:hover:border-red-400/40"
|
||||||
|
: "border-slate-200 focus-within:border-sky-500/50 focus-within:ring-sky-500/10 hover:border-slate-300 dark:border-slate-800 dark:focus-within:border-sky-400/50 dark:focus-within:ring-sky-400/10 dark:hover:border-slate-700",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Show
|
||||||
|
when={getFiles().length}
|
||||||
|
fallback={<>Click to select file{props.multiple && "s"}</>}
|
||||||
|
>
|
||||||
|
Selected file{props.multiple && "s"}:{" "}
|
||||||
|
{getFiles()
|
||||||
|
.map(({ name }) => name)
|
||||||
|
.join(", ")}
|
||||||
|
</Show>
|
||||||
|
<input
|
||||||
|
{...inputProps}
|
||||||
|
// Disable drag n drop
|
||||||
|
onDrop={(e) => e.preventDefault()}
|
||||||
|
class="absolute size-full cursor-pointer opacity-0"
|
||||||
|
type="file"
|
||||||
|
id={props.name}
|
||||||
|
aria-invalid={!!props.error}
|
||||||
|
aria-errormessage={`${props.name}-error`}
|
||||||
|
/>
|
||||||
|
{props.error && (
|
||||||
|
<span class="label-text-alt font-bold text-error">{props.error}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
48
pkgs/webview-ui/app/src/components/SelectInput.tsx
Normal file
48
pkgs/webview-ui/app/src/components/SelectInput.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { FieldValues, FormStore, ResponseData } from "@modular-forms/solid";
|
||||||
|
import { type JSX } from "solid-js";
|
||||||
|
|
||||||
|
interface SelectInputProps<T extends FieldValues, R extends ResponseData> {
|
||||||
|
formStore: FormStore<T, R>;
|
||||||
|
value: string;
|
||||||
|
options: JSX.Element;
|
||||||
|
selectProps: JSX.HTMLAttributes<HTMLSelectElement>;
|
||||||
|
label: JSX.Element;
|
||||||
|
error?: string;
|
||||||
|
required?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SelectInput<T extends FieldValues, R extends ResponseData>(
|
||||||
|
props: SelectInputProps<T, R>,
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
class="form-control w-full"
|
||||||
|
aria-disabled={props.formStore.submitting}
|
||||||
|
>
|
||||||
|
<div class="label">
|
||||||
|
<span
|
||||||
|
class="label-text block"
|
||||||
|
classList={{
|
||||||
|
"after:ml-0.5 after:text-primary after:content-['*']":
|
||||||
|
props.required,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<select
|
||||||
|
{...props.selectProps}
|
||||||
|
required={props.required}
|
||||||
|
class="select select-bordered w-full"
|
||||||
|
value={props.value}
|
||||||
|
>
|
||||||
|
{props.options}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
{props.error && (
|
||||||
|
<span class="label-text-alt font-bold text-error">{props.error}</span>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
54
pkgs/webview-ui/app/src/components/TextInput.tsx
Normal file
54
pkgs/webview-ui/app/src/components/TextInput.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { FieldValues, FormStore, ResponseData } from "@modular-forms/solid";
|
||||||
|
import { type JSX } from "solid-js";
|
||||||
|
|
||||||
|
interface TextInputProps<T extends FieldValues, R extends ResponseData> {
|
||||||
|
formStore: FormStore<T, R>;
|
||||||
|
value: string;
|
||||||
|
inputProps: JSX.HTMLAttributes<HTMLInputElement>;
|
||||||
|
label: JSX.Element;
|
||||||
|
error?: string;
|
||||||
|
required?: boolean;
|
||||||
|
inlineLabel?: JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TextInput<T extends FieldValues, R extends ResponseData>(
|
||||||
|
props: TextInputProps<T, R>,
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
class="form-control w-full"
|
||||||
|
aria-disabled={props.formStore.submitting}
|
||||||
|
>
|
||||||
|
<div class="label">
|
||||||
|
<span
|
||||||
|
class="label-text block"
|
||||||
|
classList={{
|
||||||
|
"after:ml-0.5 after:text-primary after:content-['*']":
|
||||||
|
props.required,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input input-bordered flex items-center gap-2">
|
||||||
|
{props.inlineLabel}
|
||||||
|
<input
|
||||||
|
{...props.inputProps}
|
||||||
|
value={props.value}
|
||||||
|
type="text"
|
||||||
|
class="grow"
|
||||||
|
classList={{
|
||||||
|
"input-disabled": props.formStore.submitting,
|
||||||
|
}}
|
||||||
|
placeholder="name"
|
||||||
|
required
|
||||||
|
disabled={props.formStore.submitting}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{props.error && (
|
||||||
|
<span class="label-text-alt font-bold text-error">{props.error}</span>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
import { callApi, OperationResponse } from "@/src/api";
|
import { callApi, OperationResponse } from "@/src/api";
|
||||||
import {
|
import { FileInput } from "@/src/components/FileInput";
|
||||||
createForm,
|
import { SelectInput } from "@/src/components/SelectInput";
|
||||||
required,
|
import { TextInput } from "@/src/components/TextInput";
|
||||||
FieldValues,
|
import { createForm, required, FieldValues } from "@modular-forms/solid";
|
||||||
setValue,
|
|
||||||
} from "@modular-forms/solid";
|
|
||||||
import { createQuery } from "@tanstack/solid-query";
|
import { createQuery } from "@tanstack/solid-query";
|
||||||
import { createEffect, createSignal, For } from "solid-js";
|
import { For } from "solid-js";
|
||||||
|
import toast from "solid-toast";
|
||||||
|
|
||||||
interface FlashFormValues extends FieldValues {
|
interface FlashFormValues extends FieldValues {
|
||||||
machine: {
|
machine: {
|
||||||
@@ -16,58 +15,32 @@ interface FlashFormValues extends FieldValues {
|
|||||||
disk: string;
|
disk: string;
|
||||||
language: string;
|
language: string;
|
||||||
keymap: string;
|
keymap: string;
|
||||||
sshKeys: string[];
|
sshKeys: File[];
|
||||||
}
|
}
|
||||||
|
|
||||||
type BlockDevices = Extract<
|
|
||||||
OperationResponse<"show_block_devices">,
|
|
||||||
{ status: "success" }
|
|
||||||
>["data"]["blockdevices"];
|
|
||||||
|
|
||||||
export const Flash = () => {
|
export const Flash = () => {
|
||||||
const [formStore, { Form, Field }] = createForm<FlashFormValues>({});
|
const [formStore, { Form, Field }] = createForm<FlashFormValues>({
|
||||||
const [sshKeys, setSshKeys] = createSignal<string[]>([]);
|
initialValues: {
|
||||||
const [isFlashing, setIsFlashing] = createSignal(false);
|
machine: {
|
||||||
|
flake: "git+https://git.clan.lol/clan/clan-core",
|
||||||
const selectSshPubkey = async () => {
|
devicePath: "flash-installer",
|
||||||
try {
|
},
|
||||||
const loc = await callApi("open_file", {
|
language: "en_US.UTF-8",
|
||||||
file_request: {
|
keymap: "en",
|
||||||
title: "Select SSH Key",
|
},
|
||||||
mode: "open_multiple_files",
|
|
||||||
filters: { patterns: ["*.pub"] },
|
|
||||||
initial_folder: "~/.ssh",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
console.log({ loc }, loc.status);
|
|
||||||
if (loc.status === "success" && loc.data) {
|
|
||||||
setSshKeys(loc.data);
|
|
||||||
return loc.data;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
//
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create an effect that updates the form when externalUsername changes
|
|
||||||
createEffect(() => {
|
|
||||||
const newSshKeys = sshKeys();
|
|
||||||
if (newSshKeys) {
|
|
||||||
setValue(formStore, "sshKeys", newSshKeys);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: devices, isFetching } = 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", {});
|
||||||
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;
|
||||||
},
|
},
|
||||||
staleTime: 1000 * 60 * 2, // 1 minutes
|
staleTime: 1000 * 60 * 1, // 1 minutes
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { data: keymaps, isFetching: isFetchingKeymaps } = createQuery(() => ({
|
const keymapQuery = createQuery(() => ({
|
||||||
queryKey: ["list_keymaps"],
|
queryKey: ["list_keymaps"],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const result = await callApi("list_possible_keymaps", {});
|
const result = await callApi("list_possible_keymaps", {});
|
||||||
@@ -77,20 +50,46 @@ export const Flash = () => {
|
|||||||
staleTime: 1000 * 60 * 15, // 15 minutes
|
staleTime: 1000 * 60 * 15, // 15 minutes
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { data: languages, isFetching: isFetchingLanguages } = createQuery(
|
const langQuery = createQuery(() => ({
|
||||||
() => ({
|
queryKey: ["list_languages"],
|
||||||
queryKey: ["list_languages"],
|
queryFn: async () => {
|
||||||
queryFn: async () => {
|
const result = await callApi("list_possible_languages", {});
|
||||||
const result = await callApi("list_possible_languages", {});
|
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;
|
},
|
||||||
|
staleTime: 1000 * 60 * 15, // 15 minutes
|
||||||
|
}));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the custom file dialog
|
||||||
|
* Returns a native FileList to allow interaction with the native input type="file"
|
||||||
|
*/
|
||||||
|
const selectSshKeys = async (): Promise<FileList> => {
|
||||||
|
const dataTransfer = new DataTransfer();
|
||||||
|
|
||||||
|
const response = await callApi("open_file", {
|
||||||
|
file_request: {
|
||||||
|
title: "Select SSH Key",
|
||||||
|
mode: "open_multiple_files",
|
||||||
|
filters: { patterns: ["*.pub"] },
|
||||||
|
initial_folder: "~/.ssh",
|
||||||
},
|
},
|
||||||
staleTime: 1000 * 60 * 15, // 15 minutes
|
});
|
||||||
}),
|
if (response.status === "success" && response.data) {
|
||||||
);
|
// Add synthetic files to the DataTransfer object
|
||||||
|
// FileList cannot be instantiated directly.
|
||||||
|
response.data.forEach((filename) => {
|
||||||
|
dataTransfer.items.add(new File([], filename));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return dataTransfer.files;
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = async (values: FlashFormValues) => {
|
const handleSubmit = async (values: FlashFormValues) => {
|
||||||
setIsFlashing(true);
|
console.log(
|
||||||
|
"Submit SSH Keys:",
|
||||||
|
values.sshKeys.map((file) => file.name),
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
await callApi("flash_machine", {
|
await callApi("flash_machine", {
|
||||||
machine: {
|
machine: {
|
||||||
@@ -104,16 +103,15 @@ export const Flash = () => {
|
|||||||
system_config: {
|
system_config: {
|
||||||
language: values.language,
|
language: values.language,
|
||||||
keymap: values.keymap,
|
keymap: values.keymap,
|
||||||
ssh_keys_path: values.sshKeys,
|
ssh_keys_path: values.sshKeys.map((file) => file.name),
|
||||||
},
|
},
|
||||||
dry_run: false,
|
dry_run: false,
|
||||||
write_efi_boot_entries: false,
|
write_efi_boot_entries: false,
|
||||||
debug: false,
|
debug: false,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
toast.error(`Error could not flash disk: ${error}`);
|
||||||
console.error("Error submitting form:", error);
|
console.error("Error submitting form:", error);
|
||||||
} finally {
|
|
||||||
setIsFlashing(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -126,24 +124,15 @@ export const Flash = () => {
|
|||||||
>
|
>
|
||||||
{(field, props) => (
|
{(field, props) => (
|
||||||
<>
|
<>
|
||||||
<label class="input input-bordered flex items-center gap-2">
|
<TextInput
|
||||||
<span class="material-icons">file_download</span>
|
formStore={formStore}
|
||||||
<input
|
inputProps={props}
|
||||||
type="text"
|
label="Installer (flake URL)"
|
||||||
class="grow"
|
value={String(field.value)}
|
||||||
//placeholder="machine.flake"
|
inlineLabel={<span class="material-icons">file_download</span>}
|
||||||
value="git+https://git.clan.lol/clan/clan-core"
|
error={field.error}
|
||||||
required
|
required
|
||||||
{...props}
|
/>
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<div class="label">
|
|
||||||
{field.error && (
|
|
||||||
<span class="label-text-alt font-bold text-error">
|
|
||||||
{field.error}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
@@ -153,155 +142,125 @@ export const Flash = () => {
|
|||||||
>
|
>
|
||||||
{(field, props) => (
|
{(field, props) => (
|
||||||
<>
|
<>
|
||||||
<label class="input input-bordered flex items-center gap-2">
|
<TextInput
|
||||||
<span class="material-icons">devices</span>
|
formStore={formStore}
|
||||||
<input
|
inputProps={props}
|
||||||
type="text"
|
label="Installer Image (attribute name)"
|
||||||
class="grow"
|
value={String(field.value)}
|
||||||
value="flash-installer"
|
inlineLabel={<span class="material-icons">devices</span>}
|
||||||
required
|
error={field.error}
|
||||||
{...props}
|
required
|
||||||
/>
|
/>
|
||||||
</label>
|
|
||||||
<div class="label">
|
|
||||||
{field.error && (
|
|
||||||
<span class="label-text-alt font-bold text-error">
|
|
||||||
{field.error}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
<Field name="disk" validate={[required("This field is required")]}>
|
<Field name="disk" validate={[required("This field is required")]}>
|
||||||
{(field, props) => (
|
{(field, props) => (
|
||||||
<>
|
<SelectInput
|
||||||
<label class="form-control input-bordered flex w-full items-center gap-2">
|
formStore={formStore}
|
||||||
<select
|
selectProps={props}
|
||||||
required
|
label="Flash Disk"
|
||||||
class="select select-bordered w-full"
|
value={String(field.value)}
|
||||||
{...props}
|
error={field.error}
|
||||||
>
|
required
|
||||||
|
options={
|
||||||
|
<>
|
||||||
<option value="" disabled>
|
<option value="" disabled>
|
||||||
Select a disk
|
Select a disk
|
||||||
</option>
|
</option>
|
||||||
<For each={devices?.blockdevices}>
|
<For each={deviceQuery.data?.blockdevices}>
|
||||||
{(device) => (
|
{(device) => (
|
||||||
<option value={device.path}>
|
<option value={device.path}>
|
||||||
{device.path} -- {device.size} bytes
|
{device.path} -- {device.size} bytes
|
||||||
</option>
|
</option>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</select>
|
</>
|
||||||
<div class="label">
|
}
|
||||||
{isFetching && (
|
/>
|
||||||
<span class="label-text-alt">
|
|
||||||
<span class="loading loading-bars"></span>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{field.error && (
|
|
||||||
<span class="label-text-alt font-bold text-error">
|
|
||||||
{field.error}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
<Field name="language" validate={[required("This field is required")]}>
|
<Field name="language" validate={[required("This field is required")]}>
|
||||||
{(field, props) => (
|
{(field, props) => (
|
||||||
<>
|
<>
|
||||||
<label class="form-control input-bordered flex w-full items-center gap-2">
|
<SelectInput
|
||||||
<select
|
formStore={formStore}
|
||||||
required
|
selectProps={props}
|
||||||
class="select select-bordered w-full"
|
label="Language"
|
||||||
{...props}
|
value={String(field.value)}
|
||||||
>
|
error={field.error}
|
||||||
<option>en_US.UTF-8</option>
|
required
|
||||||
<For each={languages}>
|
options={
|
||||||
|
<For each={langQuery.data}>
|
||||||
{(language) => <option value={language}>{language}</option>}
|
{(language) => <option value={language}>{language}</option>}
|
||||||
</For>
|
</For>
|
||||||
</select>
|
}
|
||||||
<div class="label">
|
/>
|
||||||
{isFetchingLanguages && (
|
|
||||||
<span class="label-text-alt">
|
|
||||||
<span class="loading loading-bars">en_US.UTF-8</span>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{field.error && (
|
|
||||||
<span class="label-text-alt font-bold text-error">
|
|
||||||
{field.error}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
<Field name="keymap" validate={[required("This field is required")]}>
|
<Field name="keymap" validate={[required("This field is required")]}>
|
||||||
{(field, props) => (
|
{(field, props) => (
|
||||||
<>
|
<>
|
||||||
<label class="form-control input-bordered flex w-full items-center gap-2">
|
<SelectInput
|
||||||
<select
|
formStore={formStore}
|
||||||
required
|
selectProps={props}
|
||||||
class="select select-bordered w-full"
|
label="Keymap"
|
||||||
{...props}
|
value={String(field.value)}
|
||||||
>
|
error={field.error}
|
||||||
<option>en</option>
|
required
|
||||||
<For each={keymaps}>
|
options={
|
||||||
|
<For each={keymapQuery.data}>
|
||||||
{(keymap) => <option value={keymap}>{keymap}</option>}
|
{(keymap) => <option value={keymap}>{keymap}</option>}
|
||||||
</For>
|
</For>
|
||||||
</select>
|
}
|
||||||
<div class="label">
|
/>
|
||||||
{isFetchingKeymaps && (
|
|
||||||
<span class="label-text-alt">
|
|
||||||
<span class="loading loading-bars"></span>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{field.error && (
|
|
||||||
<span class="label-text-alt font-bold text-error">
|
|
||||||
{field.error}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
<Field name="sshKeys" validate={[]} type="string[]">
|
|
||||||
|
<Field name="sshKeys" type="File[]">
|
||||||
{(field, props) => (
|
{(field, props) => (
|
||||||
<>
|
<>
|
||||||
<label class="input input-bordered flex items-center gap-2">
|
<FileInput
|
||||||
<span class="material-icons">key</span>
|
{...props}
|
||||||
<input
|
onClick={async (event) => {
|
||||||
type="text"
|
event.preventDefault(); // Prevent the native file dialog from opening
|
||||||
class="grow"
|
const input = event.target;
|
||||||
placeholder="Select SSH Key"
|
const files = await selectSshKeys();
|
||||||
value={field.value ? field.value.join(", ") : ""}
|
|
||||||
readOnly
|
// Set the files
|
||||||
onClick={() => selectSshPubkey()}
|
Object.defineProperty(input, "files", {
|
||||||
required
|
value: files,
|
||||||
{...props}
|
writable: true,
|
||||||
/>
|
});
|
||||||
</label>
|
// Define the files property on the input element
|
||||||
<div class="label">
|
const changeEvent = new Event("input", {
|
||||||
{field.error && (
|
bubbles: true,
|
||||||
<span class="label-text-alt font-bold text-error">
|
cancelable: true,
|
||||||
{field.error}
|
});
|
||||||
</span>
|
input.dispatchEvent(changeEvent);
|
||||||
)}
|
}}
|
||||||
</div>
|
value={field.value}
|
||||||
|
error={field.error}
|
||||||
|
label="Authorized SSH Keys"
|
||||||
|
multiple
|
||||||
|
required
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
<button class="btn btn-error" type="submit" disabled={isFlashing()}>
|
<button
|
||||||
{isFlashing() ? (
|
class="btn btn-error"
|
||||||
|
type="submit"
|
||||||
|
disabled={formStore.submitting}
|
||||||
|
>
|
||||||
|
{formStore.submitting ? (
|
||||||
<span class="loading loading-spinner"></span>
|
<span class="loading loading-spinner"></span>
|
||||||
) : (
|
) : (
|
||||||
<span class="material-icons">bolt</span>
|
<span class="material-icons">bolt</span>
|
||||||
)}
|
)}
|
||||||
{isFlashing() ? "Flashing..." : "Flash Installer"}
|
{formStore.submitting ? "Flashing..." : "Flash Installer"}
|
||||||
</button>
|
</button>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { callApi, OperationArgs, pyApi, OperationResponse } from "@/src/api";
|
import { callApi, OperationArgs, pyApi, OperationResponse } from "@/src/api";
|
||||||
import { activeURI, setRoute } from "@/src/App";
|
import { activeURI, setRoute } from "@/src/App";
|
||||||
|
import { TextInput } from "@/src/components/TextInput";
|
||||||
import { createForm, required, reset } from "@modular-forms/solid";
|
import { createForm, required, reset } from "@modular-forms/solid";
|
||||||
import { createQuery, useQueryClient } from "@tanstack/solid-query";
|
import { createQuery, useQueryClient } from "@tanstack/solid-query";
|
||||||
import { Match, Switch } from "solid-js";
|
import { Match, Switch } from "solid-js";
|
||||||
@@ -57,93 +58,44 @@ export function CreateMachine() {
|
|||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div class="px-1">
|
<div class="px-1">
|
||||||
Create new Machine
|
<span class="px-2">Create new Machine</span>
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
reset(formStore);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
reset
|
|
||||||
</button>
|
|
||||||
<Form onSubmit={handleSubmit}>
|
<Form onSubmit={handleSubmit}>
|
||||||
<Field
|
<Field
|
||||||
name="machine.name"
|
name="machine.name"
|
||||||
validate={[required("This field is required")]}
|
validate={[required("This field is required")]}
|
||||||
>
|
>
|
||||||
{(field, props) => (
|
{(field, props) => (
|
||||||
<>
|
<TextInput
|
||||||
<label
|
inputProps={props}
|
||||||
class="input input-bordered flex items-center gap-2"
|
formStore={formStore}
|
||||||
classList={{
|
value={`${field.value}`}
|
||||||
"input-disabled": formStore.submitting,
|
label={"name"}
|
||||||
}}
|
error={field.error}
|
||||||
>
|
required
|
||||||
<input
|
/>
|
||||||
{...props}
|
|
||||||
value={field.value}
|
|
||||||
type="text"
|
|
||||||
class="grow"
|
|
||||||
placeholder="name"
|
|
||||||
required
|
|
||||||
disabled={formStore.submitting}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<div class="label">
|
|
||||||
{field.error && (
|
|
||||||
<span class="label-text-alt font-bold text-error">
|
|
||||||
{field.error}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
<Field name="machine.description">
|
<Field name="machine.description">
|
||||||
{(field, props) => (
|
{(field, props) => (
|
||||||
<>
|
<TextInput
|
||||||
<label
|
inputProps={props}
|
||||||
class="input input-bordered flex items-center gap-2"
|
formStore={formStore}
|
||||||
classList={{
|
value={`${field.value}`}
|
||||||
"input-disabled": formStore.submitting,
|
label={"description"}
|
||||||
}}
|
error={field.error}
|
||||||
>
|
/>
|
||||||
<input
|
|
||||||
value={String(field.value)}
|
|
||||||
type="text"
|
|
||||||
class="grow"
|
|
||||||
placeholder="description"
|
|
||||||
required
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<div class="label">
|
|
||||||
{field.error && (
|
|
||||||
<span class="label-text-alt font-bold text-error">
|
|
||||||
{field.error}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
<Field name="machine.deploy.targetHost">
|
<Field name="machine.deploy.targetHost">
|
||||||
{(field, props) => (
|
{(field, props) => (
|
||||||
<>
|
<>
|
||||||
<label
|
<TextInput
|
||||||
class="input input-bordered flex items-center gap-2"
|
inputProps={props}
|
||||||
classList={{
|
formStore={formStore}
|
||||||
"input-disabled": formStore.submitting,
|
value={`${field.value}`}
|
||||||
}}
|
label={"Deployment target"}
|
||||||
>
|
error={field.error}
|
||||||
<input
|
/>
|
||||||
value={String(field.value)}
|
|
||||||
type="text"
|
|
||||||
class="grow"
|
|
||||||
placeholder="root@flash-installer.local"
|
|
||||||
required
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<div class="label">
|
<div class="label">
|
||||||
<span class="label-text-alt text-neutral">
|
<span class="label-text-alt text-neutral">
|
||||||
Must be set before deployment for the following tasks:
|
Must be set before deployment for the following tasks:
|
||||||
@@ -159,11 +111,6 @@ export function CreateMachine() {
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</span>
|
</span>
|
||||||
{field.error && (
|
|
||||||
<span class="label-text-alt font-bold text-error">
|
|
||||||
{field.error}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user