Merge pull request 'clan ui: setup typed api method' (#1391) from hsjobeki-main into main
All checks were successful
deploy / deploy-docs (push) Successful in 20s
buildbot/nix-build .#checks.x86_64-linux.clan-dep-age Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-bash Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-fakeroot Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-e2fsprogs Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-git Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-nix Build done.
buildbot/nix-build .#checks.x86_64-linux.check-for-breakpoints Build done.
buildbot/nix-build .#checks.aarch64-darwin.nixos-test_install_machine Build done.
buildbot/nix-build .#checks.x86_64-linux.borgbackup Build done.
buildbot/nix-build .#checks.aarch64-darwin.nixos-test-backup Build done.
buildbot/nix-build .#checks.aarch64-darwin.nixos-flash-installer Build done.
buildbot/nix-build .#checks.aarch64-darwin.nixos-iso-installer Build done.
buildbot/nix-build .#checks.aarch64-linux.nixos-test_install_machine Build done.
buildbot/nix-build .#checks.aarch64-linux.nixos-test-backup Build done.
buildbot/nix-build .#checks.x86_64-linux."clan-dep-python3.11-mypy" Build done.
buildbot/nix-build .#checks.x86_64-linux."clan-dep-python3.11-qemu" Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-rsync Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-sops Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-sshpass Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-tor Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-zbar Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-pytest-with-core Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-pytest-without-core Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-vm-manager-no-breakpoints Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-vm-manager-pytest Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-openssh Build done.
buildbot/nix-build .#checks.aarch64-linux.nixos-flash-installer Build done.
buildbot/nix-build .#checks.x86_64-linux.container Build done.
buildbot/nix-build .#checks.x86_64-linux.devShell-clan-cli Build done.
buildbot/nix-build .#checks.x86_64-linux.devShell-clan-vm-manager Build done.
buildbot/nix-build .#checks.aarch64-linux.nixos-iso-installer Build done.
buildbot/nix-build .#checks.x86_64-linux.devShell-default Build done.
buildbot/nix-build .#checks.x86_64-linux.devShell-webview-ui Build done.
buildbot/nix-build .#checks.x86_64-linux.lib-jsonschema-example-valid Build done.
buildbot/nix-build .#checks.x86_64-linux.deltachat Build done.
buildbot/nix-build .#checks.x86_64-linux.devShell-docs Build done.
buildbot/nix-build .#checks.x86_64-linux.matrix-synapse Build done.
buildbot/nix-build .#checks.x86_64-linux.package-clan-ts-api Build done.
buildbot/nix-build .#checks.x86_64-linux.package-clan-vm-manager Build done.
buildbot/nix-build .#checks.x86_64-linux.package-default Build done.
buildbot/nix-build .#checks.x86_64-linux.module-schema Build done.
buildbot/nix-build .#checks.x86_64-linux.nixos-test_install_machine Build done.
buildbot/nix-build .#checks.x86_64-linux.package-impure-checks Build done.
buildbot/nix-build .#checks.x86_64-linux.package-clan-cli Build done.
buildbot/nix-build .#checks.x86_64-linux.package-clan-cli-docs Build done.
buildbot/nix-build .#checks.x86_64-linux.package-merge-after-ci Build done.
buildbot/nix-build .#checks.x86_64-linux.nixos-test-backup Build done.
buildbot/nix-build .#checks.x86_64-linux.package-moonlight-sunshine-accept Build done.
buildbot/nix-build .#checks.x86_64-linux.package-pending-reviews Build done.
buildbot/nix-build .#checks.x86_64-linux.package-tea-create-pr Build done.
buildbot/nix-build .#checks.x86_64-linux.package-wayland-proxy-virtwl Build done.
buildbot/nix-build .#checks.x86_64-linux.package-webview-ui Build done.
buildbot/nix-build .#checks.x86_64-linux.package-zerotier-members Build done.
buildbot/nix-build .#checks.x86_64-linux.package-deploy-docs Build done.
buildbot/nix-build .#checks.x86_64-linux.package-zt-tcp-relay Build done.
buildbot/nix-build .#checks.x86_64-linux.package-zerotierone Build done.
buildbot/nix-build .#checks.x86_64-linux.package-function-schema Build done.
buildbot/nix-build .#checks.x86_64-linux.package-docs Build done.
buildbot/nix-build .#checks.x86_64-linux.package-module-docs Build done.
buildbot/nix-build .#checks.x86_64-linux.renderClanOptions Build done.
buildbot/nix-build .#checks.x86_64-linux.lib-jsonschema-nix-unit-tests Build done.
buildbot/nix-build .#checks.x86_64-linux.package-module-schema Build done.
buildbot/nix-build .#checks.x86_64-linux.secrets Build done.
buildbot/nix-build .#checks.x86_64-linux.treefmt Build done.
buildbot/nix-build .#checks.x86_64-linux.nixos-flash-installer Build done.
buildbot/nix-build .#checks.x86_64-linux.zt-tcp-relay Build done.
buildbot/nix-build .#checks.x86_64-linux.nixos-iso-installer Build done.
buildbot/nix-build .#checks.x86_64-linux.package-iso-installer Build done.
buildbot/nix-build .#checks.x86_64-linux.wayland-proxy-virtwl Build done.
buildbot/nix-build .#checks.x86_64-linux.test-backups Build done.
buildbot/nix-build .#checks.x86_64-linux.syncthing Build done.
buildbot/nix-build .#checks.x86_64-linux.test-installation Build done.
buildbot/nix-eval Build done.
checks / checks-impure (push) Successful in 2m12s

This commit is contained in:
clan-bot 2024-05-21 14:06:45 +00:00
commit e28a02ec73
20 changed files with 375 additions and 208 deletions

View File

@ -20,28 +20,6 @@
"type": "github"
}
},
"dream2nix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
],
"purescript-overlay": "purescript-overlay",
"pyproject-nix": "pyproject-nix"
},
"locked": {
"lastModified": 1716143526,
"narHash": "sha256-Pk77RzfAlrr1iR81zFJA/puxr2OeBifd0Yb0ETWMVKE=",
"owner": "nix-community",
"repo": "dream2nix",
"rev": "f28d1cc6898532ac331a1625628090dc7c5b02e2",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "dream2nix",
"type": "github"
}
},
"flake-parts": {
"inputs": {
"nixpkgs-lib": [
@ -151,49 +129,9 @@
"type": "github"
}
},
"purescript-overlay": {
"inputs": {
"nixpkgs": [
"dream2nix",
"nixpkgs"
],
"slimlock": "slimlock"
},
"locked": {
"lastModified": 1696022621,
"narHash": "sha256-eMjFmsj2G1E0Q5XiibUNgFjTiSz0GxIeSSzzVdoN730=",
"owner": "thomashoneyman",
"repo": "purescript-overlay",
"rev": "047c7933abd6da8aa239904422e22d190ce55ead",
"type": "github"
},
"original": {
"owner": "thomashoneyman",
"repo": "purescript-overlay",
"type": "github"
}
},
"pyproject-nix": {
"flake": false,
"locked": {
"lastModified": 1702448246,
"narHash": "sha256-hFg5s/hoJFv7tDpiGvEvXP0UfFvFEDgTdyHIjDVHu1I=",
"owner": "davhau",
"repo": "pyproject.nix",
"rev": "5a06a2697b228c04dd2f35659b4b659ca74f7aeb",
"type": "github"
},
"original": {
"owner": "davhau",
"ref": "dream2nix",
"repo": "pyproject.nix",
"type": "github"
}
},
"root": {
"inputs": {
"disko": "disko",
"dream2nix": "dream2nix",
"flake-parts": "flake-parts",
"nixos-generators": "nixos-generators",
"nixos-images": "nixos-images",
@ -202,28 +140,6 @@
"treefmt-nix": "treefmt-nix"
}
},
"slimlock": {
"inputs": {
"nixpkgs": [
"dream2nix",
"purescript-overlay",
"nixpkgs"
]
},
"locked": {
"lastModified": 1688610262,
"narHash": "sha256-Wg0ViDotFWGWqKIQzyYCgayeH8s4U1OZcTiWTQYdAp4=",
"owner": "thomashoneyman",
"repo": "slimlock",
"rev": "b5c6cdcaf636ebbebd0a1f32520929394493f1a6",
"type": "github"
},
"original": {
"owner": "thomashoneyman",
"repo": "slimlock",
"type": "github"
}
},
"sops-nix": {
"inputs": {
"nixpkgs": [

View File

@ -21,8 +21,6 @@
flake-parts.inputs.nixpkgs-lib.follows = "nixpkgs";
treefmt-nix.url = "github:numtide/treefmt-nix";
treefmt-nix.inputs.nixpkgs.follows = "nixpkgs";
dream2nix.url = "github:nix-community/dream2nix";
dream2nix.inputs.nixpkgs.follows = "nixpkgs";
};
outputs =

13
pkgs/clan-cli/api.py Normal file
View File

@ -0,0 +1,13 @@
from clan_cli.api import API
def main() -> None:
schema = API.to_json_schema()
print(
f"""export const schema = {schema} as const;
"""
)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,45 @@
from collections.abc import Callable
from typing import Any
class _MethodRegistry:
def __init__(self) -> None:
self._registry: dict[str, Callable] = {}
def register(self, fn: Callable) -> Callable:
self._registry[fn.__name__] = fn
return fn
def to_json_schema(self) -> str:
# Import only when needed
import json
from typing import get_type_hints
from clan_cli.api.util import type_to_dict
api_schema: dict[str, Any] = {
"$comment": "An object containing API methods. ",
"type": "object",
"additionalProperties": False,
"required": ["list_machines"],
"properties": {},
}
for name, func in self._registry.items():
hints = get_type_hints(func)
serialized_hints = {
"argument" if key != "return" else "return": type_to_dict(
value, scope=name + " argument" if key != "return" else "return"
)
for key, value in hints.items()
}
api_schema["properties"][name] = {
"type": "object",
"required": [k for k in serialized_hints.keys()],
"additionalProperties": False,
"properties": {**serialized_hints},
}
return json.dumps(api_schema, indent=2)
API = _MethodRegistry()

View File

@ -0,0 +1,79 @@
import dataclasses
import pathlib
from types import NoneType, UnionType
from typing import Any, Union
def type_to_dict(t: Any, scope: str = "") -> dict:
if t is None:
return {"type": "null"}
if dataclasses.is_dataclass(t):
fields = dataclasses.fields(t)
properties = {
f.name: type_to_dict(f.type, f"{scope} {t.__name__}.{f.name}")
for f in fields
}
required = [pn for pn, pv in properties.items() if "null" not in pv["type"]]
return {
"type": "object",
"properties": properties,
"required": required,
# Dataclasses can only have the specified properties
"additionalProperties": False,
}
elif type(t) is UnionType:
return {
"type": [type_to_dict(arg, scope)["type"] for arg in t.__args__],
}
elif hasattr(t, "__origin__"): # Check if it's a generic type
origin = getattr(t, "__origin__", None)
if origin is None:
# Non-generic user-defined or built-in type
# TODO: handle custom types
raise BaseException("Unhandled Type: ", origin)
elif origin is Union:
return {"type": [type_to_dict(arg, scope)["type"] for arg in t.__args__]}
elif issubclass(origin, list):
return {"type": "array", "items": type_to_dict(t.__args__[0], scope)}
elif issubclass(origin, dict):
return {
"type": "object",
}
raise BaseException(f"Error api type not yet supported {t!s}")
elif isinstance(t, type):
if t is str:
return {"type": "string"}
if t is int:
return {"type": "integer"}
if t is float:
return {"type": "number"}
if t is bool:
return {"type": "boolean"}
if t is object:
return {"type": "object"}
if t is Any:
raise BaseException(
f"Usage of the Any type is not supported for API functions. In: {scope}"
)
if t is pathlib.Path:
return {
# TODO: maybe give it a pattern for URI
"type": "string",
}
# Optional[T] gets internally transformed Union[T,NoneType]
if t is NoneType:
return {"type": "null"}
raise BaseException(f"Error primitive type not supported {t!s}")
else:
raise BaseException(f"Error type not supported {t!s}")

View File

@ -3,12 +3,15 @@ import json
import logging
from pathlib import Path
from clan_cli.api import API
from ..cmd import run
from ..nix import nix_config, nix_eval
log = logging.getLogger(__name__)
@API.register
def list_machines(flake_url: Path | str) -> list[str]:
config = nix_config()
system = config["system"]

View File

@ -57,6 +57,16 @@
cp -r out/* $out
'';
};
clan-ts-api = pkgs.stdenv.mkDerivation {
name = "clan-ts-api";
src = ./.;
buildInputs = [ pkgs.python3 ];
installPhase = ''
python api.py > $out
'';
};
default = self'.packages.clan-cli;
};

View File

@ -1,10 +1,11 @@
import dataclasses
import json
import logging
import sys
import threading
from collections.abc import Callable
from pathlib import Path
from typing import Any, Union
from threading import Lock
from typing import Any
import gi
@ -18,52 +19,12 @@ site_index: Path = (
/ Path("clan_vm_manager/.webui/index.html")
).resolve()
def type_to_dict(t: Any) -> dict:
if dataclasses.is_dataclass(t):
fields = dataclasses.fields(t)
return {
"type": "dataclass",
"name": t.__name__,
"fields": {f.name: type_to_dict(f.type) for f in fields},
}
if hasattr(t, "__origin__"): # Check if it's a generic type
if t.__origin__ is None:
# Non-generic user-defined or built-in type
return {"type": t.__name__}
if t.__origin__ is Union:
return {"type": "union", "of": [type_to_dict(arg) for arg in t.__args__]}
elif issubclass(t.__origin__, list):
return {"type": "list", "item_type": type_to_dict(t.__args__[0])}
elif issubclass(t.__origin__, dict):
return {
"type": "dict",
"key_type": type_to_dict(t.__args__[0]),
"value_type": type_to_dict(t.__args__[1]),
}
elif issubclass(t.__origin__, tuple):
return {
"type": "tuple",
"element_types": [type_to_dict(elem) for elem in t.__args__],
}
elif issubclass(t.__origin__, set):
return {"type": "set", "item_type": type_to_dict(t.__args__[0])}
else:
# Handle other generic types (like Union, Optional)
return {
"type": str(t.__origin__.__name__),
"parameters": [type_to_dict(arg) for arg in t.__args__],
}
elif isinstance(t, type):
return {"type": t.__name__}
else:
return {"type": str(t)}
log = logging.getLogger(__name__)
class WebView:
def __init__(self) -> None:
self.method_registry: dict[str, Callable] = {}
def __init__(self, methods: dict[str, Callable]) -> None:
self.method_registry: dict[str, Callable] = methods
self.webview = WebKit.WebView()
self.manager = self.webview.get_user_content_manager()
@ -74,39 +35,61 @@ class WebView:
self.webview.load_uri(f"file://{site_index}")
def method(self, function: Callable) -> Callable:
# type_hints = get_type_hints(function)
# serialized_hints = {key: type_to_dict(value) for key, value in type_hints.items()}
self.method_registry[function.__name__] = function
return function
# global mutex lock to ensure functions run sequentially
self.mutex_lock = Lock()
self.queue_size = 0
def on_message_received(
self, user_content_manager: WebKit.UserContentManager, message: Any
) -> None:
payload = json.loads(message.to_json(0))
print(f"Received message: {payload}")
method_name = payload["method"]
handler_fn = self.method_registry[method_name]
# Start handler_fn in a new thread
# GLib.idle_add(handler_fn)
log.debug(f"Received message: {payload}")
log.debug(f"Queue size: {self.queue_size} (Wait)")
thread = threading.Thread(
target=self.threaded_handler,
args=(handler_fn, payload.get("data"), method_name),
def threaded_wrapper() -> bool:
"""
Ensures only one function is executed at a time
Wait until there is no other function acquiring the global lock.
Starts a thread with the potentially long running API function within.
"""
if not self.mutex_lock.locked():
thread = threading.Thread(
target=self.threaded_handler,
args=(
handler_fn,
payload.get("data"),
method_name,
),
)
thread.start()
return GLib.SOURCE_REMOVE
return GLib.SOURCE_CONTINUE
GLib.idle_add(
threaded_wrapper,
)
thread.start()
self.queue_size += 1
def threaded_handler(
self, handler_fn: Callable[[Any], Any], data: Any, method_name: str
) -> None:
result = handler_fn(data)
serialized = json.dumps(result)
with self.mutex_lock:
log.debug("Executing... ", method_name)
result = handler_fn(data)
serialized = json.dumps(result)
# Use idle_add to queue the response call to js on the main GTK thread
GLib.idle_add(self.call_js, method_name, serialized)
# 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)
self.queue_size -= 1
log.debug(f"Done: Remaining queue size: {self.queue_size}")
def call_js(self, method_name: str, serialized: str) -> bool:
def return_data_to_js(self, method_name: str, serialized: str) -> bool:
# This function must be run on the main GTK thread to interact with the webview
# result = method_fn(data) # takes very long
# serialized = result
@ -119,7 +102,7 @@ class WebView:
None,
None,
)
return False # Important to return False so that it's not run again
return GLib.SOURCE_REMOVE
def get_webview(self) -> WebKit.WebView:
return self.webview

View File

@ -2,7 +2,7 @@ import logging
import threading
import gi
from clan_cli import machines
from clan_cli.api import API
from clan_cli.history.list import list_history
from clan_vm_manager.components.interfaces import ClanConfig
@ -61,11 +61,7 @@ class MainWindow(Adw.ApplicationWindow):
stack_view.add_named(Details(), "details")
stack_view.add_named(Logs(), "logs")
webview = WebView()
@webview.method
def list_machines(data: None) -> list[str]:
return machines.list.list_machines(".")
webview = WebView(methods=API._registry)
stack_view.add_named(webview.get_webview(), "list")

View File

@ -145,7 +145,7 @@ python3.pkgs.buildPythonApplication rec {
# TODO: place webui in lib/python3.11/site-packages/clan_vm_manager
postInstall = ''
mkdir -p $out/clan_vm_manager/.webui
cp -r ${webview-ui}/dist/* $out/clan_vm_manager/.webui
cp -r ${webview-ui}/lib/node_modules/@clan/webview-ui/dist/* $out/clan_vm_manager/.webui
'';
# Don't leak python packages into a devshell.

View File

@ -56,7 +56,7 @@ mkShell {
# Add the webview-ui to the .webui directory
rm -rf ./clan_vm_manager/.webui/*
mkdir -p ./clan_vm_manager/.webui
cp -a ${webview-ui}/dist/* ./clan_vm_manager/.webui
cp -a ${webview-ui}/lib/node_modules/@clan/webview-ui/dist/* ./clan_vm_manager/.webui
chmod -R +w ./clan_vm_manager/.webui
'';
}

1
pkgs/webview-ui/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
api

View File

@ -9,6 +9,8 @@
"version": "0.0.1",
"license": "MIT",
"dependencies": {
"@types/node": "^20.12.12",
"json-schema-to-ts": "^3.1.0",
"solid-js": "^1.8.11"
},
"devDependencies": {
@ -326,6 +328,17 @@
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/runtime": {
"version": "7.24.5",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz",
"integrity": "sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/template": {
"version": "7.24.0",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz",
@ -1352,6 +1365,14 @@
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
"dev": true
},
"node_modules/@types/node": {
"version": "20.12.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz",
"integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==",
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/ansi-regex": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
@ -2142,6 +2163,18 @@
"node": ">=4"
}
},
"node_modules/json-schema-to-ts": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.0.tgz",
"integrity": "sha512-UeVN/ery4/JeXI8h4rM8yZPxsH+KqPi/84qFxHfTGHZnWnK9D0UU9ZGYO+6XAaJLqCWMiks+ARuFOKAiSxJCHA==",
"dependencies": {
"@babel/runtime": "^7.18.3",
"ts-algebra": "^2.0.0"
},
"engines": {
"node": ">=16"
}
},
"node_modules/json5": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
@ -2582,6 +2615,11 @@
"node": ">=8.10.0"
}
},
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
},
"node_modules/resolve": {
"version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
@ -3008,6 +3046,11 @@
"node": ">=8.0"
}
},
"node_modules/ts-algebra": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz",
"integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="
},
"node_modules/ts-interface-checker": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
@ -3027,6 +3070,11 @@
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
},
"node_modules/update-browserslist-db": {
"version": "1.0.16",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz",

View File

@ -5,9 +5,10 @@
"scripts": {
"start": "vite",
"dev": "vite",
"build": "vite build && npm run convert-html",
"build": "npm run check && vite build && npm run convert-html",
"convert-html": "node gtk.webview.js",
"serve": "vite preview"
"serve": "vite preview",
"check": "tsc --noEmit --skipLibCheck"
},
"license": "MIT",
"devDependencies": {
@ -17,10 +18,12 @@
"solid-devtools": "^0.29.2",
"tailwindcss": "^3.4.3",
"typescript": "^5.3.3",
"vite-plugin-solid": "^2.8.2",
"vite": "^5.0.11"
"vite": "^5.0.11",
"vite-plugin-solid": "^2.8.2"
},
"dependencies": {
"@types/node": "^20.12.12",
"json-schema-to-ts": "^3.1.0",
"solid-js": "^1.8.11"
}
}

View File

@ -1,11 +1,11 @@
import { createSignal, createContext, useContext, JSXElement } from "solid-js";
import { PYAPI } from "./message";
import { pyApi } from "./message";
export const makeCountContext = () => {
const [machines, setMachines] = createSignal<string[]>([]);
const [loading, setLoading] = createSignal(false);
PYAPI.list_machines.receive((machines) => {
pyApi.list_machines.receive((machines) => {
setLoading(false);
setMachines(machines);
});
@ -16,7 +16,7 @@ export const makeCountContext = () => {
getMachines: () => {
// When the gtk function sends its data the loading state will be set to false
setLoading(true);
PYAPI.list_machines.dispatch(null);
pyApi.list_machines.dispatch(".");
},
},
] as const;

View File

@ -1,22 +1,74 @@
const deserialize = (fn: Function) => (str: string) => {
try {
fn(JSON.parse(str));
} catch (e) {
alert(`Error parsing JSON: ${e}`);
}
};
import { FromSchema } from "json-schema-to-ts";
import { schema } from "@/api";
export const PYAPI = {
list_machines: {
dispatch: (data: null) =>
// @ts-ignore
type API = FromSchema<typeof schema>;
type OperationNames = keyof API;
type OperationArgs<T extends OperationNames> = API[T]["argument"];
type OperationResponse<T extends OperationNames> = API[T]["return"];
declare global {
interface Window {
clan: {
[K in OperationNames]: (str: string) => void;
};
webkit: {
messageHandlers: {
gtk: {
postMessage: (message: { method: OperationNames; data: any }) => void;
};
};
};
}
}
function createFunctions<K extends OperationNames>(
operationName: K
): {
dispatch: (args: OperationArgs<K>) => void;
receive: (fn: (response: OperationResponse<K>) => void) => void;
} {
return {
dispatch: (args: OperationArgs<K>) => {
console.log(
`Operation: ${operationName}, Arguments: ${JSON.stringify(args)}`
);
// Send the data to the gtk app
window.webkit.messageHandlers.gtk.postMessage({
method: "list_machines",
data,
}),
receive: (fn: (response: string[]) => void) => {
// @ts-ignore
method: operationName,
data: args,
});
},
receive: (fn: (response: OperationResponse<K>) => void) => {
window.clan.list_machines = deserialize(fn);
},
},
};
}
const operations = schema.properties;
const operationNames = Object.keys(operations) as OperationNames[];
type PyApi = {
[K in OperationNames]: {
dispatch: (args: OperationArgs<K>) => void;
receive: (fn: (response: OperationResponse<K>) => void) => void;
};
};
const deserialize =
<T>(fn: (response: T) => void) =>
(str: string) => {
try {
fn(JSON.parse(str) as T);
} catch (e) {
alert(`Error parsing JSON: ${e}`);
}
};
// Create the API object
const pyApi: PyApi = {} as PyApi;
operationNames.forEach((name) => {
pyApi[name] = createFunctions(name);
});
export { pyApi };

View File

@ -10,6 +10,10 @@
"jsxImportSource": "solid-js",
"types": ["vite/client"],
"noEmit": true,
"resolveJsonModule": true,
"allowJs": true,
"isolatedModules": true,
"paths": {
"@/*": ["./*"]}
},
}

View File

@ -1,8 +1,14 @@
import { defineConfig } from "vite";
import solidPlugin from "vite-plugin-solid";
// import devtools from "solid-devtools/vite";
import path from "node:path";
export default defineConfig({
resolve: {
alias: {
"@": path.resolve(__dirname, "./"), // Adjust the path as needed
},
},
plugins: [
/*
Uncomment the following line to enable solid-devtools.

View File

@ -1,9 +1,14 @@
{ dream2nix, config, ... }:
{
dream2nix,
config,
src,
...
}:
{
imports = [ dream2nix.modules.dream2nix.WIP-nodejs-builder-v3 ];
mkDerivation = {
src = ./app;
inherit src;
};
deps =
@ -14,8 +19,14 @@
WIP-nodejs-builder-v3 = {
packageLockFile = "${config.mkDerivation.src}/package-lock.json";
# config.groups.all.packages.${config.name}.${config.version}.
};
public.out = {
checkPhase = ''
echo "Running tests"
echo "Tests passed"
'';
};
name = "@clan/webview-ui";
version = "0.0.1";
}

View File

@ -1,33 +1,32 @@
{ inputs, ... }:
{ ... }:
{
perSystem =
{ pkgs, config, ... }:
{
system,
pkgs,
config,
...
}:
let
node_modules-dev = config.packages.webview-ui.prepared-dev;
in
{
packages.webview-ui = inputs.dream2nix.lib.evalModules {
packageSets.nixpkgs = inputs.dream2nix.inputs.nixpkgs.legacyPackages.${system};
modules = [ ./default.nix ];
packages.webview-ui = pkgs.buildNpmPackage {
pname = "clan-webview-ui";
version = "0.0.1";
src = ./app;
# npmDepsHash = "sha256-bRD2vzijhdOOvcEi6XaG/neSqhkVQMqIX/8bxvRQkTc=";
npmDeps = pkgs.fetchNpmDeps {
src = ./app;
hash = "sha256-bRD2vzijhdOOvcEi6XaG/neSqhkVQMqIX/8bxvRQkTc=";
};
# The prepack script runs the build script, which we'd rather do in the build phase.
npmPackFlags = [ "--ignore-scripts" ];
preBuild = ''
mkdir -p api
cat ${config.packages.clan-ts-api} > api/index.ts
'';
};
devShells.webview-ui = pkgs.mkShell {
inputsFrom = [ config.packages.webview-ui.out ];
inputsFrom = [ config.packages.webview-ui ];
shellHook = ''
ID=${node_modules-dev}
currID=$(cat .dream2nix/.node_modules_id 2> /dev/null)
mkdir -p .dream2nix
if [[ "$ID" != "$currID" || ! -d "app/node_modules" ]];
then
${pkgs.rsync}/bin/rsync -a --chmod=ug+w --delete ${node_modules-dev}/node_modules/ ./app/node_modules/
echo -n $ID > .dream2nix/.node_modules_id
echo "Ok: node_modules updated"
fi
mkdir -p ./app/api
cat ${config.packages.clan-ts-api} > ./app/api/index.ts
'';
};
};