From 18e75c99548cb14b7f0b99cd6bad4086f824d260 Mon Sep 17 00:00:00 2001 From: DavHau Date: Wed, 7 May 2025 17:15:16 +0700 Subject: [PATCH] GUI: initialize support for vars prompts ... for now only when updating a machine (not when installing) Whenever the user clicks on the update button in the machine view, and only if user input is needed for some missing vars, the user will be forwarded to a vars page. --- pkgs/clan-cli/clan_cli/tests/test_vars.py | 52 +++++---- pkgs/clan-cli/clan_cli/vars/generate.py | 44 ++++++-- pkgs/webview-ui/app/src/index.tsx | 9 +- .../app/src/routes/machines/details.tsx | 72 +++++++++---- .../src/routes/machines/install/vars-step.tsx | 101 +++++++++++++++--- 5 files changed, 214 insertions(+), 64 deletions(-) diff --git a/pkgs/clan-cli/clan_cli/tests/test_vars.py b/pkgs/clan-cli/clan_cli/tests/test_vars.py index ad51563f1..62713c781 100644 --- a/pkgs/clan-cli/clan_cli/tests/test_vars.py +++ b/pkgs/clan-cli/clan_cli/tests/test_vars.py @@ -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" diff --git a/pkgs/clan-cli/clan_cli/vars/generate.py b/pkgs/clan-cli/clan_cli/vars/generate.py index cadbdc4f7..c515f7967 100644 --- a/pkgs/clan-cli/clan_cli/vars/generate.py +++ b/pkgs/clan-cli/clan_cli/vars/generate.py @@ -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 diff --git a/pkgs/webview-ui/app/src/index.tsx b/pkgs/webview-ui/app/src/index.tsx index 155294f66..94b6ba375 100644 --- a/pkgs/webview-ui/app/src/index.tsx +++ b/pkgs/webview-ui/app/src/index.tsx @@ -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: () => , }, + { + path: "/:id/vars", + label: "Vars", + hidden: true, + component: () => , + }, ], }, { diff --git a/pkgs/webview-ui/app/src/routes/machines/details.tsx b/pkgs/webview-ui/app/src/routes/machines/details.tsx index c5742fd1e..9b64691a1 100644 --- a/pkgs/webview-ui/app/src/routes/machines/details.tsx +++ b/pkgs/webview-ui/app/src/routes/machines/details.tsx @@ -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) => { /> - } - handleNext={(data) => { - // const prev = getValue(formStore, "2"); - // setValue(formStore, "2", { ...prev, ...data }); - handleNext(); - }} - initial={{ - ...getValue(formStore, "3"), - }} - /> +
TODO: vars
{ 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 ( <>
@@ -626,7 +660,7 @@ const MachineForm = (props: MachineDetailsProps) => {
- {(f) => ( + {(prompt) => ( - {!f.previous_value ? "Required" : "Optional"} + {!prompt.previous_value ? "Required" : "Optional"} - {f.name} + {prompt.name} + {/* Avoid nesting issue in case of a "." */} + + {(field, props) => ( + + )} + )} @@ -80,7 +141,17 @@ export const VarsStep = (props: StepProps) => { - {props.footer} + ); }; + +export const VarsForMachine = () => { + const params = useParams(); + + return ( + + {(uri) => } + + ); +};