diff --git a/pkgs/clan-cli/clan_cli/api/serde.py b/pkgs/clan-cli/clan_cli/api/serde.py index a3ff265d5..a82df0dc8 100644 --- a/pkgs/clan-cli/clan_cli/api/serde.py +++ b/pkgs/clan-cli/clan_cli/api/serde.py @@ -127,6 +127,11 @@ def construct_value(t: type, field_value: JsonValue, loc: list[str] = []) -> Any """ if t is None and field_value: raise ClanError(f"Expected None but got: {field_value}", location=f"{loc}") + + if is_type_in_union(t, type(None)) and field_value is None: + # Sometimes the field value is None, which is valid if the type hint allows None + return None + # If the field is another dataclass # Field_value must be a dictionary if is_dataclass(t) and isinstance(field_value, dict): @@ -160,9 +165,9 @@ def construct_value(t: type, field_value: JsonValue, loc: list[str] = []) -> Any # Union types construct the first non-None type elif is_union_type(t): # Unwrap the union type - t = unwrap_none_type(t) + inner = unwrap_none_type(t) # Construct the field value - return construct_value(t, field_value) + return construct_value(inner, field_value) # Nested types # list diff --git a/pkgs/clan-cli/tests/test_deserializers.py b/pkgs/clan-cli/tests/test_deserializers.py index 23a36793a..905f6c2bc 100644 --- a/pkgs/clan-cli/tests/test_deserializers.py +++ b/pkgs/clan-cli/tests/test_deserializers.py @@ -244,6 +244,27 @@ def test_alias_field_from_orig_name() -> None: from_dict(Person, data) +def test_none_or_string() -> None: + """ + Field declares an alias. But the data is provided with the field name. + """ + + data = None + + @dataclass + class Person: + name: Path + + checked = from_dict(str | None, data) + assert checked is None + + checked2 = from_dict(dict[str, str] | None, data) + assert checked2 is None + + checked3 = from_dict(Person | None, data) + assert checked3 is None + + def test_path_field() -> None: @dataclass class Person: diff --git a/pkgs/webview-ui/app/src/routes/machines/[name]/view.tsx b/pkgs/webview-ui/app/src/routes/machines/[name]/view.tsx index 730b9b6fc..af026a204 100644 --- a/pkgs/webview-ui/app/src/routes/machines/[name]/view.tsx +++ b/pkgs/webview-ui/app/src/routes/machines/[name]/view.tsx @@ -64,6 +64,53 @@ const InstallMachine = (props: InstallMachineProps) => { const handleInstall = async (values: InstallForm) => { console.log("Installing", values); + const curr_uri = activeURI(); + if (!curr_uri) { + return; + } + if (!props.name || !props.targetHost) { + return; + } + + const r = await callApi("install_machine", { + opts: { + flake: { + loc: curr_uri, + }, + machine: props.name, + target_host: props.targetHost, + }, + password: "", + }); + + if (r.status === "error") { + toast.error("Failed to install machine"); + } + if (r.status === "success") { + toast.success("Machine installed successfully"); + } + }; + + const handleDiskConfirm = async () => { + const curr_uri = activeURI(); + const disk = getValue(formStore, "disk"); + const disk_id = props.disks.find((d) => d.name === disk)?.id_link; + if (!curr_uri || !disk_id || !props.name) { + return; + } + + const r = await callApi("set_single_disk_uuid", { + base_path: curr_uri, + machine_name: props.name, + disk_uuid: disk_id, + }); + if (r.status === "error") { + toast.error("Failed to set disk"); + } + if (r.status === "success") { + toast.success("Disk set successfully"); + setConfirmDisk(true); + } }; return ( <> @@ -111,7 +158,7 @@ const InstallMachine = (props: InstallMachineProps) => { fallback={