From d5e54d262b91849be388f65c4e05d838f2e9bde5 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Tue, 7 Jan 2025 09:18:57 +0100 Subject: [PATCH 1/3] API: Disk templates, persist original values --- pkgs/clan-cli/clan_cli/api/disk.py | 14 ++++++++++++-- pkgs/clan-cli/clan_cli/machines/list.py | 7 ++++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/pkgs/clan-cli/clan_cli/api/disk.py b/pkgs/clan-cli/clan_cli/api/disk.py index 53b3e9427..26d2cfd10 100644 --- a/pkgs/clan-cli/clan_cli/api/disk.py +++ b/pkgs/clan-cli/clan_cli/api/disk.py @@ -3,7 +3,7 @@ import logging from collections.abc import Callable from dataclasses import dataclass from pathlib import Path -from typing import Any +from typing import Any, TypedDict from uuid import uuid4 from clan_cli.api import API @@ -124,6 +124,11 @@ def get_disk_schemas( return disk_schemas +class MachineDiskMatter(TypedDict): + schema_name: str + placeholders: dict[str, str] + + @API.register def set_machine_disk_schema( base_path: Path, @@ -134,7 +139,7 @@ def set_machine_disk_schema( placeholders: dict[str, str], force: bool = False, ) -> None: - """ " + """ Set the disk placeholders of the template """ # Assert the hw-config must exist before setting the disk @@ -183,8 +188,13 @@ def set_machine_disk_schema( ) raise ClanError(msg, description=f"Valid options: {ph.options}") + placeholders_toml = "\n".join( + [f"""# {k} = "{v}""" for k, v in placeholders.items() if v is not None] + ) header = f"""# --- # schema = "{schema_name}" +# [placeholders] +{placeholders_toml} # --- # This file was automatically generated! # CHANGING this configuration requires wiping and reinstalling the machine diff --git a/pkgs/clan-cli/clan_cli/machines/list.py b/pkgs/clan-cli/clan_cli/machines/list.py index a41709e10..d85291964 100644 --- a/pkgs/clan-cli/clan_cli/machines/list.py +++ b/pkgs/clan-cli/clan_cli/machines/list.py @@ -6,6 +6,7 @@ from pathlib import Path from typing import Literal from clan_cli.api import API +from clan_cli.api.disk import MachineDiskMatter from clan_cli.api.modules import parse_frontmatter from clan_cli.api.serde import dataclass_to_dict from clan_cli.cmd import RunOpts, run_no_stdout @@ -41,7 +42,7 @@ def list_inventory_machines(flake_url: str | Path) -> dict[str, Machine]: class MachineDetails: machine: Machine hw_config: HardwareConfig | None = None - disk_schema: str | None = None + disk_schema: MachineDiskMatter | None = None import re @@ -69,7 +70,7 @@ def get_inventory_machine_details(flake_url: Path, machine_name: str) -> Machine hw_config = HardwareConfig.detect_type(flake_url, machine_name) machine_dir = specific_machine_dir(flake_url, machine_name) - disk_schema: str | None = None + disk_schema: MachineDiskMatter | None = None disk_path = machine_dir / "disko.nix" if disk_path.exists(): with disk_path.open() as f: @@ -77,7 +78,7 @@ def get_inventory_machine_details(flake_url: Path, machine_name: str) -> Machine header = extract_header(content) data, _rest = parse_frontmatter(header) if data: - disk_schema = data.get("schema") + disk_schema = data # type: ignore return MachineDetails(machine=machine, hw_config=hw_config, disk_schema=disk_schema) From 97e342d74962081a3912aeba0828554cfca11b18 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Tue, 7 Jan 2025 09:19:30 +0100 Subject: [PATCH 2/3] UI: fix select disabled --- pkgs/webview-ui/app/src/Form/fields/Select.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/pkgs/webview-ui/app/src/Form/fields/Select.tsx b/pkgs/webview-ui/app/src/Form/fields/Select.tsx index 5b92cf391..18a98534b 100644 --- a/pkgs/webview-ui/app/src/Form/fields/Select.tsx +++ b/pkgs/webview-ui/app/src/Form/fields/Select.tsx @@ -153,6 +153,7 @@ export function SelectInput(props: SelectInputpProps) { // Currently the popover only opens with onClick // Options are not selectable with keyboard tabIndex={-1} + disabled={props.disabled} onClick={() => { const popover = document.getElementById(_id); if (popover) { From 497002bb20f471a17b03675462fa97d8239e1236 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Tue, 7 Jan 2025 09:20:00 +0100 Subject: [PATCH 3/3] UI: improve disk workflow when already initialized --- .../app/src/routes/machines/details.tsx | 130 +++++++++++------- .../src/routes/machines/install/disk-step.tsx | 16 +-- 2 files changed, 87 insertions(+), 59 deletions(-) diff --git a/pkgs/webview-ui/app/src/routes/machines/details.tsx b/pkgs/webview-ui/app/src/routes/machines/details.tsx index 9e8249712..1f3c3dfaf 100644 --- a/pkgs/webview-ui/app/src/routes/machines/details.tsx +++ b/pkgs/webview-ui/app/src/routes/machines/details.tsx @@ -21,10 +21,10 @@ import { InputLabel } from "@/src/components/inputBase"; import { FieldLayout } from "@/src/Form/fields/layout"; import { Modal } from "@/src/components/modal"; import { Typography } from "@/src/components/Typography"; -import cx from "classnames"; 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"; type MachineFormInterface = MachineData & { @@ -48,12 +48,30 @@ export interface AllStepsValues extends FieldValues { "3": NonNullable; } +const LoadingBar = () => ( +
+); + function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } interface InstallMachineProps { name?: string; targetHost?: string | null; + machine: MachineData; } const InstallMachine = (props: InstallMachineProps) => { const curr = activeURI(); @@ -64,9 +82,9 @@ const InstallMachine = (props: InstallMachineProps) => { const [formStore, { Form, Field }] = createForm(); + const [isDone, setIsDone] = createSignal(false); const [isInstalling, setIsInstalling] = createSignal(false); const [progressText, setProgressText] = createSignal(); - const [installError, setInstallError] = createSignal(); const handleInstall = async (values: AllStepsValues) => { console.log("Installing", values); @@ -86,42 +104,36 @@ const InstallMachine = (props: InstallMachineProps) => { "Installing machine. Grab coffee (15min)...", ); setIsInstalling(true); - setProgressText("Setting up disk ... (1/5)"); - const disk_response = await callApi("set_machine_disk_schema", { - base_path: curr_uri, - machine_name: props.name, - placeholders: diskValues.placeholders, - schema_name: diskValues.schema, - force: true, - }); + // props.machine.disk_ + const shouldRunDisk = + JSON.stringify(props.machine.disk_schema?.placeholders) !== + JSON.stringify(diskValues.placeholders); - if (disk_response.status === "error") { - toast.error( - `Failed to set disk schema: ${disk_response.errors[0].message}`, - ); - setProgressText( - "Failed to set disk schema. \n" + disk_response.errors[0].message, - ); - return; + if (shouldRunDisk) { + setProgressText("Setting up disk ... (1/5)"); + const disk_response = await callApi("set_machine_disk_schema", { + base_path: curr_uri, + machine_name: props.name, + placeholders: diskValues.placeholders, + schema_name: diskValues.schema, + force: true, + }); + + if (disk_response.status === "error") { + toast.error( + `Failed to set disk schema: ${disk_response.errors[0].message}`, + ); + setProgressText( + "Failed to set disk schema. \n" + disk_response.errors[0].message, + ); + return; + } } - // Next step - if (disk_response.status === "success") { - setProgressText("Evaluate configuration ... (2/5)"); - } - // Next step - await sleep(2000); - setProgressText("Building machine ... (3/5)"); - await sleep(2000); - setProgressText("Formatting remote disk ... (4/5)"); - await sleep(2000); - setProgressText("Copying system ... (5/5)"); - await sleep(2000); - setProgressText("Rebooting remote system ... "); - await sleep(2000); + setProgressText("Installing machine ... (2/5)"); - const r = await callApi("install_machine", { + const installPromise = callApi("install_machine", { opts: { machine: { name: props.name, @@ -133,13 +145,36 @@ const InstallMachine = (props: InstallMachineProps) => { password: "", }, }); + + // Next step + await sleep(10 * 1000); + setProgressText("Building machine ... (3/5)"); + await sleep(10 * 1000); + setProgressText("Formatting remote disk ... (4/5)"); + await sleep(10 * 1000); + setProgressText("Copying system ... (5/5)"); + await sleep(20 * 1000); + setProgressText("Rebooting remote system ... "); + await sleep(10 * 1000); + + const installResponse = await installPromise; + toast.dismiss(loading_toast); - if (r.status === "error") { + if (installResponse.status === "error") { toast.error("Failed to install machine"); + setIsDone(true); + setProgressText( + "Failed to install machine. \n" + installResponse.errors[0].message, + ); } - if (r.status === "success") { + + if (installResponse.status === "success") { toast.success("Machine installed successfully"); + setIsDone(true); + setProgressText( + "Machine installed successfully. Please unplug the usb stick and reboot the system.", + ); } }; @@ -271,7 +306,12 @@ const InstallMachine = (props: InstallMachineProps) => { setValue(formStore, "2", { ...prev, ...data }); handleNext(); }} - initial={getValue(formStore, "2")} + // @ts-expect-error: The placeholder type is to wide + initial={{ + ...props.machine.disk_schema, + ...getValue(formStore, "2"), + initialized: !!props.machine.disk_schema, + }} /> @@ -326,20 +366,7 @@ const InstallMachine = (props: InstallMachineProps) => {
-
+ {isDone() && } { /> )} - + {(field, props) => ( <> { diff --git a/pkgs/webview-ui/app/src/routes/machines/install/disk-step.tsx b/pkgs/webview-ui/app/src/routes/machines/install/disk-step.tsx index a23e9388b..78b1bf3b3 100644 --- a/pkgs/webview-ui/app/src/routes/machines/install/disk-step.tsx +++ b/pkgs/webview-ui/app/src/routes/machines/install/disk-step.tsx @@ -17,6 +17,7 @@ export interface DiskValues extends FieldValues { mainDisk: string; }; schema: string; + initialized: boolean; } export const DiskStep = (props: StepProps) => { const [formStore, { Form, Field }] = createForm({ @@ -74,9 +75,14 @@ export const DiskStep = (props: StepProps) => { + {props.initial?.initialized && "Disk has been initialized already"} {(field, fieldProps) => ( ) => { { label: "No options", value: "" }, ] } - // options={ - // deviceQuery.data?.blockdevices.map((d) => ({ - // value: d.path, - // label: `${d.path} -- ${d.size} bytes`, - // })) || [] - // } error={field.error} label="Main Disk" value={field.value || ""} placeholder="Select a disk" selectProps={fieldProps} - required + required={!props.initial?.initialized} /> )}