From 3e9ebbc90f986d2ab07d035852974fcfc56c5df7 Mon Sep 17 00:00:00 2001 From: Qubasa Date: Fri, 2 Aug 2024 17:51:45 +0200 Subject: [PATCH 1/2] clan-app: Finish flash view. clan-cli: Flash cli now verifies if language and keymap are valid. --- pkgs/clan-app/clan_app/api/file.py | 37 ++- pkgs/clan-app/clan_app/windows/main_window.py | 2 + pkgs/clan-cli/clan_cli/api/cli.py | 13 + pkgs/clan-cli/clan_cli/api/directory.py | 25 +- pkgs/clan-cli/clan_cli/api/serde.py | 2 +- pkgs/clan-cli/clan_cli/flash.py | 143 ++++++++--- pkgs/clan-cli/tests/test_deserializers.py | 50 ++++ .../clan_vm_manager/components/executor.py | 11 +- pkgs/webview-ui/.vscode/settings.json | 8 +- pkgs/webview-ui/app/README.md | 21 +- pkgs/webview-ui/app/gtk.webview.js | 2 +- pkgs/webview-ui/app/src/App.tsx | 4 +- pkgs/webview-ui/app/src/api.ts | 36 ++- .../app/src/components/MachineListItem.tsx | 26 +- pkgs/webview-ui/app/src/floating/index.tsx | 12 +- pkgs/webview-ui/app/src/index.tsx | 4 +- pkgs/webview-ui/app/src/layout/layout.tsx | 3 +- .../app/src/routes/blockdevices/view.tsx | 52 ++-- .../app/src/routes/clan/clanDetails.tsx | 10 +- .../app/src/routes/clan/editClan.tsx | 8 +- pkgs/webview-ui/app/src/routes/flash/view.tsx | 235 +++++++++++++++--- pkgs/webview-ui/app/src/routes/hosts/view.tsx | 6 +- .../app/src/routes/machines/view.tsx | 14 +- .../app/src/routes/settings/index.tsx | 10 +- pkgs/webview-ui/app/tests/types.test.ts | 16 +- pkgs/webview-ui/app/tsconfig.json | 5 +- pkgs/webview-ui/app/util.ts | 3 +- 27 files changed, 556 insertions(+), 202 deletions(-) create mode 100755 pkgs/clan-cli/clan_cli/api/cli.py diff --git a/pkgs/clan-app/clan_app/api/file.py b/pkgs/clan-app/clan_app/api/file.py index a86f23874..b6e2b22d8 100644 --- a/pkgs/clan-app/clan_app/api/file.py +++ b/pkgs/clan-app/clan_app/api/file.py @@ -4,6 +4,8 @@ import gi gi.require_version("Gtk", "4.0") import logging +from pathlib import Path +from typing import Any from clan_cli.api import ErrorDataClass, SuccessDataClass from clan_cli.api.directory import FileRequest @@ -17,7 +19,7 @@ log = logging.getLogger(__name__) # This implements the abstract function open_file with one argument, file_request, # which is a FileRequest object and returns a string or None. class open_file( - ImplFunc[[FileRequest, str], SuccessDataClass[str | None] | ErrorDataClass] + ImplFunc[[FileRequest, str], SuccessDataClass[list[str] | None] | ErrorDataClass] ): def __init__(self) -> None: super().__init__() @@ -27,7 +29,7 @@ class open_file( try: gfile = file_dialog.open_finish(task) if gfile: - selected_path = gfile.get_path() + selected_path = [gfile.get_path()] self.returns( SuccessDataClass( op_key=op_key, data=selected_path, status="success" @@ -36,11 +38,26 @@ class open_file( except Exception as e: print(f"Error getting selected file or directory: {e}") + def on_file_select_multiple( + file_dialog: Gtk.FileDialog, task: Gio.Task + ) -> None: + try: + gfiles: Any = file_dialog.open_multiple_finish(task) + if gfiles: + selected_paths = [gfile.get_path() for gfile in gfiles] + self.returns( + SuccessDataClass( + op_key=op_key, data=selected_paths, status="success" + ) + ) + except Exception as e: + print(f"Error getting selected files: {e}") + def on_folder_select(file_dialog: Gtk.FileDialog, task: Gio.Task) -> None: try: gfile = file_dialog.select_folder_finish(task) if gfile: - selected_path = gfile.get_path() + selected_path = [gfile.get_path()] self.returns( SuccessDataClass( op_key=op_key, data=selected_path, status="success" @@ -53,7 +70,7 @@ class open_file( try: gfile = file_dialog.save_finish(task) if gfile: - selected_path = gfile.get_path() + selected_path = [gfile.get_path()] self.returns( SuccessDataClass( op_key=op_key, data=selected_path, status="success" @@ -90,9 +107,21 @@ class open_file( filters.append(file_filters) dialog.set_filters(filters) + if file_request.initial_file: + p = Path(file_request.initial_file).expanduser() + f = Gio.File.new_for_path(str(p)) + dialog.set_initial_file(f) + + if file_request.initial_folder: + p = Path(file_request.initial_folder).expanduser() + f = Gio.File.new_for_path(str(p)) + dialog.set_initial_folder(f) + # if select_folder if file_request.mode == "select_folder": dialog.select_folder(callback=on_folder_select) + if file_request.mode == "open_multiple_files": + dialog.open_multiple(callback=on_file_select_multiple) elif file_request.mode == "open_file": dialog.open(callback=on_file_select) elif file_request.mode == "save": diff --git a/pkgs/clan-app/clan_app/windows/main_window.py b/pkgs/clan-app/clan_app/windows/main_window.py index d540dbc1e..cd1c42971 100644 --- a/pkgs/clan-app/clan_app/windows/main_window.py +++ b/pkgs/clan-app/clan_app/windows/main_window.py @@ -1,4 +1,5 @@ import logging +import os import gi from clan_cli.api import API @@ -47,3 +48,4 @@ class MainWindow(Adw.ApplicationWindow): def on_destroy(self, source: "Adw.ApplicationWindow") -> None: log.debug("Destroying Adw.ApplicationWindow") + os._exit(0) diff --git a/pkgs/clan-cli/clan_cli/api/cli.py b/pkgs/clan-cli/clan_cli/api/cli.py new file mode 100755 index 000000000..763d5ec24 --- /dev/null +++ b/pkgs/clan-cli/clan_cli/api/cli.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 + +import argparse +import json + +from clan_cli.api import API + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Debug the API.") + args = parser.parse_args() + + schema = API.to_json_schema() + print(json.dumps(schema, indent=4)) diff --git a/pkgs/clan-cli/clan_cli/api/directory.py b/pkgs/clan-cli/clan_cli/api/directory.py index c7f95b87f..d47225b3b 100644 --- a/pkgs/clan-cli/clan_cli/api/directory.py +++ b/pkgs/clan-cli/clan_cli/api/directory.py @@ -12,24 +12,26 @@ from . import API @dataclass class FileFilter: - title: str | None - mime_types: list[str] | None - patterns: list[str] | None - suffixes: list[str] | None + title: str | None = field(default=None) + mime_types: list[str] | None = field(default=None) + patterns: list[str] | None = field(default=None) + suffixes: list[str] | None = field(default=None) @dataclass class FileRequest: # Mode of the os dialog window - mode: Literal["open_file", "select_folder", "save"] + mode: Literal["open_file", "select_folder", "save", "open_multiple_files"] # Title of the os dialog window - title: str | None = None + title: str | None = field(default=None) # Pre-applied filters for the file dialog - filters: FileFilter | None = None + filters: FileFilter | None = field(default=None) + initial_file: str | None = field(default=None) + initial_folder: str | None = field(default=None) @API.register_abstract -def open_file(file_request: FileRequest) -> str | None: +def open_file(file_request: FileRequest) -> list[str] | None: """ Abstract api method to open a file dialog window. It must return the name of the selected file or None if no file was selected. @@ -88,6 +90,7 @@ def get_directory(current_path: str) -> Directory: @dataclass class BlkInfo: name: str + path: str rm: str size: str ro: bool @@ -103,6 +106,7 @@ class Blockdevices: def blk_from_dict(data: dict) -> BlkInfo: return BlkInfo( name=data["name"], + path=data["path"], rm=data["rm"], size=data["size"], ro=data["ro"], @@ -117,7 +121,10 @@ def show_block_devices() -> Blockdevices: Abstract api method to show block devices. It must return a list of block devices. """ - cmd = nix_shell(["nixpkgs#util-linux"], ["lsblk", "--json"]) + cmd = nix_shell( + ["nixpkgs#util-linux"], + ["lsblk", "--json", "--output", "PATH,NAME,RM,SIZE,RO,MOUNTPOINTS,TYPE"], + ) proc = run_no_stdout(cmd) res = proc.stdout.strip() diff --git a/pkgs/clan-cli/clan_cli/api/serde.py b/pkgs/clan-cli/clan_cli/api/serde.py index 298cbe9de..ce7089535 100644 --- a/pkgs/clan-cli/clan_cli/api/serde.py +++ b/pkgs/clan-cli/clan_cli/api/serde.py @@ -237,7 +237,7 @@ def from_dict(t: type[T], data: dict[str, Any], path: list[str] = []) -> T: if field_name not in field_values: formatted_path = " ".join(path) raise ClanError( - f"Required field missing: '{field_name}' in {t} {formatted_path}, got Value: {data}" + f"Default value missing for: '{field_name}' in {t} {formatted_path}, got Value: {data}" ) return t(**field_values) # type: ignore diff --git a/pkgs/clan-cli/clan_cli/flash.py b/pkgs/clan-cli/clan_cli/flash.py index f846068a5..3c9df9616 100644 --- a/pkgs/clan-cli/clan_cli/flash.py +++ b/pkgs/clan-cli/clan_cli/flash.py @@ -6,7 +6,7 @@ import os import shutil import textwrap from collections.abc import Sequence -from dataclasses import dataclass +from dataclasses import dataclass, field from pathlib import Path from tempfile import TemporaryDirectory from typing import Any @@ -19,23 +19,105 @@ from .completions import add_dynamic_completer, complete_machines from .errors import ClanError from .facts.secret_modules import SecretStoreBase from .machines.machines import Machine -from .nix import nix_shell +from .nix import nix_build, nix_shell log = logging.getLogger(__name__) +@dataclass +class SystemConfig: + language: str | None = field(default=None) + keymap: str | None = field(default=None) + ssh_keys_path: list[str] | None = field(default=None) + + +@API.register +def list_possible_keymaps() -> list[str]: + cmd = nix_build(["nixpkgs#kbd"]) + result = run(cmd, log=Log.STDERR, error_msg="Failed to find kbdinfo") + keymaps_dir = Path(result.stdout.strip()) / "share" / "keymaps" + + if not keymaps_dir.exists(): + raise FileNotFoundError(f"Keymaps directory '{keymaps_dir}' does not exist.") + + keymap_files = [] + + for root, _, files in os.walk(keymaps_dir): + for file in files: + if file.endswith(".map.gz"): + # Remove '.map.gz' ending + name_without_ext = file[:-7] + keymap_files.append(name_without_ext) + + return keymap_files + + +@API.register +def list_possible_languages() -> list[str]: + cmd = nix_build(["nixpkgs#glibcLocales"]) + result = run(cmd, log=Log.STDERR, error_msg="Failed to find glibc locales") + locale_file = Path(result.stdout.strip()) / "share" / "i18n" / "SUPPORTED" + + if not locale_file.exists(): + raise FileNotFoundError(f"Locale file '{locale_file}' does not exist.") + + with locale_file.open() as f: + lines = f.readlines() + + languages = [] + for line in lines: + if line.startswith("#"): + continue + if "SUPPORTED-LOCALES" in line: + continue + # Split by '/' and take the first part + language = line.split("/")[0].strip() + languages.append(language) + + return languages + + @API.register def flash_machine( machine: Machine, *, mode: str, disks: dict[str, str], - system_config: dict[str, Any], + system_config: SystemConfig, dry_run: bool, write_efi_boot_entries: bool, debug: bool, extra_args: list[str] = [], ) -> None: + system_config_nix: dict[str, Any] = {} + + if system_config.language: + if system_config.language not in list_possible_languages(): + raise ClanError( + f"Language '{system_config.language}' is not a valid language. " + f"Run 'clan flash --list-languages' to see a list of possible languages." + ) + system_config_nix["i18n"] = {"defaultLocale": system_config.language} + + if system_config.keymap: + if system_config.keymap not in list_possible_keymaps(): + raise ClanError( + f"Keymap '{system_config.keymap}' is not a valid keymap. " + f"Run 'clan flash --list-keymaps' to see a list of possible keymaps." + ) + system_config_nix["console"] = {"keyMap": system_config.keymap} + + if system_config.ssh_keys_path: + root_keys = [] + for key_path in map(lambda x: Path(x), system_config.ssh_keys_path): + try: + root_keys.append(key_path.read_text()) + except OSError as e: + raise ClanError(f"Cannot read SSH public key file: {key_path}: {e}") + system_config_nix["users"] = { + "users": {"root": {"openssh": {"authorizedKeys": {"keys": root_keys}}}} + } + secret_facts_module = importlib.import_module(machine.secret_facts_module) secret_facts_store: SecretStoreBase = secret_facts_module.SecretStore( machine=machine @@ -76,7 +158,7 @@ def flash_machine( disko_install.extend( [ "--system-config", - json.dumps(system_config), + json.dumps(system_config_nix), ] ) disko_install.extend(["--option", "dry-run", "true"]) @@ -94,15 +176,13 @@ class FlashOptions: flake: FlakeId machine: str disks: dict[str, str] - ssh_keys_path: list[Path] dry_run: bool confirm: bool debug: bool mode: str - language: str - keymap: str write_efi_boot_entries: bool nix_options: list[str] + system_config: SystemConfig class AppendDiskAction(argparse.Action): @@ -126,17 +206,29 @@ def flash_command(args: argparse.Namespace) -> None: flake=args.flake, machine=args.machine, disks=args.disk, - ssh_keys_path=args.ssh_pubkey, dry_run=args.dry_run, confirm=not args.yes, debug=args.debug, mode=args.mode, - language=args.language, - keymap=args.keymap, + system_config=SystemConfig( + language=args.language, + keymap=args.keymap, + ssh_keys_path=args.ssh_pubkey, + ), write_efi_boot_entries=args.write_efi_boot_entries, nix_options=args.option, ) + if args.list_languages: + for language in list_possible_languages(): + print(language) + return + + if args.list_keymaps: + for keymap in list_possible_keymaps(): + print(keymap) + return + machine = Machine(opts.machine, flake=opts.flake) if opts.confirm and not opts.dry_run: disk_str = ", ".join(f"{name}={device}" for name, device in opts.disks.items()) @@ -148,28 +240,11 @@ def flash_command(args: argparse.Namespace) -> None: if ask != "y": return - extra_config: dict[str, Any] = {} - if opts.ssh_keys_path: - root_keys = [] - for key_path in opts.ssh_keys_path: - try: - root_keys.append(key_path.read_text()) - except OSError as e: - raise ClanError(f"Cannot read SSH public key file: {key_path}: {e}") - extra_config["users"] = { - "users": {"root": {"openssh": {"authorizedKeys": {"keys": root_keys}}}} - } - if opts.keymap: - extra_config["console"] = {"keyMap": opts.keymap} - - if opts.language: - extra_config["i18n"] = {"defaultLocale": opts.language} - flash_machine( machine, mode=opts.mode, disks=opts.disks, - system_config=extra_config, + system_config=opts.system_config, dry_run=opts.dry_run, debug=opts.debug, write_efi_boot_entries=opts.write_efi_boot_entries, @@ -221,6 +296,18 @@ def register_parser(parser: argparse.ArgumentParser) -> None: type=str, help="system language", ) + parser.add_argument( + "--list-languages", + help="List possible languages", + default=False, + action="store_true", + ) + parser.add_argument( + "--list-keymaps", + help="List possible keymaps", + default=False, + action="store_true", + ) parser.add_argument( "--keymap", type=str, diff --git a/pkgs/clan-cli/tests/test_deserializers.py b/pkgs/clan-cli/tests/test_deserializers.py index e858b5398..24da254cb 100644 --- a/pkgs/clan-cli/tests/test_deserializers.py +++ b/pkgs/clan-cli/tests/test_deserializers.py @@ -19,6 +19,7 @@ from clan_cli.inventory import ( ServiceBorgbackupRoleServer, ServiceMeta, ) +from clan_cli.machines import machines def test_simple() -> None: @@ -73,6 +74,55 @@ def test_nested() -> None: assert from_dict(Person, person_dict) == expected_person +def test_nested_nullable() -> None: + @dataclass + class SystemConfig: + language: str | None = field(default=None) + keymap: str | None = field(default=None) + ssh_keys_path: list[str] | None = field(default=None) + + @dataclass + class FlashOptions: + machine: machines.Machine + mode: str + disks: dict[str, str] + system_config: SystemConfig + dry_run: bool + write_efi_boot_entries: bool + debug: bool + + data = { + "machine": { + "name": "flash-installer", + "flake": {"loc": "git+https://git.clan.lol/clan/clan-core"}, + }, + "mode": "format", + "disks": {"main": "/dev/sda"}, + "system_config": {"language": "en_US.utf-8", "keymap": "en"}, + "dry_run": False, + "write_efi_boot_entries": False, + "debug": False, + "op_key": "jWnTSHwYhSgr7Qz3u4ppD", + } + + expected = FlashOptions( + machine=machines.Machine( + name="flash-installer", + flake=machines.FlakeId("git+https://git.clan.lol/clan/clan-core"), + ), + mode="format", + disks={"main": "/dev/sda"}, + system_config=SystemConfig( + language="en_US.utf-8", keymap="en", ssh_keys_path=None + ), + dry_run=False, + write_efi_boot_entries=False, + debug=False, + ) + + assert from_dict(FlashOptions, data) == expected + + def test_simple_field_missing() -> None: @dataclass class Person: diff --git a/pkgs/clan-vm-manager/clan_vm_manager/components/executor.py b/pkgs/clan-vm-manager/clan_vm_manager/components/executor.py index 7ca59d52f..b97013c50 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/components/executor.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/components/executor.py @@ -1,19 +1,14 @@ +import dataclasses import logging +import multiprocessing as mp import os import signal import sys import traceback +from collections.abc import Callable from pathlib import Path from typing import Any -import gi - -gi.require_version("GdkPixbuf", "2.0") - -import dataclasses -import multiprocessing as mp -from collections.abc import Callable - log = logging.getLogger(__name__) diff --git a/pkgs/webview-ui/.vscode/settings.json b/pkgs/webview-ui/.vscode/settings.json index 335c251a2..f9c1a6bc2 100644 --- a/pkgs/webview-ui/.vscode/settings.json +++ b/pkgs/webview-ui/.vscode/settings.json @@ -1,5 +1,7 @@ { - "typescript.tsdk": "node_modules/typescript/lib", - "tailwindCSS.experimental.classRegex": [["cx\\(([^)]*)\\)", "[\"'`]([^\"'`]*)[\"'`]"]], - "editor.wordWrap": "on" + "typescript.tsdk": "node_modules/typescript/lib", + "tailwindCSS.experimental.classRegex": [ + ["cx\\(([^)]*)\\)", "[\"'`]([^\"'`]*)[\"'`]"] + ], + "editor.wordWrap": "on" } diff --git a/pkgs/webview-ui/app/README.md b/pkgs/webview-ui/app/README.md index 6a1764536..f171a99cd 100644 --- a/pkgs/webview-ui/app/README.md +++ b/pkgs/webview-ui/app/README.md @@ -1,8 +1,10 @@ ## Usage -Those templates dependencies are maintained via [pnpm](https://pnpm.io) via `pnpm up -Lri`. +Those templates dependencies are maintained via [pnpm](https://pnpm.io) via +`pnpm up -Lri`. -This is the reason you see a `pnpm-lock.yaml`. That being said, any package manager will work. This file can be safely be removed once you clone a template. +This is the reason you see a `pnpm-lock.yaml`. That being said, any package +manager will work. This file can be safely be removed once you clone a template. ```bash $ npm install # or pnpm install or yarn install @@ -16,19 +18,20 @@ In the project directory, you can run: ### `npm run dev` or `npm start` -Runs the app in the development mode.
-Open [http://localhost:3000](http://localhost:3000) to view it in the browser. +Runs the app in the development mode.
Open +[http://localhost:3000](http://localhost:3000) to view it in the browser. The page will reload if you make edits.
### `npm run build` -Builds the app for production to the `dist` folder.
-It correctly bundles Solid in production mode and optimizes the build for the best performance. +Builds the app for production to the `dist` folder.
It correctly bundles +Solid in production mode and optimizes the build for the best performance. -The build is minified and the filenames include the hashes.
-Your app is ready to be deployed! +The build is minified and the filenames include the hashes.
Your app is +ready to be deployed! ## Deployment -You can deploy the `dist` folder to any static host provider (netlify, surge, now, etc.) +You can deploy the `dist` folder to any static host provider (netlify, surge, +now, etc.) diff --git a/pkgs/webview-ui/app/gtk.webview.js b/pkgs/webview-ui/app/gtk.webview.js index 522d93cc6..a768e1b5e 100644 --- a/pkgs/webview-ui/app/gtk.webview.js +++ b/pkgs/webview-ui/app/gtk.webview.js @@ -43,7 +43,7 @@ fs.readFile(manifestPath, { encoding: "utf8" }, (err, data) => { console.log(`Rewriting CSS url(): ${asset.url} to ${res}`); return res; }, - }) + }), ) .process(css, { from: `dist/${cssEntry}`, diff --git a/pkgs/webview-ui/app/src/App.tsx b/pkgs/webview-ui/app/src/App.tsx index 016cff09f..ea0b2742f 100644 --- a/pkgs/webview-ui/app/src/App.tsx +++ b/pkgs/webview-ui/app/src/App.tsx @@ -1,4 +1,4 @@ -import { createEffect, createSignal, type Component } from "solid-js"; +import { type Component, createEffect, createSignal } from "solid-js"; import { Layout } from "./layout/layout"; import { Route, Router } from "./Routes"; import { Toaster } from "solid-toast"; @@ -18,7 +18,7 @@ const [activeURI, setActiveURI] = makePersisted( { name: "activeURI", storage: localStorage, - } + }, ); export { activeURI, setActiveURI }; diff --git a/pkgs/webview-ui/app/src/api.ts b/pkgs/webview-ui/app/src/api.ts index dbe19ad9b..820b28b76 100644 --- a/pkgs/webview-ui/app/src/api.ts +++ b/pkgs/webview-ui/app/src/api.ts @@ -58,11 +58,11 @@ const registry: ObserverRegistry = operationNames.reduce( ...acc, [opName]: {}, }), - {} as ObserverRegistry + {} as ObserverRegistry, ); function createFunctions( - operationName: K + operationName: K, ): { dispatch: (args: OperationArgs) => void; receive: (fn: (response: OperationResponse) => void, id: string) => void; @@ -104,7 +104,7 @@ function download(filename: string, text: string) { const element = document.createElement("a"); element.setAttribute( "href", - "data:text/plain;charset=utf-8," + encodeURIComponent(text) + "data:text/plain;charset=utf-8," + encodeURIComponent(text), ); element.setAttribute("download", filename); @@ -118,7 +118,7 @@ function download(filename: string, text: string) { export const callApi = ( method: K, - args: OperationArgs + args: OperationArgs, ) => { return new Promise>((resolve, reject) => { const id = nanoid(); @@ -134,21 +134,19 @@ export const callApi = ( }); }; -const deserialize = - (fn: (response: T) => void) => - (str: string) => { - try { - const r = JSON.parse(str) as T; - console.log("Received: ", r); - fn(r); - } catch (e) { - console.log("Error parsing JSON: ", e); - window.localStorage.setItem("error", str); - console.error(str); - console.error("See localStorage 'error'"); - alert(`Error parsing JSON: ${e}`); - } - }; +const deserialize = (fn: (response: T) => void) => (str: string) => { + try { + const r = JSON.parse(str) as T; + console.log("Received: ", r); + fn(r); + } catch (e) { + console.log("Error parsing JSON: ", e); + window.localStorage.setItem("error", str); + console.error(str); + console.error("See localStorage 'error'"); + alert(`Error parsing JSON: ${e}`); + } +}; // Create the API object diff --git a/pkgs/webview-ui/app/src/components/MachineListItem.tsx b/pkgs/webview-ui/app/src/components/MachineListItem.tsx index 05ee9b61f..b53c55ae5 100644 --- a/pkgs/webview-ui/app/src/components/MachineListItem.tsx +++ b/pkgs/webview-ui/app/src/components/MachineListItem.tsx @@ -1,5 +1,5 @@ -import { Match, Show, Switch, createSignal } from "solid-js"; -import { ErrorData, SuccessData, pyApi } from "../api"; +import { createSignal, Match, Show, Switch } from "solid-js"; +import { ErrorData, pyApi, SuccessData } from "../api"; type MachineDetails = SuccessData<"list_machines">["data"][string]; @@ -94,21 +94,18 @@ export const MachineListItem = (props: MachineListItemProps) => {
System: - {hwInfo()[name]?.system ? ( - {hwInfo()[name]?.system} - ) : ( - Not set - )} + {hwInfo()[name]?.system + ? {hwInfo()[name]?.system} + : Not set}
Target Host: - {deploymentInfo()[name] ? ( - {deploymentInfo()[name]} - ) : ( - Not set - )} - {/* {deploymentInfo()[name]} + : Not set} + { + /*
}> @@ -119,7 +116,8 @@ export const MachineListItem = (props: MachineListItemProps) => { } > {(i) => + i()} - */} + */ + }
{/* Show only the first error at the bottom */} diff --git a/pkgs/webview-ui/app/src/floating/index.tsx b/pkgs/webview-ui/app/src/floating/index.tsx index 96267c82f..71ff78bbb 100644 --- a/pkgs/webview-ui/app/src/floating/index.tsx +++ b/pkgs/webview-ui/app/src/floating/index.tsx @@ -13,9 +13,9 @@ export interface UseFloatingOptions< whileElementsMounted?: ( reference: R, floating: F, - update: () => void - // eslint-disable-next-line @typescript-eslint/no-invalid-void-type - ) => void | (() => void); + update: () => void, + ) => // eslint-disable-next-line @typescript-eslint/no-invalid-void-type + void | (() => void); } interface UseFloatingState extends Omit { @@ -30,7 +30,7 @@ export interface UseFloatingResult extends UseFloatingState { export function useFloating( reference: () => R | undefined | null, floating: () => F | undefined | null, - options?: UseFloatingOptions + options?: UseFloatingOptions, ): UseFloatingResult { const placement = () => options?.placement ?? "bottom"; const strategy = () => options?.strategy ?? "absolute"; @@ -77,7 +77,7 @@ export function useFloating( }, (err) => { setError(err); - } + }, ); } } @@ -95,7 +95,7 @@ export function useFloating( const cleanup = options.whileElementsMounted( currentReference, currentFloating, - update + update, ); if (cleanup) { diff --git a/pkgs/webview-ui/app/src/index.tsx b/pkgs/webview-ui/app/src/index.tsx index 4cc82a85d..6dff3cd3a 100644 --- a/pkgs/webview-ui/app/src/index.tsx +++ b/pkgs/webview-ui/app/src/index.tsx @@ -13,7 +13,7 @@ window.clan = window.clan || {}; if (import.meta.env.DEV && !(root instanceof HTMLElement)) { throw new Error( - "Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?" + "Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?", ); } @@ -30,5 +30,5 @@ render( ), // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - root! + root!, ); diff --git a/pkgs/webview-ui/app/src/layout/layout.tsx b/pkgs/webview-ui/app/src/layout/layout.tsx index 616faa57d..c1a48497e 100644 --- a/pkgs/webview-ui/app/src/layout/layout.tsx +++ b/pkgs/webview-ui/app/src/layout/layout.tsx @@ -32,7 +32,8 @@ export const Layout: Component = (props) => { for="toplevel-drawer" aria-label="close sidebar" class="drawer-overlay" - > + > + diff --git a/pkgs/webview-ui/app/src/routes/blockdevices/view.tsx b/pkgs/webview-ui/app/src/routes/blockdevices/view.tsx index 31b035f32..f22170791 100644 --- a/pkgs/webview-ui/app/src/routes/blockdevices/view.tsx +++ b/pkgs/webview-ui/app/src/routes/blockdevices/view.tsx @@ -27,35 +27,35 @@ export const BlockDevicesView: Component = () => {
- {isFetching ? ( - - ) : ( - - {(devices) => ( - - {(device) => ( -
-
-
Name
-
- {" "} - storage{" "} - {device.name} + {isFetching + ? + : ( + + {(devices) => ( + + {(device) => ( +
+
+
Name
+
+ {" "} + storage{" "} + {device.name} +
+
-
-
-
-
Size
-
{device.size}
-
+
+
Size
+
{device.size}
+
+
-
- )} - - )} - - )} + )} + + )} + + )}
); diff --git a/pkgs/webview-ui/app/src/routes/clan/clanDetails.tsx b/pkgs/webview-ui/app/src/routes/clan/clanDetails.tsx index fa6e872fe..fec7a8891 100644 --- a/pkgs/webview-ui/app/src/routes/clan/clanDetails.tsx +++ b/pkgs/webview-ui/app/src/routes/clan/clanDetails.tsx @@ -1,10 +1,10 @@ -import { OperationResponse, callApi, pyApi } from "@/src/api"; +import { callApi, OperationResponse, pyApi } from "@/src/api"; import { Show } from "solid-js"; import { - SubmitHandler, createForm, required, reset, + SubmitHandler, } from "@modular-forms/solid"; import toast from "solid-toast"; import { setActiveURI, setRoute } from "@/src/App"; @@ -43,7 +43,7 @@ export const ClanForm = () => { (async () => { await callApi("create_clan", { options: { - directory: target_dir, + directory: target_dir[0], template_url, initial: { meta, @@ -52,14 +52,14 @@ export const ClanForm = () => { }, }, }); - setActiveURI(target_dir); + setActiveURI(target_dir[0]); setRoute("machines"); })(), { loading: "Creating clan...", success: "Clan Successfully Created", error: "Failed to create clan", - } + }, ); reset(formStore); }; diff --git a/pkgs/webview-ui/app/src/routes/clan/editClan.tsx b/pkgs/webview-ui/app/src/routes/clan/editClan.tsx index 6494d1e7d..fc437c409 100644 --- a/pkgs/webview-ui/app/src/routes/clan/editClan.tsx +++ b/pkgs/webview-ui/app/src/routes/clan/editClan.tsx @@ -1,10 +1,10 @@ -import { OperationResponse, callApi, pyApi } from "@/src/api"; -import { Accessor, Show, Switch, Match } from "solid-js"; +import { callApi, OperationResponse, pyApi } from "@/src/api"; +import { Accessor, Match, Show, Switch } from "solid-js"; import { - SubmitHandler, createForm, required, reset, + SubmitHandler, } from "@modular-forms/solid"; import toast from "solid-toast"; import { createQuery } from "@tanstack/solid-query"; @@ -65,7 +65,7 @@ export const FinalEditClanForm = (props: FinalEditClanFormProps) => { loading: "Updating clan...", success: "Clan Successfully updated", error: "Failed to update clan", - } + }, ); props.done(); }; diff --git a/pkgs/webview-ui/app/src/routes/flash/view.tsx b/pkgs/webview-ui/app/src/routes/flash/view.tsx index 31a2275bc..ca40137dc 100644 --- a/pkgs/webview-ui/app/src/routes/flash/view.tsx +++ b/pkgs/webview-ui/app/src/routes/flash/view.tsx @@ -1,24 +1,24 @@ import { route } from "@/src/App"; -import { OperationArgs, OperationResponse, callApi, pyApi } from "@/src/api"; -import { SubmitHandler, createForm, required } from "@modular-forms/solid"; +import { callApi, OperationArgs, OperationResponse, pyApi } from "@/src/api"; +import { + createForm, + required, + setValue, + SubmitHandler, +} from "@modular-forms/solid"; import { createQuery } from "@tanstack/solid-query"; -import { For, createSignal } from "solid-js"; +import { createEffect, createSignal, For } from "solid-js"; import { effect } from "solid-js/web"; -// type FlashMachineArgs = { -// machine: Omit["machine"], "cached_deployment">; -// } & Omit, "machine">, "system_config">; - -// type FlashMachineArgs = OperationArgs<"flash_machine">; - -// type k = keyof FlashMachineArgs; -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions type FlashFormValues = { machine: { - name: string; + devicePath: string; flake: string; }; disk: string; + language: string; + keymap: string; + sshKeys: string[]; }; type BlockDevices = Extract< @@ -28,10 +28,39 @@ type BlockDevices = Extract< export const Flash = () => { const [formStore, { Form, Field }] = createForm({}); + const [sshKeys, setSshKeys] = createSignal([]); + const [isFlashing, setIsFlashing] = createSignal(false); + + const selectSshPubkey = async () => { + try { + const loc = await callApi("open_file", { + file_request: { + title: "Select SSH Key", + mode: "open_multiple_files", + filters: { patterns: ["*.pub"] }, + initial_folder: "~/.ssh", + }, + }); + console.log({ loc }, loc.status); + if (loc.status === "success" && loc.data) { + setSshKeys(loc.data); + return loc.data; + } + } catch (e) { + // + } + }; + + // Create an effect that updates the form when externalUsername changes + createEffect(() => { + const newSshKeys = sshKeys(); + if (newSshKeys) { + setValue(formStore, "sshKeys", newSshKeys); + } + }); const { data: devices, - refetch: loadDevices, isFetching, } = createQuery(() => ({ queryKey: ["block_devices"], @@ -40,20 +69,61 @@ export const Flash = () => { if (result.status === "error") throw new Error("Failed to fetch data"); return result.data; }, - staleTime: 1000 * 60 * 1, // 1 minutes + staleTime: 1000 * 60 * 2, // 1 minutes + })); + + const { + data: keymaps, + isFetching: isFetchingKeymaps, + } = createQuery(() => ({ + queryKey: ["list_keymaps"], + queryFn: async () => { + const result = await callApi("list_possible_keymaps", {}); + if (result.status === "error") throw new Error("Failed to fetch data"); + return result.data; + }, + staleTime: 1000 * 60 * 15, // 15 minutes + })); + + const { + data: languages, + isFetching: isFetchingLanguages, + } = createQuery(() => ({ + queryKey: ["list_languages"], + queryFn: async () => { + const result = await callApi("list_possible_languages", {}); + if (result.status === "error") throw new Error("Failed to fetch data"); + return result.data; + }, + staleTime: 1000 * 60 * 15, // 15 minutes })); const handleSubmit = async (values: FlashFormValues) => { - // TODO: Rework Flash machine API - // Its unusable in its current state - // await callApi("flash_machine", { - // machine: { - // name: "", - // }, - // disks: {values.disk }, - // dry_run: true, - // }); - console.log("submit", values); + setIsFlashing(true); + try { + await callApi("flash_machine", { + machine: { + name: values.machine.devicePath, + flake: { + loc: values.machine.flake, + }, + }, + mode: "format", + disks: { "main": values.disk }, + system_config: { + language: values.language, + keymap: values.keymap, + ssh_keys_path: values.sshKeys, + }, + dry_run: false, + write_efi_boot_entries: false, + debug: false, + }); + } catch (error) { + console.error("Error submitting form:", error); + } finally { + setIsFlashing(false); + } }; return ( @@ -70,7 +140,8 @@ export const Flash = () => { @@ -86,7 +157,7 @@ export const Flash = () => { )} {(field, props) => ( @@ -96,7 +167,7 @@ export const Flash = () => { @@ -120,13 +191,11 @@ export const Flash = () => { class="select select-bordered w-full" {...props} > - {/* devices */} - - + {(device) => ( - )} @@ -147,8 +216,106 @@ export const Flash = () => { )} -
diff --git a/pkgs/webview-ui/app/src/routes/hosts/view.tsx b/pkgs/webview-ui/app/src/routes/hosts/view.tsx index 68c02e010..bb75162fa 100644 --- a/pkgs/webview-ui/app/src/routes/hosts/view.tsx +++ b/pkgs/webview-ui/app/src/routes/hosts/view.tsx @@ -1,9 +1,9 @@ import { - For, - Show, + type Component, createEffect, createSignal, - type Component, + For, + Show, } from "solid-js"; import { route } from "@/src/App"; import { OperationResponse, pyApi } from "@/src/api"; diff --git a/pkgs/webview-ui/app/src/routes/machines/view.tsx b/pkgs/webview-ui/app/src/routes/machines/view.tsx index 441fe755e..87b8b28c1 100644 --- a/pkgs/webview-ui/app/src/routes/machines/view.tsx +++ b/pkgs/webview-ui/app/src/routes/machines/view.tsx @@ -1,14 +1,14 @@ import { + type Component, + createEffect, + createSignal, For, Match, Show, Switch, - createEffect, - createSignal, - type Component, } from "solid-js"; import { activeURI, route, setActiveURI, setRoute } from "@/src/App"; -import { OperationResponse, callApi, pyApi } from "@/src/api"; +import { callApi, OperationResponse, pyApi } from "@/src/api"; import toast from "solid-toast"; import { MachineListItem } from "@/src/components/MachineListItem"; @@ -91,7 +91,8 @@ export const MachineListView: Component = () => { add - {/* + { + /* {(services) => ( {(service) => ( @@ -137,7 +138,8 @@ export const MachineListView: Component = () => { )} )} - */} + */ + } {/* Loading skeleton */} diff --git a/pkgs/webview-ui/app/src/routes/settings/index.tsx b/pkgs/webview-ui/app/src/routes/settings/index.tsx index 8fa7869f7..8aae735fe 100644 --- a/pkgs/webview-ui/app/src/routes/settings/index.tsx +++ b/pkgs/webview-ui/app/src/routes/settings/index.tsx @@ -1,16 +1,16 @@ import { callApi } from "@/src/api"; import { - SubmitHandler, createForm, required, setValue, + SubmitHandler, } from "@modular-forms/solid"; import { activeURI, - setClanList, - setActiveURI, - setRoute, clanList, + setActiveURI, + setClanList, + setRoute, } from "@/src/App"; import { createEffect, @@ -140,7 +140,7 @@ const ClanDetails = (props: ClanDetailsProps) => { s.filter((v, idx) => { if (v == clan_dir) { setActiveURI( - clanList()[idx - 1] || clanList()[idx + 1] || null + clanList()[idx - 1] || clanList()[idx + 1] || null, ); return false; } diff --git a/pkgs/webview-ui/app/tests/types.test.ts b/pkgs/webview-ui/app/tests/types.test.ts index f25455e3e..5e8fbeaa3 100644 --- a/pkgs/webview-ui/app/tests/types.test.ts +++ b/pkgs/webview-ui/app/tests/types.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expectTypeOf } from "vitest"; +import { describe, expectTypeOf, it } from "vitest"; import { OperationNames, pyApi } from "@/src/api"; @@ -44,13 +44,13 @@ describe.concurrent("API types work properly", () => { .parameter(0) .toMatchTypeOf< | { - status: "success"; - data: { - machine_name: string; - machine_icon?: string | null; - machine_description?: string | null; - }; - } + status: "success"; + data: { + machine_name: string; + machine_icon?: string | null; + machine_description?: string | null; + }; + } | { status: "error"; errors: any } >(); }); diff --git a/pkgs/webview-ui/app/tsconfig.json b/pkgs/webview-ui/app/tsconfig.json index adaab944c..80d8db30d 100644 --- a/pkgs/webview-ui/app/tsconfig.json +++ b/pkgs/webview-ui/app/tsconfig.json @@ -14,6 +14,7 @@ "allowJs": true, "isolatedModules": true, "paths": { - "@/*": ["./*"]} - }, + "@/*": ["./*"] + } + } } diff --git a/pkgs/webview-ui/app/util.ts b/pkgs/webview-ui/app/util.ts index eaeeab027..93d9f220a 100644 --- a/pkgs/webview-ui/app/util.ts +++ b/pkgs/webview-ui/app/util.ts @@ -20,8 +20,7 @@ export function isValidHostname(value: string | null | undefined) { const isValid = labels.every(function (label) { const validLabelChars = /^([a-zA-Z0-9-]+)$/g; - const validLabel = - validLabelChars.test(label) && + const validLabel = validLabelChars.test(label) && label.length < 64 && !label.startsWith("-") && !label.endsWith("-"); From fb4ceebccf8dcbf1f6dbf620ed9c5017f780206e Mon Sep 17 00:00:00 2001 From: Qubasa Date: Fri, 2 Aug 2024 18:56:53 +0200 Subject: [PATCH 2/2] formatter.nix: Add prettier --- .gitea/workflows/deploy.yaml | 4 +- checks/secrets/sops/machines/machine/key.json | 2 +- checks/secrets/sops/users/admin/key.json | 2 +- docs/overrides/main.html | 21 ++++-- docs/site/static/extra.css | 14 ++-- formatter.nix | 33 +++++++++ pkgs/clan-app/clan_app/assets/style.css | 47 ++++++------ pkgs/clan-cli/.vscode/launch.json | 48 ++++++------- pkgs/clan-cli/.vscode/settings.json | 42 +++++------ pkgs/clan-cli/tests/data/secrets.yaml | 40 +++++------ .../clan_vm_manager/assets/style.css | 47 ++++++------ pkgs/webview-ui/app/eslint.config.mjs | 2 +- pkgs/webview-ui/app/index.html | 2 +- pkgs/webview-ui/app/src/api.ts | 28 ++++---- .../app/src/components/MachineListItem.tsx | 22 +++--- pkgs/webview-ui/app/src/index.css | 8 +-- pkgs/webview-ui/app/src/layout/layout.tsx | 3 +- .../app/src/routes/blockdevices/view.tsx | 54 +++++++------- pkgs/webview-ui/app/src/routes/flash/view.tsx | 72 ++++++++----------- .../app/src/routes/machines/view.tsx | 6 +- .../app/src/routes/settings/index.tsx | 10 +-- pkgs/webview-ui/app/tests/types.test.ts | 14 ++-- pkgs/webview-ui/app/util.ts | 3 +- 23 files changed, 272 insertions(+), 252 deletions(-) diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml index 839c74cb1..cc50bc0ad 100644 --- a/.gitea/workflows/deploy.yaml +++ b/.gitea/workflows/deploy.yaml @@ -1,6 +1,6 @@ name: deploy on: - push: + push: branches: - main jobs: @@ -10,4 +10,4 @@ jobs: - uses: actions/checkout@v3 - run: nix run .#deploy-docs env: - SSH_HOMEPAGE_KEY: ${{ secrets.SSH_HOMEPAGE_KEY }} \ No newline at end of file + SSH_HOMEPAGE_KEY: ${{ secrets.SSH_HOMEPAGE_KEY }} diff --git a/checks/secrets/sops/machines/machine/key.json b/checks/secrets/sops/machines/machine/key.json index 75648379c..c05c0a321 100755 --- a/checks/secrets/sops/machines/machine/key.json +++ b/checks/secrets/sops/machines/machine/key.json @@ -1,4 +1,4 @@ { "publickey": "age15x8u838dwqflr3t6csf4tlghxm4tx77y379ncqxav7y2n8qp7yzqgrwt00", "type": "age" -} \ No newline at end of file +} diff --git a/checks/secrets/sops/users/admin/key.json b/checks/secrets/sops/users/admin/key.json index 75648379c..c05c0a321 100755 --- a/checks/secrets/sops/users/admin/key.json +++ b/checks/secrets/sops/users/admin/key.json @@ -1,4 +1,4 @@ { "publickey": "age15x8u838dwqflr3t6csf4tlghxm4tx77y379ncqxav7y2n8qp7yzqgrwt00", "type": "age" -} \ No newline at end of file +} diff --git a/docs/overrides/main.html b/docs/overrides/main.html index 184e71158..4cd88fc44 100644 --- a/docs/overrides/main.html +++ b/docs/overrides/main.html @@ -1,12 +1,19 @@ -{% extends "base.html" %} - -{% block extrahead %} - - - +{% extends "base.html" %} {% block extrahead %} + + + -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/docs/site/static/extra.css b/docs/site/static/extra.css index 505b38d32..79325ade9 100644 --- a/docs/site/static/extra.css +++ b/docs/site/static/extra.css @@ -1,13 +1,13 @@ @font-face { - font-family: "Roboto"; - src: url(./Roboto-Regular.ttf) format('truetype'); + font-family: "Roboto"; + src: url(./Roboto-Regular.ttf) format("truetype"); } @font-face { - font-family: "Fira Code"; - src: url(./FiraCode-VF.ttf) format('truetype'); + font-family: "Fira Code"; + src: url(./FiraCode-VF.ttf) format("truetype"); } :root { - --md-text-font: "Roboto"; - --md-code-font: "Fira Code"; -} \ No newline at end of file + --md-text-font: "Roboto"; + --md-code-font: "Fira Code"; +} diff --git a/formatter.nix b/formatter.nix index cd61d4e25..ed8acb71a 100644 --- a/formatter.nix +++ b/formatter.nix @@ -11,6 +11,39 @@ treefmt.programs.nixfmt.enable = true; treefmt.programs.nixfmt.package = pkgs.nixfmt-rfc-style; treefmt.programs.deadnix.enable = true; + treefmt.settings.global.excludes = [ + "*.png" + "*.jpeg" + "*.gitignore" + ".vscode/*" + "*.toml" + "*.clan-flake" + "*.code-workspace" + "*.pub" + "*.typed" + "*.age" + "*.list" + "*.desktop" + ]; + treefmt.programs.prettier = { + enable = true; + includes = [ + "*.cjs" + "*.css" + "*.html" + "*.js" + "*.json5" + "*.jsx" + "*.mdx" + "*.mjs" + "*.scss" + "*.ts" + "*.tsx" + "*.vue" + "*.yaml" + "*.yml" + ]; + }; treefmt.programs.mypy.directories = { diff --git a/pkgs/clan-app/clan_app/assets/style.css b/pkgs/clan-app/clan_app/assets/style.css index c179744dd..772b9ad7c 100644 --- a/pkgs/clan-app/clan_app/assets/style.css +++ b/pkgs/clan-app/clan_app/assets/style.css @@ -1,66 +1,63 @@ /* Insert custom styles here */ navigation-view { - padding: 5px; - /* padding-left: 5px; + padding: 5px; + /* padding-left: 5px; padding-right: 5px; padding-bottom: 5px; */ } avatar { - margin: 2px; + margin: 2px; } .trust { - padding-top: 25px; - padding-bottom: 25px; + padding-top: 25px; + padding-bottom: 25px; } .join-list { - margin-top: 1px; - margin-left: 2px; - margin-right: 2px; - + margin-top: 1px; + margin-left: 2px; + margin-right: 2px; } .progress-bar { - margin-right: 25px; - min-width: 200px; + margin-right: 25px; + min-width: 200px; } .group-list { - background-color: inherit; + background-color: inherit; } .group-list > .activatable:hover { - background-color: unset; + background-color: unset; } .group-list > row { - margin-top: 12px; - border-bottom: unset; + margin-top: 12px; + border-bottom: unset; } - .vm-list { - margin-top: 25px; - margin-bottom: 25px; + margin-top: 25px; + margin-bottom: 25px; } .no-shadow { - box-shadow: none; + box-shadow: none; } .search-entry { - margin-bottom: 12px; + margin-bottom: 12px; } searchbar { - margin-bottom: 25px; + margin-bottom: 25px; } - .log-view { - margin-top: 12px; - font-family: monospace; - padding: 8px; + margin-top: 12px; + font-family: monospace; + padding: 8px; } diff --git a/pkgs/clan-cli/.vscode/launch.json b/pkgs/clan-cli/.vscode/launch.json index 4e2c20a75..163754efe 100644 --- a/pkgs/clan-cli/.vscode/launch.json +++ b/pkgs/clan-cli/.vscode/launch.json @@ -1,26 +1,24 @@ { - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Clan Webui", - "type": "python", - "request": "launch", - "module": "clan_cli.webui", - "justMyCode": false, - "args": [ "--reload", "--no-open", "--log-level", "debug" ], - - }, - { - "name": "Clan Cli VMs", - "type": "python", - "request": "launch", - "module": "clan_cli", - "justMyCode": false, - "args": [ "vms" ], - - } - ] -} \ No newline at end of file + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Clan Webui", + "type": "python", + "request": "launch", + "module": "clan_cli.webui", + "justMyCode": false, + "args": ["--reload", "--no-open", "--log-level", "debug"] + }, + { + "name": "Clan Cli VMs", + "type": "python", + "request": "launch", + "module": "clan_cli", + "justMyCode": false, + "args": ["vms"] + } + ] +} diff --git a/pkgs/clan-cli/.vscode/settings.json b/pkgs/clan-cli/.vscode/settings.json index e5c263238..c40fd15aa 100644 --- a/pkgs/clan-cli/.vscode/settings.json +++ b/pkgs/clan-cli/.vscode/settings.json @@ -1,22 +1,22 @@ { - "python.testing.pytestArgs": [ - // Coverage is not supported by vscode: - // https://github.com/Microsoft/vscode-python/issues/693 - // Note that this will make pytest fail if pytest-cov is not installed, - // if that's the case, then this option needs to be be removed (overrides - // can be set at a workspace level, it's up to you to decide what's the - // best approach). You might also prefer to only set this option - // per-workspace (wherever coverage is used). - "--no-cov", - "tests" - ], - "python.testing.unittestEnabled": false, - "python.testing.pytestEnabled": true, - "search.exclude": { - "**/.direnv": true - }, - "python.linting.mypyPath": "mypy", - "python.linting.mypyEnabled": true, - "python.linting.enabled": true, - "python.defaultInterpreterPath": "python" -} \ No newline at end of file + "python.testing.pytestArgs": [ + // Coverage is not supported by vscode: + // https://github.com/Microsoft/vscode-python/issues/693 + // Note that this will make pytest fail if pytest-cov is not installed, + // if that's the case, then this option needs to be be removed (overrides + // can be set at a workspace level, it's up to you to decide what's the + // best approach). You might also prefer to only set this option + // per-workspace (wherever coverage is used). + "--no-cov", + "tests" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true, + "search.exclude": { + "**/.direnv": true + }, + "python.linting.mypyPath": "mypy", + "python.linting.mypyEnabled": true, + "python.linting.enabled": true, + "python.defaultInterpreterPath": "python" +} diff --git a/pkgs/clan-cli/tests/data/secrets.yaml b/pkgs/clan-cli/tests/data/secrets.yaml index 3bc636597..c59abca52 100644 --- a/pkgs/clan-cli/tests/data/secrets.yaml +++ b/pkgs/clan-cli/tests/data/secrets.yaml @@ -1,23 +1,23 @@ secret-key: ENC[AES256_GCM,data:gjX4OmCUdd3TlA4p,iv:3yZVpyd6FqkITQY0nU2M1iubmzvkR6PfkK2m/s6nQh8=,tag:Abgp9xkiFFylZIyAlap6Ew==,type:str] nested: - secret-key: ENC[AES256_GCM,data:iUMgDhhIjwvd7wL4,iv:jiJIrh12dSu/sXX+z9ITVoEMNDMjwIlFBnyv40oN4LE=,tag:G9VmAa66Km1sc7JEhW5AvA==,type:str] + secret-key: ENC[AES256_GCM,data:iUMgDhhIjwvd7wL4,iv:jiJIrh12dSu/sXX+z9ITVoEMNDMjwIlFBnyv40oN4LE=,tag:G9VmAa66Km1sc7JEhW5AvA==,type:str] sops: - kms: [] - gcp_kms: [] - azure_kv: [] - hc_vault: [] - age: - - recipient: age14tva0txcrl0zes05x7gkx56qd6wd9q3nwecjac74xxzz4l47r44sv3fz62 - enc: | - -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA0eWdRVjlydXlXOVZFQ3lO - bzU1eG9Iam5Ka29Sdlo0cHJ4b1R6bjdNSzBjCkgwRndCbWZQWHlDU0x1cWRmaGVt - N29lbjR6UjN0L2RhaXEzSG9zQmRsZGsKLS0tIEdsdWgxSmZwU3BWUDVxVWRSSC9M - eVZ6bjgwZnR2TTM5MkRYZWNFSFplQWsKmSzv12/dftL9jx2y35UZUGVK6xWdatE8 - BGJiCvMlp0BQNrh2s/+YaEaBa48w8LL79U/XJnEZ+ZUwxmlbSTn6Hg== - -----END AGE ENCRYPTED FILE----- - lastmodified: "2023-08-08T14:27:20Z" - mac: ENC[AES256_GCM,data:iRWWX+L5Q5nKn3fBCLaWoz/mvqGnNnRd93gJmYXDZbRjFoHa9IFJZst5QDIDa1ZRYUe6G0/+lV5SBi+vwRm1pHysJ3c0ZWYjBP+e1jw3jLXxLV5gACsDC8by+6rFUCho0Xgu+Nqu2ehhNenjQQnCvDH5ivWbW70KFT5ynNgR9Tw=,iv:RYnnbLMC/hNfMwWPreMq9uvY0khajwQTZENO/P34ckY=,tag:Xi1PS5vM1c+sRkroHkPn1Q==,type:str] - pgp: [] - unencrypted_suffix: _unencrypted - version: 3.7.3 + kms: [] + gcp_kms: [] + azure_kv: [] + hc_vault: [] + age: + - recipient: age14tva0txcrl0zes05x7gkx56qd6wd9q3nwecjac74xxzz4l47r44sv3fz62 + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA0eWdRVjlydXlXOVZFQ3lO + bzU1eG9Iam5Ka29Sdlo0cHJ4b1R6bjdNSzBjCkgwRndCbWZQWHlDU0x1cWRmaGVt + N29lbjR6UjN0L2RhaXEzSG9zQmRsZGsKLS0tIEdsdWgxSmZwU3BWUDVxVWRSSC9M + eVZ6bjgwZnR2TTM5MkRYZWNFSFplQWsKmSzv12/dftL9jx2y35UZUGVK6xWdatE8 + BGJiCvMlp0BQNrh2s/+YaEaBa48w8LL79U/XJnEZ+ZUwxmlbSTn6Hg== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2023-08-08T14:27:20Z" + mac: ENC[AES256_GCM,data:iRWWX+L5Q5nKn3fBCLaWoz/mvqGnNnRd93gJmYXDZbRjFoHa9IFJZst5QDIDa1ZRYUe6G0/+lV5SBi+vwRm1pHysJ3c0ZWYjBP+e1jw3jLXxLV5gACsDC8by+6rFUCho0Xgu+Nqu2ehhNenjQQnCvDH5ivWbW70KFT5ynNgR9Tw=,iv:RYnnbLMC/hNfMwWPreMq9uvY0khajwQTZENO/P34ckY=,tag:Xi1PS5vM1c+sRkroHkPn1Q==,type:str] + pgp: [] + unencrypted_suffix: _unencrypted + version: 3.7.3 diff --git a/pkgs/clan-vm-manager/clan_vm_manager/assets/style.css b/pkgs/clan-vm-manager/clan_vm_manager/assets/style.css index c179744dd..772b9ad7c 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/assets/style.css +++ b/pkgs/clan-vm-manager/clan_vm_manager/assets/style.css @@ -1,66 +1,63 @@ /* Insert custom styles here */ navigation-view { - padding: 5px; - /* padding-left: 5px; + padding: 5px; + /* padding-left: 5px; padding-right: 5px; padding-bottom: 5px; */ } avatar { - margin: 2px; + margin: 2px; } .trust { - padding-top: 25px; - padding-bottom: 25px; + padding-top: 25px; + padding-bottom: 25px; } .join-list { - margin-top: 1px; - margin-left: 2px; - margin-right: 2px; - + margin-top: 1px; + margin-left: 2px; + margin-right: 2px; } .progress-bar { - margin-right: 25px; - min-width: 200px; + margin-right: 25px; + min-width: 200px; } .group-list { - background-color: inherit; + background-color: inherit; } .group-list > .activatable:hover { - background-color: unset; + background-color: unset; } .group-list > row { - margin-top: 12px; - border-bottom: unset; + margin-top: 12px; + border-bottom: unset; } - .vm-list { - margin-top: 25px; - margin-bottom: 25px; + margin-top: 25px; + margin-bottom: 25px; } .no-shadow { - box-shadow: none; + box-shadow: none; } .search-entry { - margin-bottom: 12px; + margin-bottom: 12px; } searchbar { - margin-bottom: 25px; + margin-bottom: 25px; } - .log-view { - margin-top: 12px; - font-family: monospace; - padding: 8px; + margin-top: 12px; + font-family: monospace; + padding: 8px; } diff --git a/pkgs/webview-ui/app/eslint.config.mjs b/pkgs/webview-ui/app/eslint.config.mjs index 619db917b..858cc8fec 100644 --- a/pkgs/webview-ui/app/eslint.config.mjs +++ b/pkgs/webview-ui/app/eslint.config.mjs @@ -28,5 +28,5 @@ export default tseslint.config( "no-unused-vars": "off", "@typescript-eslint/no-unused-vars": "off", }, - } + }, ); diff --git a/pkgs/webview-ui/app/index.html b/pkgs/webview-ui/app/index.html index 68f32847f..0c649c3fb 100644 --- a/pkgs/webview-ui/app/index.html +++ b/pkgs/webview-ui/app/index.html @@ -1,4 +1,4 @@ - + Solid App diff --git a/pkgs/webview-ui/app/src/api.ts b/pkgs/webview-ui/app/src/api.ts index 820b28b76..1ced2a193 100644 --- a/pkgs/webview-ui/app/src/api.ts +++ b/pkgs/webview-ui/app/src/api.ts @@ -134,19 +134,21 @@ export const callApi = ( }); }; -const deserialize = (fn: (response: T) => void) => (str: string) => { - try { - const r = JSON.parse(str) as T; - console.log("Received: ", r); - fn(r); - } catch (e) { - console.log("Error parsing JSON: ", e); - window.localStorage.setItem("error", str); - console.error(str); - console.error("See localStorage 'error'"); - alert(`Error parsing JSON: ${e}`); - } -}; +const deserialize = + (fn: (response: T) => void) => + (str: string) => { + try { + const r = JSON.parse(str) as T; + console.log("Received: ", r); + fn(r); + } catch (e) { + console.log("Error parsing JSON: ", e); + window.localStorage.setItem("error", str); + console.error(str); + console.error("See localStorage 'error'"); + alert(`Error parsing JSON: ${e}`); + } + }; // Create the API object diff --git a/pkgs/webview-ui/app/src/components/MachineListItem.tsx b/pkgs/webview-ui/app/src/components/MachineListItem.tsx index b53c55ae5..d3a396119 100644 --- a/pkgs/webview-ui/app/src/components/MachineListItem.tsx +++ b/pkgs/webview-ui/app/src/components/MachineListItem.tsx @@ -94,18 +94,21 @@ export const MachineListItem = (props: MachineListItemProps) => {
System: - {hwInfo()[name]?.system - ? {hwInfo()[name]?.system} - : Not set} + {hwInfo()[name]?.system ? ( + {hwInfo()[name]?.system} + ) : ( + Not set + )}
Target Host: - {deploymentInfo()[name] - ? {deploymentInfo()[name]} - : Not set} - { - /* {deploymentInfo()[name]} + ) : ( + Not set + )} + {/*
}> @@ -116,8 +119,7 @@ export const MachineListItem = (props: MachineListItemProps) => { } > {(i) => + i()} - */ - } + */}
{/* Show only the first error at the bottom */} diff --git a/pkgs/webview-ui/app/src/index.css b/pkgs/webview-ui/app/src/index.css index bbde75111..31e7c33b8 100644 --- a/pkgs/webview-ui/app/src/index.css +++ b/pkgs/webview-ui/app/src/index.css @@ -1,10 +1,10 @@ -@import 'material-icons/iconfont/filled.css'; +@import "material-icons/iconfont/filled.css"; /* List of icons: https://marella.me/material-icons/demo/ */ @tailwind base; @tailwind components; @tailwind utilities; html { - overflow-x: hidden; - overflow-y: scroll; -} \ No newline at end of file + overflow-x: hidden; + overflow-y: scroll; +} diff --git a/pkgs/webview-ui/app/src/layout/layout.tsx b/pkgs/webview-ui/app/src/layout/layout.tsx index c1a48497e..616faa57d 100644 --- a/pkgs/webview-ui/app/src/layout/layout.tsx +++ b/pkgs/webview-ui/app/src/layout/layout.tsx @@ -32,8 +32,7 @@ export const Layout: Component = (props) => { for="toplevel-drawer" aria-label="close sidebar" class="drawer-overlay" - > - + > diff --git a/pkgs/webview-ui/app/src/routes/blockdevices/view.tsx b/pkgs/webview-ui/app/src/routes/blockdevices/view.tsx index f22170791..31b035f32 100644 --- a/pkgs/webview-ui/app/src/routes/blockdevices/view.tsx +++ b/pkgs/webview-ui/app/src/routes/blockdevices/view.tsx @@ -27,35 +27,35 @@ export const BlockDevicesView: Component = () => {
- {isFetching - ? - : ( - - {(devices) => ( - - {(device) => ( -
-
-
Name
-
- {" "} - storage{" "} - {device.name} -
-
-
- -
-
Size
-
{device.size}
-
+ {isFetching ? ( + + ) : ( + + {(devices) => ( + + {(device) => ( +
+
+
Name
+
+ {" "} + storage{" "} + {device.name}
+
- )} - - )} - - )} + +
+
Size
+
{device.size}
+
+
+
+ )} +
+ )} +
+ )}
); diff --git a/pkgs/webview-ui/app/src/routes/flash/view.tsx b/pkgs/webview-ui/app/src/routes/flash/view.tsx index ca40137dc..c5ae34d24 100644 --- a/pkgs/webview-ui/app/src/routes/flash/view.tsx +++ b/pkgs/webview-ui/app/src/routes/flash/view.tsx @@ -1,16 +1,14 @@ -import { route } from "@/src/App"; -import { callApi, OperationArgs, OperationResponse, pyApi } from "@/src/api"; +import { callApi, OperationResponse } from "@/src/api"; import { createForm, required, + FieldValues, setValue, - SubmitHandler, } from "@modular-forms/solid"; import { createQuery } from "@tanstack/solid-query"; import { createEffect, createSignal, For } from "solid-js"; -import { effect } from "solid-js/web"; -type FlashFormValues = { +interface FlashFormValues extends FieldValues { machine: { devicePath: string; flake: string; @@ -19,7 +17,7 @@ type FlashFormValues = { language: string; keymap: string; sshKeys: string[]; -}; +} type BlockDevices = Extract< OperationResponse<"show_block_devices">, @@ -59,10 +57,7 @@ export const Flash = () => { } }); - const { - data: devices, - isFetching, - } = createQuery(() => ({ + const { data: devices, isFetching } = createQuery(() => ({ queryKey: ["block_devices"], queryFn: async () => { const result = await callApi("show_block_devices", {}); @@ -72,10 +67,7 @@ export const Flash = () => { staleTime: 1000 * 60 * 2, // 1 minutes })); - const { - data: keymaps, - isFetching: isFetchingKeymaps, - } = createQuery(() => ({ + const { data: keymaps, isFetching: isFetchingKeymaps } = createQuery(() => ({ queryKey: ["list_keymaps"], queryFn: async () => { const result = await callApi("list_possible_keymaps", {}); @@ -85,18 +77,17 @@ export const Flash = () => { staleTime: 1000 * 60 * 15, // 15 minutes })); - const { - data: languages, - isFetching: isFetchingLanguages, - } = createQuery(() => ({ - queryKey: ["list_languages"], - queryFn: async () => { - const result = await callApi("list_possible_languages", {}); - if (result.status === "error") throw new Error("Failed to fetch data"); - return result.data; - }, - staleTime: 1000 * 60 * 15, // 15 minutes - })); + const { data: languages, isFetching: isFetchingLanguages } = createQuery( + () => ({ + queryKey: ["list_languages"], + queryFn: async () => { + const result = await callApi("list_possible_languages", {}); + if (result.status === "error") throw new Error("Failed to fetch data"); + return result.data; + }, + staleTime: 1000 * 60 * 15, // 15 minutes + }), + ); const handleSubmit = async (values: FlashFormValues) => { setIsFlashing(true); @@ -109,7 +100,7 @@ export const Flash = () => { }, }, mode: "format", - disks: { "main": values.disk }, + disks: { main: values.disk }, system_config: { language: values.language, keymap: values.keymap, @@ -191,7 +182,9 @@ export const Flash = () => { class="select select-bordered w-full" {...props} > - + {(device) => ( - {(language) => ( - - )} + {(language) => }
@@ -261,17 +250,12 @@ export const Flash = () => { > - {(keymap) => ( - - )} + {(keymap) => }
{isFetchingKeymaps && ( - + )} @@ -312,9 +296,11 @@ export const Flash = () => { )} diff --git a/pkgs/webview-ui/app/src/routes/machines/view.tsx b/pkgs/webview-ui/app/src/routes/machines/view.tsx index 87b8b28c1..404a6af56 100644 --- a/pkgs/webview-ui/app/src/routes/machines/view.tsx +++ b/pkgs/webview-ui/app/src/routes/machines/view.tsx @@ -91,8 +91,7 @@ export const MachineListView: Component = () => { add
- { - /* + {/* {(services) => ( {(service) => ( @@ -138,8 +137,7 @@ export const MachineListView: Component = () => { )} )} - */ - } + */} {/* Loading skeleton */} diff --git a/pkgs/webview-ui/app/src/routes/settings/index.tsx b/pkgs/webview-ui/app/src/routes/settings/index.tsx index 8aae735fe..81eb22bdf 100644 --- a/pkgs/webview-ui/app/src/routes/settings/index.tsx +++ b/pkgs/webview-ui/app/src/routes/settings/index.tsx @@ -33,17 +33,17 @@ export const registerClan = async () => { }); console.log({ loc }, loc.status); if (loc.status === "success" && loc.data) { - // @ts-expect-error: data is a string + const data = loc.data[0]; setClanList((s) => { - const res = new Set([...s, loc.data]); + const res = new Set([...s, data]); return Array.from(res); }); - setActiveURI(loc.data); + setActiveURI(data); setRoute((r) => { if (r === "welcome") return "machines"; return r; }); - return loc.data; + return data; } } catch (e) { // @@ -145,7 +145,7 @@ const ClanDetails = (props: ClanDetailsProps) => { return false; } return true; - }) + }), ); }} > diff --git a/pkgs/webview-ui/app/tests/types.test.ts b/pkgs/webview-ui/app/tests/types.test.ts index 5e8fbeaa3..f27fd4485 100644 --- a/pkgs/webview-ui/app/tests/types.test.ts +++ b/pkgs/webview-ui/app/tests/types.test.ts @@ -44,13 +44,13 @@ describe.concurrent("API types work properly", () => { .parameter(0) .toMatchTypeOf< | { - status: "success"; - data: { - machine_name: string; - machine_icon?: string | null; - machine_description?: string | null; - }; - } + status: "success"; + data: { + machine_name: string; + machine_icon?: string | null; + machine_description?: string | null; + }; + } | { status: "error"; errors: any } >(); }); diff --git a/pkgs/webview-ui/app/util.ts b/pkgs/webview-ui/app/util.ts index 93d9f220a..eaeeab027 100644 --- a/pkgs/webview-ui/app/util.ts +++ b/pkgs/webview-ui/app/util.ts @@ -20,7 +20,8 @@ export function isValidHostname(value: string | null | undefined) { const isValid = labels.every(function (label) { const validLabelChars = /^([a-zA-Z0-9-]+)$/g; - const validLabel = validLabelChars.test(label) && + const validLabel = + validLabelChars.test(label) && label.length < 64 && !label.startsWith("-") && !label.endsWith("-");