Merge pull request 'GUI: initialize support for vars prompts' (#3529) from DavHau/clan-core:gui-prompts into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/3529
This commit is contained in:
@@ -12,7 +12,12 @@ from clan_cli.tests.age_keys import SopsSetup
|
||||
from clan_cli.tests.fixtures_flakes import ClanFlake
|
||||
from clan_cli.tests.helpers import cli
|
||||
from clan_cli.vars.check import check_vars
|
||||
from clan_cli.vars.generate import Generator, generate_vars_for_machine_interactive
|
||||
from clan_cli.vars.generate import (
|
||||
Generator,
|
||||
generate_vars_for_machine,
|
||||
generate_vars_for_machine_interactive,
|
||||
get_generators_closure,
|
||||
)
|
||||
from clan_cli.vars.get import get_var
|
||||
from clan_cli.vars.graph import all_missing_closure, requested_closure
|
||||
from clan_cli.vars.list import stringify_all_vars
|
||||
@@ -640,9 +645,6 @@ def test_api_set_prompts(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
flake: ClanFlake,
|
||||
) -> None:
|
||||
from clan_cli.vars._types import GeneratorUpdate
|
||||
from clan_cli.vars.list import get_generators, set_prompts
|
||||
|
||||
config = flake.machines["my_machine"]
|
||||
config["nixpkgs"]["hostPlatform"] = "x86_64-linux"
|
||||
my_generator = config["clan"]["core"]["vars"]["generators"]["my_generator"]
|
||||
@@ -652,33 +654,39 @@ def test_api_set_prompts(
|
||||
flake.refresh()
|
||||
|
||||
monkeypatch.chdir(flake.path)
|
||||
params = {"machine_name": "my_machine", "base_dir": str(flake.path)}
|
||||
|
||||
set_prompts(
|
||||
**params,
|
||||
updates=[
|
||||
GeneratorUpdate(
|
||||
generator="my_generator",
|
||||
prompt_values={"prompt1": "input1"},
|
||||
)
|
||||
],
|
||||
generate_vars_for_machine(
|
||||
machine_name="my_machine",
|
||||
base_dir=flake.path,
|
||||
generators=["my_generator"],
|
||||
all_prompt_values={
|
||||
"my_generator": {
|
||||
"prompt1": "input1",
|
||||
}
|
||||
},
|
||||
)
|
||||
machine = Machine(name="my_machine", flake=Flake(str(flake.path)))
|
||||
store = in_repo.FactStore(machine)
|
||||
assert store.exists(Generator("my_generator"), "prompt1")
|
||||
assert store.get(Generator("my_generator"), "prompt1").decode() == "input1"
|
||||
set_prompts(
|
||||
**params,
|
||||
updates=[
|
||||
GeneratorUpdate(
|
||||
generator="my_generator",
|
||||
prompt_values={"prompt1": "input2"},
|
||||
)
|
||||
],
|
||||
generate_vars_for_machine(
|
||||
machine_name="my_machine",
|
||||
base_dir=flake.path,
|
||||
generators=["my_generator"],
|
||||
all_prompt_values={
|
||||
"my_generator": {
|
||||
"prompt1": "input2",
|
||||
}
|
||||
},
|
||||
)
|
||||
assert store.get(Generator("my_generator"), "prompt1").decode() == "input2"
|
||||
|
||||
generators = get_generators(**params)
|
||||
generators = get_generators_closure(
|
||||
machine_name="my_machine",
|
||||
base_dir=flake.path,
|
||||
regenerate=True,
|
||||
include_previous_values=True,
|
||||
)
|
||||
assert len(generators) == 1
|
||||
assert generators[0].name == "my_generator"
|
||||
assert generators[0].prompts[0].name == "prompt1"
|
||||
|
||||
@@ -294,10 +294,28 @@ def _ask_prompts(
|
||||
return prompt_values
|
||||
|
||||
|
||||
def _get_previous_value(
|
||||
machine: "Machine",
|
||||
generator: Generator,
|
||||
prompt: Prompt,
|
||||
) -> str | None:
|
||||
if not prompt.persist:
|
||||
return None
|
||||
|
||||
pub_store = machine.public_vars_store
|
||||
if pub_store.exists(generator, prompt.name):
|
||||
return pub_store.get(generator, prompt.name).decode()
|
||||
sec_store = machine.secret_vars_store
|
||||
if sec_store.exists(generator, prompt.name):
|
||||
return sec_store.get(generator, prompt.name).decode()
|
||||
return None
|
||||
|
||||
|
||||
def get_closure(
|
||||
machine: "Machine",
|
||||
generator_name: str | None,
|
||||
regenerate: bool,
|
||||
include_previous_values: bool = False,
|
||||
) -> list[Generator]:
|
||||
from .graph import all_missing_closure, full_closure
|
||||
|
||||
@@ -310,14 +328,24 @@ def get_closure(
|
||||
for generator in vars_generators:
|
||||
generator.machine(machine)
|
||||
|
||||
result_closure = []
|
||||
if generator_name is None: # all generators selected
|
||||
if regenerate:
|
||||
return full_closure(generators)
|
||||
return all_missing_closure(generators)
|
||||
result_closure = full_closure(generators)
|
||||
else:
|
||||
result_closure = all_missing_closure(generators)
|
||||
# specific generator selected
|
||||
if regenerate:
|
||||
return requested_closure([generator_name], generators)
|
||||
return minimal_closure([generator_name], generators)
|
||||
elif regenerate:
|
||||
result_closure = requested_closure([generator_name], generators)
|
||||
else:
|
||||
result_closure = minimal_closure([generator_name], generators)
|
||||
|
||||
if include_previous_values:
|
||||
for generator in result_closure:
|
||||
for prompt in generator.prompts:
|
||||
prompt.previous_value = _get_previous_value(machine, generator, prompt)
|
||||
|
||||
return result_closure
|
||||
|
||||
|
||||
@API.register
|
||||
@@ -325,6 +353,7 @@ def get_generators_closure(
|
||||
machine_name: str,
|
||||
base_dir: Path,
|
||||
regenerate: bool = False,
|
||||
include_previous_values: bool = False,
|
||||
) -> list[Generator]:
|
||||
from clan_cli.machines.machines import Machine
|
||||
|
||||
@@ -332,13 +361,14 @@ def get_generators_closure(
|
||||
machine=Machine(name=machine_name, flake=Flake(str(base_dir))),
|
||||
generator_name=None,
|
||||
regenerate=regenerate,
|
||||
include_previous_values=include_previous_values,
|
||||
)
|
||||
|
||||
|
||||
def _generate_vars_for_machine(
|
||||
machine: "Machine",
|
||||
generators: list[Generator],
|
||||
all_prompt_values: dict[str, dict],
|
||||
all_prompt_values: dict[str, dict[str, str]],
|
||||
no_sandbox: bool = False,
|
||||
) -> bool:
|
||||
for generator in generators:
|
||||
@@ -350,7 +380,7 @@ def _generate_vars_for_machine(
|
||||
generator=generator,
|
||||
secret_vars_store=machine.secret_vars_store,
|
||||
public_vars_store=machine.public_vars_store,
|
||||
prompt_values=all_prompt_values[generator.name],
|
||||
prompt_values=all_prompt_values.get(generator.name, {}),
|
||||
no_sandbox=no_sandbox,
|
||||
)
|
||||
return True
|
||||
|
||||
@@ -21,6 +21,8 @@ import { ModuleDetails as AddModule } from "./routes/modules/add";
|
||||
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";
|
||||
|
||||
export const client = new QueryClient();
|
||||
|
||||
@@ -31,7 +33,6 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
|
||||
"Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?",
|
||||
);
|
||||
}
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
console.log("Development mode");
|
||||
// Load the debugger in development mode
|
||||
@@ -73,6 +74,12 @@ export const routes: AppRoute[] = [
|
||||
hidden: true,
|
||||
component: () => <MachineDetails />,
|
||||
},
|
||||
{
|
||||
path: "/:id/vars",
|
||||
label: "Vars",
|
||||
hidden: true,
|
||||
component: () => <VarsForMachine />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -11,9 +11,9 @@ import {
|
||||
getValues,
|
||||
setValue,
|
||||
} from "@modular-forms/solid";
|
||||
import { useParams } from "@solidjs/router";
|
||||
import { createQuery } from "@tanstack/solid-query";
|
||||
import { createSignal, For, Match, Show, Switch } from "solid-js";
|
||||
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 toast from "solid-toast";
|
||||
import { MachineAvatar } from "./avatar";
|
||||
import { Header } from "@/src/layout/header";
|
||||
@@ -316,21 +316,7 @@ 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"),
|
||||
}}
|
||||
/>
|
||||
<div>TODO: vars</div>
|
||||
</Match>
|
||||
<Match when={step() === "4"}>
|
||||
<SummaryStep
|
||||
@@ -416,6 +402,10 @@ const MachineForm = (props: MachineDetailsProps) => {
|
||||
|
||||
const [installModalOpen, setInstallModalOpen] = createSignal(false);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const handleSubmit = async (values: MachineFormInterface) => {
|
||||
console.log("submitting", values);
|
||||
|
||||
@@ -447,7 +437,40 @@ const MachineForm = (props: MachineDetailsProps) => {
|
||||
return null;
|
||||
};
|
||||
|
||||
const generatorsQuery = createQuery(() => ({
|
||||
queryKey: [activeURI(), machineName(), "generators"],
|
||||
queryFn: async () => {
|
||||
const machine_name = machineName();
|
||||
const base_dir = activeURI();
|
||||
if (!machine_name || !base_dir) {
|
||||
return [];
|
||||
}
|
||||
const result = await callApi("get_generators_closure", {
|
||||
base_dir: base_dir,
|
||||
machine_name: machine_name,
|
||||
});
|
||||
if (result.status === "error") throw new Error("Failed to fetch data");
|
||||
return result.data;
|
||||
},
|
||||
}));
|
||||
|
||||
const handleUpdateButton = async () => {
|
||||
const t = toast.loading("Checking for generators...");
|
||||
await generatorsQuery.refetch();
|
||||
toast.dismiss(t);
|
||||
if (generatorsQuery.data?.length !== 0) {
|
||||
navigate(`/machines/${machineName()}/vars`);
|
||||
} else {
|
||||
handleUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
const [isUpdating, setIsUpdating] = createSignal(false);
|
||||
|
||||
const handleUpdate = async () => {
|
||||
if (isUpdating()) {
|
||||
return;
|
||||
}
|
||||
const curr_uri = activeURI();
|
||||
if (!curr_uri) {
|
||||
return;
|
||||
@@ -461,6 +484,7 @@ const MachineForm = (props: MachineDetailsProps) => {
|
||||
const target = targetHost();
|
||||
|
||||
const loading_toast = toast.loading("Updating machine...");
|
||||
setIsUpdating(true);
|
||||
const r = await callApi("update_machines", {
|
||||
base_path: curr_uri,
|
||||
machines: [
|
||||
@@ -472,6 +496,7 @@ const MachineForm = (props: MachineDetailsProps) => {
|
||||
},
|
||||
],
|
||||
});
|
||||
setIsUpdating(false);
|
||||
toast.dismiss(loading_toast);
|
||||
|
||||
if (r.status === "error") {
|
||||
@@ -481,6 +506,15 @@ const MachineForm = (props: MachineDetailsProps) => {
|
||||
toast.success("Machine updated successfully");
|
||||
}
|
||||
};
|
||||
|
||||
createEffect(() => {
|
||||
const action = searchParams.action;
|
||||
if (action === "update") {
|
||||
setSearchParams({ action: undefined });
|
||||
handleUpdate();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="flex flex-col gap-6 p-4">
|
||||
@@ -626,7 +660,7 @@ const MachineForm = (props: MachineDetailsProps) => {
|
||||
<Button
|
||||
class="w-full"
|
||||
// disabled={!online()}
|
||||
onClick={() => handleUpdate()}
|
||||
onClick={() => handleUpdateButton()}
|
||||
endIcon={<Icon icon="Update" />}
|
||||
>
|
||||
Update
|
||||
|
||||
@@ -5,31 +5,73 @@ import {
|
||||
validate,
|
||||
FieldValues,
|
||||
} from "@modular-forms/solid";
|
||||
import { createQuery } from "@tanstack/solid-query";
|
||||
import { StepProps } from "./hardware-step";
|
||||
import { createQuery, useQueryClient } from "@tanstack/solid-query";
|
||||
import { Typography } from "@/src/components/Typography";
|
||||
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 { activeURI } from "@/src/App";
|
||||
|
||||
export type VarsValues = FieldValues & Record<string, string>;
|
||||
export type VarsValues = FieldValues & Record<string, Record<string, string>>;
|
||||
|
||||
export const VarsStep = (props: StepProps<VarsValues>) => {
|
||||
const [formStore, { Form, Field }] = createForm<VarsValues>({
|
||||
initialValues: { ...props.initial, schema: "single-disk" },
|
||||
});
|
||||
export interface VarsStepProps {
|
||||
machine_id: string;
|
||||
dir: string;
|
||||
}
|
||||
|
||||
export const VarsStep = (props: VarsStepProps) => {
|
||||
const [formStore, { Form, Field }] = createForm<VarsValues>({});
|
||||
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const handleSubmit: SubmitHandler<VarsValues> = async (values, event) => {
|
||||
console.log("Submit Disk", { 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(
|
||||
Object.entries(values).map(([key, value]) => [
|
||||
key.replaceAll("__dot__", "."),
|
||||
Object.fromEntries(
|
||||
Object.entries(value).map(([k, v]) => [
|
||||
k.replaceAll("__dot__", "."),
|
||||
v,
|
||||
]),
|
||||
),
|
||||
]),
|
||||
) as VarsValues;
|
||||
const valid = await validate(formStore);
|
||||
console.log("Valid", valid);
|
||||
if (!valid) return;
|
||||
props.handleNext(values);
|
||||
if (generatorsQuery.data === undefined) {
|
||||
toast.error("Error fetching data");
|
||||
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`);
|
||||
}
|
||||
};
|
||||
|
||||
const generatorsQuery = createQuery(() => ({
|
||||
queryKey: [props.dir, props.machine_id, "generators"],
|
||||
queryFn: async () => {
|
||||
const result = await callApi("get_generators", {
|
||||
const result = await callApi("get_generators_closure", {
|
||||
base_dir: props.dir,
|
||||
machine_name: props.machine_id,
|
||||
});
|
||||
@@ -61,14 +103,33 @@ export const VarsStep = (props: StepProps<VarsValues>) => {
|
||||
{generator.share ? "True" : "False"}
|
||||
</div>
|
||||
<For each={generator.prompts}>
|
||||
{(f) => (
|
||||
{(prompt) => (
|
||||
<Group>
|
||||
<Typography hierarchy="label" size="s">
|
||||
{!f.previous_value ? "Required" : "Optional"}
|
||||
{!prompt.previous_value ? "Required" : "Optional"}
|
||||
</Typography>
|
||||
<Typography hierarchy="label" size="s">
|
||||
{f.name}
|
||||
{prompt.name}
|
||||
</Typography>
|
||||
{/* Avoid nesting issue in case of a "." */}
|
||||
<Field
|
||||
name={`${generator.name.replaceAll(".", "__dot__")}.${prompt.name.replaceAll(".", "__dot__")}`}
|
||||
>
|
||||
{(field, props) => (
|
||||
<TextInput
|
||||
inputProps={{
|
||||
...props,
|
||||
type:
|
||||
prompt.prompt_type === "hidden"
|
||||
? "password"
|
||||
: "text",
|
||||
}}
|
||||
label={prompt.description}
|
||||
value={prompt.previous_value ?? ""}
|
||||
error={field.error}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
</Group>
|
||||
)}
|
||||
</For>
|
||||
@@ -80,7 +141,17 @@ export const VarsStep = (props: StepProps<VarsValues>) => {
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={generatorsQuery.isFetched}>{props.footer}</Show>
|
||||
<button type="submit">Submit</button>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export const VarsForMachine = () => {
|
||||
const params = useParams();
|
||||
|
||||
return (
|
||||
<Show when={activeURI()}>
|
||||
{(uri) => <VarsStep machine_id={params.id} dir={uri()} />}
|
||||
</Show>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user