diff --git a/pkgs/clan-cli/clan_cli/api/__init__.py b/pkgs/clan-cli/clan_cli/api/__init__.py index 29b3f45b..f7bb4d1e 100644 --- a/pkgs/clan-cli/clan_cli/api/__init__.py +++ b/pkgs/clan-cli/clan_cli/api/__init__.py @@ -1,12 +1,14 @@ from collections.abc import Callable -from typing import Any +from typing import Any, TypeVar + +T = TypeVar("T") class _MethodRegistry: def __init__(self) -> None: self._registry: dict[str, Callable] = {} - def register(self, fn: Callable) -> Callable: + def register(self, fn: Callable[..., T]) -> Callable[..., T]: self._registry[fn.__name__] = fn return fn diff --git a/pkgs/clan-cli/clan_cli/api/util.py b/pkgs/clan-cli/clan_cli/api/util.py index 9765a5fe..bd9b10a4 100644 --- a/pkgs/clan-cli/clan_cli/api/util.py +++ b/pkgs/clan-cli/clan_cli/api/util.py @@ -44,6 +44,7 @@ def type_to_dict(t: Any, scope: str = "") -> dict: elif issubclass(origin, dict): return { "type": "object", + "additionalProperties": type_to_dict(t.__args__[1], scope), } raise BaseException(f"Error api type not yet supported {t!s}") diff --git a/pkgs/clan-cli/clan_cli/machines/list.py b/pkgs/clan-cli/clan_cli/machines/list.py index 37ea60ac..d0966ea6 100644 --- a/pkgs/clan-cli/clan_cli/machines/list.py +++ b/pkgs/clan-cli/clan_cli/machines/list.py @@ -1,4 +1,5 @@ import argparse +import dataclasses import json import logging from pathlib import Path @@ -11,18 +12,24 @@ from ..nix import nix_config, nix_eval log = logging.getLogger(__name__) +@dataclasses.dataclass +class MachineInfo: + machine_name: str + machine_description: str | None + machine_icon: str | None + + @API.register -def list_machines( - debug: bool, - flake_url: Path | str, -) -> list[str]: +def list_machines(debug: bool, flake_url: Path | str) -> dict[str, MachineInfo]: config = nix_config() system = config["system"] cmd = nix_eval( [ f"{flake_url}#clanInternals.machines.{system}", "--apply", - "builtins.attrNames", + """builtins.mapAttrs (name: attrs: { + inherit (attrs.config.clanCore) machineDescription machineIcon machineName; +})""", "--json", ] ) @@ -33,12 +40,27 @@ def list_machines( proc = run(cmd) res = proc.stdout.strip() - return json.loads(res) + machines_dict = json.loads(res) + + return { + k: MachineInfo( + machine_name=v.get("machineName"), + machine_description=v.get("machineDescription", None), + machine_icon=v.get("machineIcon", None), + ) + for k, v in machines_dict.items() + } def list_command(args: argparse.Namespace) -> None: - for machine in list_machines(args.debug, Path(args.flake)): - print(machine) + flake_path = Path(args.flake).resolve() + print("Listing all machines:\n") + print("Source: ", flake_path) + print("-" * 40) + for name, machine in list_machines(args.debug, flake_path).items(): + description = machine.machine_description or "[no description]" + print(f"{name}\n: {description}\n") + print("-" * 40) def register_list_parser(parser: argparse.ArgumentParser) -> None: diff --git a/pkgs/clan-cli/tests/test_create_flake.py b/pkgs/clan-cli/tests/test_create_flake.py index 9a69d21d..89338430 100644 --- a/pkgs/clan-cli/tests/test_create_flake.py +++ b/pkgs/clan-cli/tests/test_create_flake.py @@ -26,6 +26,14 @@ def test_create_flake( cli.run(["machines", "create", "machine1"]) capsys.readouterr() # flush cache + # create a hardware-configuration.nix that doesn't throw an eval error + + for patch_machine in ["jon", "sara"]: + with open( + flake_dir / "machines" / f"{patch_machine}/hardware-configuration.nix", "w" + ) as hw_config_nix: + hw_config_nix.write("{}") + cli.run(["machines", "list"]) assert "machine1" in capsys.readouterr().out flake_show = subprocess.run( diff --git a/pkgs/clan-cli/tests/test_machines_cli.py b/pkgs/clan-cli/tests/test_machines_cli.py index 325f45e2..a06ce656 100644 --- a/pkgs/clan-cli/tests/test_machines_cli.py +++ b/pkgs/clan-cli/tests/test_machines_cli.py @@ -16,7 +16,10 @@ def test_machine_subcommands( cli.run(["--flake", str(test_flake_with_core.path), "machines", "list"]) out = capsys.readouterr() - assert "machine1\nvm1\nvm2\n" == out.out + + assert "machine1" in out.out + assert "vm1" in out.out + assert "vm2" in out.out cli.run( ["--flake", str(test_flake_with_core.path), "machines", "delete", "machine1"] @@ -25,4 +28,7 @@ def test_machine_subcommands( capsys.readouterr() cli.run(["--flake", str(test_flake_with_core.path), "machines", "list"]) out = capsys.readouterr() - assert "vm1\nvm2\n" == out.out + + assert "machine1" not in out.out + assert "vm1" in out.out + assert "vm2" in out.out diff --git a/pkgs/clan-vm-manager/clan_vm_manager/app.py b/pkgs/clan-vm-manager/clan_vm_manager/app.py index 18a8f4ef..f0278faf 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/app.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/app.py @@ -106,7 +106,7 @@ class MainApplication(Adw.Application): def on_activate(self, source: "MainApplication") -> None: if not self.window: self.init_style() - self.window = MainWindow(config=ClanConfig(initial_view="webview")) + self.window = MainWindow(config=ClanConfig(initial_view="list")) self.window.set_application(self) self.window.show() diff --git a/pkgs/clan-vm-manager/clan_vm_manager/views/webview.py b/pkgs/clan-vm-manager/clan_vm_manager/views/webview.py index 0c28ad01..667913ea 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/views/webview.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/views/webview.py @@ -1,3 +1,4 @@ +import dataclasses import json import logging import sys @@ -22,6 +23,23 @@ site_index: Path = ( log = logging.getLogger(__name__) +def dataclass_to_dict(obj: Any) -> Any: + """ + Utility function to convert dataclasses to dictionaries + It converts all nested dataclasses, lists, tuples, and dictionaries to dictionaries + + It does NOT convert member functions. + """ + if dataclasses.is_dataclass(obj): + return {k: dataclass_to_dict(v) for k, v in dataclasses.asdict(obj).items()} + elif isinstance(obj, list | tuple): + return [dataclass_to_dict(item) for item in obj] + elif isinstance(obj, dict): + return {k: dataclass_to_dict(v) for k, v in obj.items()} + else: + return obj + + class WebView: def __init__(self, methods: dict[str, Callable]) -> None: self.method_registry: dict[str, Callable] = methods @@ -82,7 +100,7 @@ class WebView: with self.mutex_lock: log.debug("Executing... ", method_name) result = handler_fn(data) - serialized = json.dumps(result) + serialized = json.dumps(dataclass_to_dict(result)) # Use idle_add to queue the response call to js on the main GTK thread GLib.idle_add(self.return_data_to_js, method_name, serialized) diff --git a/pkgs/clan-vm-manager/clan_vm_manager/windows/main_window.py b/pkgs/clan-vm-manager/clan_vm_manager/windows/main_window.py index 5ec1fc12..8bb0f91f 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/windows/main_window.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/windows/main_window.py @@ -62,8 +62,7 @@ class MainWindow(Adw.ApplicationWindow): stack_view.add_named(Logs(), "logs") webview = WebView(methods=API._registry) - - stack_view.add_named(webview.get_webview(), "list") + stack_view.add_named(webview.get_webview(), "webview") stack_view.set_visible_child_name(config.initial_view) diff --git a/pkgs/webview-ui/app/src/Config.tsx b/pkgs/webview-ui/app/src/Config.tsx index 3dd3c374..a2ccaf3b 100644 --- a/pkgs/webview-ui/app/src/Config.tsx +++ b/pkgs/webview-ui/app/src/Config.tsx @@ -1,8 +1,16 @@ -import { createSignal, createContext, useContext, JSXElement } from "solid-js"; -import { pyApi } from "./message"; +import { + createSignal, + createContext, + useContext, + JSXElement, + createEffect, +} from "solid-js"; +import { OperationResponse, pyApi } from "./message"; export const makeCountContext = () => { - const [machines, setMachines] = createSignal([]); + const [machines, setMachines] = createSignal< + OperationResponse<"list_machines"> + >({}); const [loading, setLoading] = createSignal(false); pyApi.list_machines.receive((machines) => { @@ -10,6 +18,10 @@ export const makeCountContext = () => { setMachines(machines); }); + createEffect(() => { + console.log("The count is now", machines()); + }); + return [ { loading, machines }, { @@ -25,7 +37,10 @@ export const makeCountContext = () => { type CountContextType = ReturnType; export const CountContext = createContext([ - { loading: () => false, machines: () => [] }, + { + loading: () => false, + machines: () => ({}), + }, { getMachines: () => {}, }, diff --git a/pkgs/webview-ui/app/src/message.ts b/pkgs/webview-ui/app/src/message.ts index fdf5339b..ee059a38 100644 --- a/pkgs/webview-ui/app/src/message.ts +++ b/pkgs/webview-ui/app/src/message.ts @@ -1,11 +1,11 @@ import { FromSchema } from "json-schema-to-ts"; import { schema } from "@/api"; -type API = FromSchema; +export type API = FromSchema; -type OperationNames = keyof API; -type OperationArgs = API[T]["argument"]; -type OperationResponse = API[T]["return"]; +export type OperationNames = keyof API; +export type OperationArgs = API[T]["argument"]; +export type OperationResponse = API[T]["return"]; declare global { interface Window { @@ -40,7 +40,7 @@ function createFunctions( }); }, receive: (fn: (response: OperationResponse) => void) => { - window.clan.list_machines = deserialize(fn); + window.clan[operationName] = deserialize(fn); }, }; } @@ -59,6 +59,7 @@ const deserialize = (fn: (response: T) => void) => (str: string) => { try { + console.debug("Received data: ", str); fn(JSON.parse(str) as T); } catch (e) { alert(`Error parsing JSON: ${e}`); diff --git a/pkgs/webview-ui/app/src/nested.tsx b/pkgs/webview-ui/app/src/nested.tsx index 7f1e9ecd..ebbd0bca 100644 --- a/pkgs/webview-ui/app/src/nested.tsx +++ b/pkgs/webview-ui/app/src/nested.tsx @@ -1,24 +1,34 @@ -import { For, Match, Switch, type Component } from "solid-js"; +import { For, Match, Switch, createEffect, type Component } from "solid-js"; import { useCountContext } from "./Config"; export const Nested: Component = () => { const [{ machines, loading }, { getMachines }] = useCountContext(); + + const list = () => Object.values(machines()); + + createEffect(() => { + console.log("1", list()); + }); + createEffect(() => { + console.log("2", machines()); + }); return (
-
+
Loading... - + No machines found - - - {(machine, i) => ( + + + {(entry, i) => (
  • - {i() + 1}: {machine} + {i() + 1}: {entry.machine_name}{" "} + {entry.machine_description || "No description"}
  • )}