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 { Components } from "./routes/components";
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";
export const client = new QueryClient();
@@ -79,7 +79,7 @@ export const routes: AppRoute[] = [
path: "/:id/vars",
label: "Vars",
hidden: true,
component: () => <VarsForMachine />,
component: () => <VarsPage />,
},
],
},

View File

@@ -293,7 +293,19 @@ const InstallMachine = (props: InstallMachineProps) => {
/>
</Match>
<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 when={step() === "4"}>
<SummaryStep
@@ -433,8 +445,10 @@ const MachineForm = (props: MachineDetailsProps) => {
const handleUpdateButton = async () => {
await generatorsQuery.refetch();
if (generatorsQuery.data?.length !== 0) {
navigate(`/machines/${machineName()}/vars`);
if (
generatorsQuery.data?.some((generator) => generator.prompts?.length !== 0)
) {
navigate(`/machines/${machineName()}/vars?action=update`);
} else {
handleUpdate();
}
@@ -472,6 +486,7 @@ const MachineForm = (props: MachineDetailsProps) => {
createEffect(() => {
const action = searchParams.action;
console.log({ action });
if (action === "update") {
setSearchParams({ action: undefined });
handleUpdate();

View File

@@ -1,4 +1,4 @@
import { callApi } from "@/src/api";
import { callApi, SuccessData } from "@/src/api";
import {
createForm,
FieldValues,
@@ -11,24 +11,82 @@ import { Group } from "@/src/components/group";
import { For, Match, Show, Switch } from "solid-js";
import { TextInput } from "@/src/Form/fields";
import toast from "solid-toast";
import { useNavigate, useParams } from "@solidjs/router";
import { useNavigate, useParams, useSearchParams } from "@solidjs/router";
import { activeURI } from "@/src/App";
import { StepProps } from "./hardware-step";
export type VarsValues = FieldValues & Record<string, Record<string, string>>;
export interface VarsStepProps {
machine_id: string;
dir: string;
}
export const VarsStep = (props: VarsStepProps) => {
const [formStore, { Form, Field }] = createForm<VarsValues>({});
const navigate = useNavigate();
export const VarsStep = (props: StepProps<VarsValues>) => {
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) => {
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__)
// This hack is needed because we are using "." in the keys of the form
const sanitizedValues = Object.fromEntries(
@@ -43,43 +101,13 @@ export const VarsStep = (props: VarsStepProps) => {
]),
) as VarsValues;
const valid = await validate(formStore);
if (generatorsQuery.data === undefined) {
toast.error("Error fetching data");
if (!valid) {
toast.error("Please fill all required fields");
return;
}
const loading_toast = toast.loading("Generating vars...");
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`);
}
props.handleSubmit(sanitizedValues, event);
};
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 (
<Form
onSubmit={handleSubmit}
@@ -88,79 +116,71 @@ export const VarsStep = (props: VarsStepProps) => {
>
<div class="max-h-[calc(100vh-20rem)] overflow-y-scroll">
<div class="flex h-full flex-col gap-6 p-4">
<Switch>
<Match when={generatorsQuery.isLoading}>Loading ...</Match>
<Match when={generatorsQuery.data}>
{(generators) => (
<For each={generators()}>
{(generator) => (
<For each={props.generators}>
{(generator) => (
<Group>
<Typography hierarchy="label" size="default">
{generator.name}
</Typography>
<div>
Bound to module (shared): {generator.share ? "True" : "False"}
</div>
<For each={generator.prompts}>
{(prompt) => (
<Group>
<Typography hierarchy="label" size="default">
{generator.name}
<Typography hierarchy="label" size="s">
{!prompt.previous_value ? "Required" : "Optional"}
</Typography>
<div>
Bound to module (shared):{" "}
{generator.share ? "True" : "False"}
</div>
<For each={generator.prompts}>
{(prompt) => (
<Group>
<Typography hierarchy="label" size="s">
{!prompt.previous_value ? "Required" : "Optional"}
</Typography>
<Typography hierarchy="label" size="s">
{prompt.name}
</Typography>
{/* Avoid nesting issue in case of a "." */}
<Field
name={`${generator.name.replaceAll(
".",
"__dot__",
)}.${prompt.name.replaceAll(".", "__dot__")}`}
<Typography hierarchy="label" size="s">
{prompt.name}
</Typography>
<Field
// Avoid nesting issue in case of a "."
name={`${generator.name.replaceAll(
".",
"__dot__",
)}.${prompt.name.replaceAll(".", "__dot__")}`}
>
{(field, props) => (
<Switch
fallback={
<TextInput
inputProps={{
...props,
type:
prompt.prompt_type === "hidden"
? "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) => (
<Switch
fallback={
<TextInput
inputProps={{
...props,
type:
prompt.prompt_type === "hidden"
? "password"
: "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>
<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>
)}
</For>
</Field>
</Group>
)}
</For>
)}
</Match>
</Switch>
</Group>
)}
</For>
</div>
</div>
<button type="submit">Submit</button>
@@ -168,12 +188,27 @@ export const VarsStep = (props: VarsStepProps) => {
);
};
export const VarsForMachine = () => {
export const VarsPage = () => {
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 (
<Show when={activeURI()}>
{(uri) => <VarsStep machine_id={params.id} dir={uri()} />}
{(uri) => (
<VarsStep
machine_id={params.id}
dir={uri()}
handleNext={handleNext}
footer
/>
)}
</Show>
);
};