UI: add vars step to installation

This commit is contained in:
Johannes Kirschbauer
2025-01-09 10:33:49 +01:00
parent 4dcdb3e926
commit a3f6fb21c8
6 changed files with 385 additions and 257 deletions

View File

@@ -311,9 +311,9 @@ def test_generated_shared_secret_sops(
shared_generator["script"] = "echo hello > $out/my_shared_secret"
m2_config = flake.machines["machine2"]
m2_config["nixpkgs"]["hostPlatform"] = "x86_64-linux"
m2_config["clan"]["core"]["vars"]["generators"][
"my_shared_generator"
] = shared_generator.copy()
m2_config["clan"]["core"]["vars"]["generators"]["my_shared_generator"] = (
shared_generator.copy()
)
flake.refresh()
monkeypatch.chdir(flake.path)
sops_setup.init()
@@ -732,9 +732,9 @@ def test_migration(
my_service = config["clan"]["core"]["facts"]["services"]["my_service"]
my_service["public"]["my_value"] = {}
my_service["secret"]["my_secret"] = {}
my_service["generator"][
"script"
] = "echo -n hello > $facts/my_value && echo -n hello > $secrets/my_secret"
my_service["generator"]["script"] = (
"echo -n hello > $facts/my_value && echo -n hello > $secrets/my_secret"
)
my_generator = config["clan"]["core"]["vars"]["generators"]["my_generator"]
my_generator["files"]["my_value"]["secret"] = False
my_generator["files"]["my_secret"]["secret"] = True

View File

@@ -28,11 +28,20 @@ export const Modal = (props: ModalProps) => {
const handleMouseMove = (e: MouseEvent) => {
if (dragging()) {
const newTop = e.clientY - startOffset().y;
const newLeft = e.clientX - startOffset().x;
let newTop = e.clientY - startOffset().y;
let newLeft = e.clientX - startOffset().x;
if (newTop < 0) {
newTop = 0;
}
if (newLeft < 0) {
newLeft = 0;
}
dialogRef.style.top = `${newTop}px`;
dialogRef.style.left = `${newLeft}px`;
// dialogRef.style.maxHeight = `calc(100vh - ${newTop}px - 2rem)`;
// dialogRef.style.maxHeight = `calc(100vh - ${newTop}px - 2rem)`;
}
};
@@ -53,7 +62,7 @@ export const Modal = (props: ModalProps) => {
)}
classList={{
"!cursor-grabbing": dragging(),
[cx("scale-105 transition-transform")]: dragging(),
[cx("scale-[101%] transition-transform")]: dragging(),
}}
ref={(el) => {
dialogRef = el;
@@ -112,7 +121,10 @@ export const Modal = (props: ModalProps) => {
/>
</div>
</Dialog.Label>
<Dialog.Description class="flex flex-col bg-def-1" as="div">
<Dialog.Description
class="flex max-h-[90vh] flex-col overflow-y-hidden bg-def-1"
as="div"
>
{props.children}
</Dialog.Description>
</Dialog.Content>

View File

@@ -25,7 +25,7 @@ import { HardwareValues, HWStep } from "./install/hardware-step";
import { DiskStep, DiskValues } from "./install/disk-step";
import { SummaryStep } from "./install/summary-step";
import cx from "classnames";
import { SectionHeader } from "@/src/components/group";
import { VarsStep, VarsValues } from "./install/vars-step";
type MachineFormInterface = MachineData & {
sshKey?: File;
@@ -37,7 +37,8 @@ type MachineData = SuccessData<"get_inventory_machine_details">;
const steps: Record<StepIdx, string> = {
"1": "Hardware detection",
"2": "Disk schema",
"3": "Installation",
"3": "Credentials & Data",
"4": "Installation",
};
type StepIdx = keyof AllStepsValues;
@@ -45,7 +46,8 @@ type StepIdx = keyof AllStepsValues;
export interface AllStepsValues extends FieldValues {
"1": HardwareValues;
"2": DiskValues;
"3": NonNullable<unknown>;
"3": VarsValues;
"4": NonNullable<unknown>;
}
const LoadingBar = () => (
@@ -190,7 +192,7 @@ const InstallMachine = (props: InstallMachineProps) => {
};
const Footer = () => (
<div class="flex justify-between">
<div class="flex justify-between p-4">
<Button
startIcon={<Icon icon="ArrowLeft" />}
variant="light"
@@ -214,7 +216,10 @@ const InstallMachine = (props: InstallMachineProps) => {
return (
<Switch
fallback={
<Form onSubmit={handleInstall}>
<Form
onSubmit={handleInstall}
class="relative top-0 flex h-full flex-col gap-0"
>
{/* Register each step as form field */}
{/* @ts-expect-error: object type is not statically supported */}
<Field name="1">{(field, fieldProps) => <></>}</Field>
@@ -269,11 +274,7 @@ const InstallMachine = (props: InstallMachineProps) => {
)}
</For>
</div>
<div class="flex flex-col gap-6 p-6">
<Switch
fallback={"Undefined content. This Step seems to not exist."}
>
<Switch fallback={"Undefined content. This Step seems to not exist."}>
<Match when={step() === "1"}>
<HWStep
// @ts-expect-error: This cannot be undefined in this context.
@@ -315,6 +316,23 @@ const InstallMachine = (props: InstallMachineProps) => {
/>
</Match>
<Match when={step() === "3"}>
<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()}
footer={<Footer />}
handleNext={(data) => {
// const prev = getValue(formStore, "2");
// setValue(formStore, "2", { ...prev, ...data });
handleNext();
}}
initial={{
...getValue(formStore, "3"),
}}
/>
</Match>
<Match when={step() === "4"}>
<SummaryStep
// @ts-expect-error: This cannot be undefined in this context.
machine_id={props.name}
@@ -324,7 +342,7 @@ const InstallMachine = (props: InstallMachineProps) => {
// @ts-expect-error: This cannot be known.
initial={getValues(formStore)}
footer={
<div class="flex justify-between">
<div class="flex justify-between p-4">
<Button
startIcon={<Icon icon="ArrowLeft" />}
variant="light"
@@ -340,7 +358,6 @@ const InstallMachine = (props: InstallMachineProps) => {
/>
</Match>
</Switch>
</div>
</Form>
}
>

View File

@@ -105,6 +105,8 @@ export const HWStep = (props: StepProps<HardwareValues>) => {
return (
<Form onSubmit={handleSubmit} class="flex flex-col gap-6">
<div class="max-h-[calc(100vh-20rem)] overflow-y-scroll">
<div class="flex h-full flex-col gap-6 p-4">
<Group>
<Field name="target" validate={required("Target must be provided")}>
{(field, fieldProps) => (
@@ -200,6 +202,8 @@ export const HWStep = (props: StepProps<HardwareValues>) => {
)}
</Field>
</Group>
</div>
</div>
{props.footer}
</Form>
);

View File

@@ -12,6 +12,8 @@ export const SummaryStep = (props: StepProps<AllStepsValues>) => {
const diskValues = () => props.initial?.["2"];
return (
<>
<div class="max-h-[calc(100vh-20rem)] overflow-y-scroll">
<div class="flex h-full flex-col gap-6 p-4">
<Section>
<Typography
hierarchy="label"
@@ -79,7 +81,12 @@ export const SummaryStep = (props: StepProps<AllStepsValues>) => {
variant="danger"
headline={
<span>
<Typography hierarchy="body" size="s" weight="bold" color="inherit">
<Typography
hierarchy="body"
size="s"
weight="bold"
color="inherit"
>
Setup your device.
</Typography>
<Typography
@@ -93,6 +100,8 @@ export const SummaryStep = (props: StepProps<AllStepsValues>) => {
</span>
}
/>
</div>
</div>
{props.footer}
</>
);

View File

@@ -0,0 +1,86 @@
import { callApi } from "@/src/api";
import {
createForm,
SubmitHandler,
validate,
FieldValues,
} from "@modular-forms/solid";
import { createQuery } from "@tanstack/solid-query";
import { StepProps } from "./hardware-step";
import { Typography } from "@/src/components/Typography";
import { Group } from "@/src/components/group";
import { For, Match, Show, Switch } from "solid-js";
export type VarsValues = FieldValues & Record<string, string>;
export const VarsStep = (props: StepProps<VarsValues>) => {
const [formStore, { Form, Field }] = createForm<VarsValues>({
initialValues: { ...props.initial, schema: "single-disk" },
});
const handleSubmit: SubmitHandler<VarsValues> = async (values, event) => {
console.log("Submit Disk", { values });
const valid = await validate(formStore);
console.log("Valid", valid);
if (!valid) return;
props.handleNext(values);
};
const generatorsQuery = createQuery(() => ({
queryKey: [props.dir, props.machine_id, "generators"],
queryFn: async () => {
const result = await callApi("get_generators", {
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}
class="flex h-full flex-col gap-6"
noValidate={false}
>
<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) => (
<Group>
<Typography hierarchy="label" size="default">
{generator.name}
</Typography>
<div>
Bound to module (shared):{" "}
{generator.share ? "True" : "False"}
</div>
<For each={generator.prompts}>
{(f) => (
<Group>
<Typography hierarchy="label" size="s">
{!f.previous_value ? "Required" : "Optional"}
</Typography>
<Typography hierarchy="label" size="s">
{f.name}
</Typography>
</Group>
)}
</For>
</Group>
)}
</For>
)}
</Match>
</Switch>
</div>
</div>
<Show when={generatorsQuery.isFetched}>{props.footer}</Show>
</Form>
);
};