UI: add vars step to installation flow

This commit is contained in:
DavHau
2025-05-19 19:11:19 +07:00
parent 2b4e624ee8
commit 72fa59c9fa
3 changed files with 169 additions and 119 deletions

View File

@@ -22,7 +22,7 @@ import { ApiTester } from "./api_test";
import { IconVariant } from "./components/icon"; import { IconVariant } from "./components/icon";
import { Components } from "./routes/components"; import { Components } from "./routes/components";
import { activeURI } from "./App"; import { activeURI } from "./App";
import { VarsForMachine, VarsStep } from "./routes/machines/install/vars-step"; import { VarsPage, VarsForm } from "./routes/machines/install/vars-step";
import { ThreePlayground } from "./three"; import { ThreePlayground } from "./three";
export const client = new QueryClient(); export const client = new QueryClient();
@@ -79,7 +79,7 @@ export const routes: AppRoute[] = [
path: "/:id/vars", path: "/:id/vars",
label: "Vars", label: "Vars",
hidden: true, hidden: true,
component: () => <VarsForMachine />, component: () => <VarsPage />,
}, },
], ],
}, },

View File

@@ -293,7 +293,19 @@ const InstallMachine = (props: InstallMachineProps) => {
/> />
</Match> </Match>
<Match when={step() === "3"}> <Match when={step() === "3"}>
<div>TODO: vars</div> <VarsStep
// @ts-expect-error: This cannot be undefined in this context.
machine_id={props.name}
// @ts-expect-error: This cannot be undefined in this context.
dir={activeURI()}
handleNext={(data) => {
const prev = getValue(formStore, "3");
setValue(formStore, "3", { ...prev, ...data });
handleNext();
}}
initial={getValue(formStore, "3") || {}}
footer={<Footer />}
/>
</Match> </Match>
<Match when={step() === "4"}> <Match when={step() === "4"}>
<SummaryStep <SummaryStep
@@ -433,8 +445,10 @@ const MachineForm = (props: MachineDetailsProps) => {
const handleUpdateButton = async () => { const handleUpdateButton = async () => {
await generatorsQuery.refetch(); await generatorsQuery.refetch();
if (generatorsQuery.data?.length !== 0) { if (
navigate(`/machines/${machineName()}/vars`); generatorsQuery.data?.some((generator) => generator.prompts?.length !== 0)
) {
navigate(`/machines/${machineName()}/vars?action=update`);
} else { } else {
handleUpdate(); handleUpdate();
} }
@@ -472,6 +486,7 @@ const MachineForm = (props: MachineDetailsProps) => {
createEffect(() => { createEffect(() => {
const action = searchParams.action; const action = searchParams.action;
console.log({ action });
if (action === "update") { if (action === "update") {
setSearchParams({ action: undefined }); setSearchParams({ action: undefined });
handleUpdate(); handleUpdate();

View File

@@ -1,4 +1,4 @@
import { callApi } from "@/src/api"; import { callApi, SuccessData } from "@/src/api";
import { import {
createForm, createForm,
FieldValues, FieldValues,
@@ -11,24 +11,82 @@ import { Group } from "@/src/components/group";
import { For, Match, Show, Switch } from "solid-js"; import { For, Match, Show, Switch } from "solid-js";
import { TextInput } from "@/src/Form/fields"; import { TextInput } from "@/src/Form/fields";
import toast from "solid-toast"; import toast from "solid-toast";
import { useNavigate, useParams } from "@solidjs/router"; import { useNavigate, useParams, useSearchParams } from "@solidjs/router";
import { activeURI } from "@/src/App"; import { activeURI } from "@/src/App";
import { StepProps } from "./hardware-step";
export type VarsValues = FieldValues & Record<string, Record<string, string>>; export type VarsValues = FieldValues & Record<string, Record<string, string>>;
export interface VarsStepProps { export const VarsStep = (props: StepProps<VarsValues>) => {
machine_id: string;
dir: string;
}
export const VarsStep = (props: VarsStepProps) => {
const [formStore, { Form, Field }] = createForm<VarsValues>({});
const navigate = useNavigate();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const generatorsQuery = createQuery(() => ({
queryKey: [props.dir, props.machine_id, "generators"],
queryFn: async () => {
const result = await callApi("get_generators_closure", {
base_dir: props.dir,
machine_name: props.machine_id,
});
if (result.status === "error") throw new Error("Failed to fetch data");
return result.data;
},
}));
const handleSubmit: SubmitHandler<VarsValues> = async (values, event) => { const handleSubmit: SubmitHandler<VarsValues> = async (values, event) => {
console.log("Submit Disk", { values }); const loading_toast = toast.loading("Generating vars...");
if (generatorsQuery.data === undefined) {
toast.error("Error fetching data");
return;
}
const result = await callApi("generate_vars_for_machine", {
machine_name: props.machine_id,
base_dir: props.dir,
generators: generatorsQuery.data.map((generator) => generator.name),
all_prompt_values: values,
});
queryClient.invalidateQueries({
queryKey: [props.dir, props.machine_id, "generators"],
});
toast.dismiss(loading_toast);
if (result.status === "error") {
toast.error(result.errors[0].message);
return;
}
if (result.status === "success") {
toast.success("Vars saved successfully");
}
props.handleNext(values);
};
return (
<Switch>
<Match when={generatorsQuery.isLoading}>Loading ...</Match>
<Match when={generatorsQuery.data}>
{(generators) => (
<VarsForm
machine_id={props.machine_id}
dir={props.dir}
handleSubmit={handleSubmit}
generators={generators()}
/>
)}
</Match>
</Switch>
);
};
export interface VarsFormProps {
machine_id: string;
dir: string;
handleSubmit: SubmitHandler<VarsValues>;
generators: SuccessData<"get_generators_closure">;
}
export const VarsForm = (props: VarsFormProps) => {
const [formStore, { Form, Field }] = createForm<VarsValues>({});
const handleSubmit: SubmitHandler<VarsValues> = async (values, event) => {
console.log("Submit Vars", { values });
// sanitize the values back (replace __dot__) // sanitize the values back (replace __dot__)
// This hack is needed because we are using "." in the keys of the form // This hack is needed because we are using "." in the keys of the form
const sanitizedValues = Object.fromEntries( const sanitizedValues = Object.fromEntries(
@@ -43,43 +101,13 @@ export const VarsStep = (props: VarsStepProps) => {
]), ]),
) as VarsValues; ) as VarsValues;
const valid = await validate(formStore); const valid = await validate(formStore);
if (generatorsQuery.data === undefined) { if (!valid) {
toast.error("Error fetching data"); toast.error("Please fill all required fields");
return; return;
} }
const loading_toast = toast.loading("Generating vars..."); props.handleSubmit(sanitizedValues, event);
const result = await callApi("generate_vars_for_machine", {
machine_name: props.machine_id,
base_dir: props.dir,
generators: generatorsQuery.data.map((generator) => generator.name),
all_prompt_values: sanitizedValues,
});
queryClient.invalidateQueries({
queryKey: [props.dir, props.machine_id, "generators"],
});
toast.dismiss(loading_toast);
if (result.status === "error") {
toast.error(result.errors[0].message);
return;
}
if (result.status === "success") {
toast.success("Vars saved successfully");
navigate(`/machines/${props.machine_id}?action=update`);
}
}; };
const generatorsQuery = createQuery(() => ({
queryKey: [props.dir, props.machine_id, "generators"],
queryFn: async () => {
const result = await callApi("get_generators_closure", {
base_dir: props.dir,
machine_name: props.machine_id,
});
if (result.status === "error") throw new Error("Failed to fetch data");
return result.data;
},
}));
return ( return (
<Form <Form
onSubmit={handleSubmit} onSubmit={handleSubmit}
@@ -88,79 +116,71 @@ export const VarsStep = (props: VarsStepProps) => {
> >
<div class="max-h-[calc(100vh-20rem)] overflow-y-scroll"> <div class="max-h-[calc(100vh-20rem)] overflow-y-scroll">
<div class="flex h-full flex-col gap-6 p-4"> <div class="flex h-full flex-col gap-6 p-4">
<Switch> <For each={props.generators}>
<Match when={generatorsQuery.isLoading}>Loading ...</Match> {(generator) => (
<Match when={generatorsQuery.data}> <Group>
{(generators) => ( <Typography hierarchy="label" size="default">
<For each={generators()}> {generator.name}
{(generator) => ( </Typography>
<div>
Bound to module (shared): {generator.share ? "True" : "False"}
</div>
<For each={generator.prompts}>
{(prompt) => (
<Group> <Group>
<Typography hierarchy="label" size="default"> <Typography hierarchy="label" size="s">
{generator.name} {!prompt.previous_value ? "Required" : "Optional"}
</Typography> </Typography>
<div> <Typography hierarchy="label" size="s">
Bound to module (shared):{" "} {prompt.name}
{generator.share ? "True" : "False"} </Typography>
</div> <Field
<For each={generator.prompts}> // Avoid nesting issue in case of a "."
{(prompt) => ( name={`${generator.name.replaceAll(
<Group> ".",
<Typography hierarchy="label" size="s"> "__dot__",
{!prompt.previous_value ? "Required" : "Optional"} )}.${prompt.name.replaceAll(".", "__dot__")}`}
</Typography> >
<Typography hierarchy="label" size="s"> {(field, props) => (
{prompt.name} <Switch
</Typography> fallback={
{/* Avoid nesting issue in case of a "." */} <TextInput
<Field inputProps={{
name={`${generator.name.replaceAll( ...props,
".", type:
"__dot__", prompt.prompt_type === "hidden"
)}.${prompt.name.replaceAll(".", "__dot__")}`} ? "password"
: "text",
}}
label={prompt.description}
value={prompt.previous_value ?? ""}
error={field.error}
/>
}
>
<Match
when={
prompt.prompt_type === "multiline" ||
prompt.prompt_type === "multiline-hidden"
}
> >
{(field, props) => ( <textarea
<Switch {...props}
fallback={ class="w-full h-32 border border-gray-300 rounded-md p-2"
<TextInput placeholder={prompt.description}
inputProps={{ value={prompt.previous_value ?? ""}
...props, name={prompt.description}
type: />
prompt.prompt_type === "hidden" </Match>
? "password" </Switch>
: "text",
}}
label={prompt.description}
value={prompt.previous_value ?? ""}
error={field.error}
/>
}
>
<Match
when={
prompt.prompt_type === "multiline" ||
prompt.prompt_type === "multiline-hidden"
}
>
<textarea
{...props}
class="w-full h-32 border border-gray-300 rounded-md p-2"
placeholder={prompt.description}
value={prompt.previous_value ?? ""}
name={prompt.description}
/>
</Match>
</Switch>
)}
</Field>
</Group>
)} )}
</For> </Field>
</Group> </Group>
)} )}
</For> </For>
)} </Group>
</Match> )}
</Switch> </For>
</div> </div>
</div> </div>
<button type="submit">Submit</button> <button type="submit">Submit</button>
@@ -168,12 +188,27 @@ export const VarsStep = (props: VarsStepProps) => {
); );
}; };
export const VarsForMachine = () => { export const VarsPage = () => {
const params = useParams(); const params = useParams();
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const handleNext = (values: VarsValues) => {
if (searchParams?.action === "update") {
navigate(`/machines/${params.id}?action=update`);
} else {
toast.error("Invalid action for vars page");
}
};
return ( return (
<Show when={activeURI()}> <Show when={activeURI()}>
{(uri) => <VarsStep machine_id={params.id} dir={uri()} />} {(uri) => (
<VarsStep
machine_id={params.id}
dir={uri()}
handleNext={handleNext}
footer
/>
)}
</Show> </Show>
); );
}; };