clan-app: Finish flash view. clan-cli: Flash cli now verifies if language and keymap are valid.
This commit is contained in:
@@ -4,6 +4,8 @@ import gi
|
|||||||
gi.require_version("Gtk", "4.0")
|
gi.require_version("Gtk", "4.0")
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from clan_cli.api import ErrorDataClass, SuccessDataClass
|
from clan_cli.api import ErrorDataClass, SuccessDataClass
|
||||||
from clan_cli.api.directory import FileRequest
|
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,
|
# This implements the abstract function open_file with one argument, file_request,
|
||||||
# which is a FileRequest object and returns a string or None.
|
# which is a FileRequest object and returns a string or None.
|
||||||
class open_file(
|
class open_file(
|
||||||
ImplFunc[[FileRequest, str], SuccessDataClass[str | None] | ErrorDataClass]
|
ImplFunc[[FileRequest, str], SuccessDataClass[list[str] | None] | ErrorDataClass]
|
||||||
):
|
):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@@ -27,7 +29,7 @@ class open_file(
|
|||||||
try:
|
try:
|
||||||
gfile = file_dialog.open_finish(task)
|
gfile = file_dialog.open_finish(task)
|
||||||
if gfile:
|
if gfile:
|
||||||
selected_path = gfile.get_path()
|
selected_path = [gfile.get_path()]
|
||||||
self.returns(
|
self.returns(
|
||||||
SuccessDataClass(
|
SuccessDataClass(
|
||||||
op_key=op_key, data=selected_path, status="success"
|
op_key=op_key, data=selected_path, status="success"
|
||||||
@@ -36,11 +38,26 @@ class open_file(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error getting selected file or directory: {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:
|
def on_folder_select(file_dialog: Gtk.FileDialog, task: Gio.Task) -> None:
|
||||||
try:
|
try:
|
||||||
gfile = file_dialog.select_folder_finish(task)
|
gfile = file_dialog.select_folder_finish(task)
|
||||||
if gfile:
|
if gfile:
|
||||||
selected_path = gfile.get_path()
|
selected_path = [gfile.get_path()]
|
||||||
self.returns(
|
self.returns(
|
||||||
SuccessDataClass(
|
SuccessDataClass(
|
||||||
op_key=op_key, data=selected_path, status="success"
|
op_key=op_key, data=selected_path, status="success"
|
||||||
@@ -53,7 +70,7 @@ class open_file(
|
|||||||
try:
|
try:
|
||||||
gfile = file_dialog.save_finish(task)
|
gfile = file_dialog.save_finish(task)
|
||||||
if gfile:
|
if gfile:
|
||||||
selected_path = gfile.get_path()
|
selected_path = [gfile.get_path()]
|
||||||
self.returns(
|
self.returns(
|
||||||
SuccessDataClass(
|
SuccessDataClass(
|
||||||
op_key=op_key, data=selected_path, status="success"
|
op_key=op_key, data=selected_path, status="success"
|
||||||
@@ -90,9 +107,21 @@ class open_file(
|
|||||||
filters.append(file_filters)
|
filters.append(file_filters)
|
||||||
dialog.set_filters(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 select_folder
|
||||||
if file_request.mode == "select_folder":
|
if file_request.mode == "select_folder":
|
||||||
dialog.select_folder(callback=on_folder_select)
|
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":
|
elif file_request.mode == "open_file":
|
||||||
dialog.open(callback=on_file_select)
|
dialog.open(callback=on_file_select)
|
||||||
elif file_request.mode == "save":
|
elif file_request.mode == "save":
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
from clan_cli.api import API
|
from clan_cli.api import API
|
||||||
@@ -47,3 +48,4 @@ class MainWindow(Adw.ApplicationWindow):
|
|||||||
|
|
||||||
def on_destroy(self, source: "Adw.ApplicationWindow") -> None:
|
def on_destroy(self, source: "Adw.ApplicationWindow") -> None:
|
||||||
log.debug("Destroying Adw.ApplicationWindow")
|
log.debug("Destroying Adw.ApplicationWindow")
|
||||||
|
os._exit(0)
|
||||||
|
|||||||
13
pkgs/clan-cli/clan_cli/api/cli.py
Executable file
13
pkgs/clan-cli/clan_cli/api/cli.py
Executable file
@@ -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))
|
||||||
@@ -12,24 +12,26 @@ from . import API
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class FileFilter:
|
class FileFilter:
|
||||||
title: str | None
|
title: str | None = field(default=None)
|
||||||
mime_types: list[str] | None
|
mime_types: list[str] | None = field(default=None)
|
||||||
patterns: list[str] | None
|
patterns: list[str] | None = field(default=None)
|
||||||
suffixes: list[str] | None
|
suffixes: list[str] | None = field(default=None)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class FileRequest:
|
class FileRequest:
|
||||||
# Mode of the os dialog window
|
# 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 of the os dialog window
|
||||||
title: str | None = None
|
title: str | None = field(default=None)
|
||||||
# Pre-applied filters for the file dialog
|
# 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
|
@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.
|
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.
|
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
|
@dataclass
|
||||||
class BlkInfo:
|
class BlkInfo:
|
||||||
name: str
|
name: str
|
||||||
|
path: str
|
||||||
rm: str
|
rm: str
|
||||||
size: str
|
size: str
|
||||||
ro: bool
|
ro: bool
|
||||||
@@ -103,6 +106,7 @@ class Blockdevices:
|
|||||||
def blk_from_dict(data: dict) -> BlkInfo:
|
def blk_from_dict(data: dict) -> BlkInfo:
|
||||||
return BlkInfo(
|
return BlkInfo(
|
||||||
name=data["name"],
|
name=data["name"],
|
||||||
|
path=data["path"],
|
||||||
rm=data["rm"],
|
rm=data["rm"],
|
||||||
size=data["size"],
|
size=data["size"],
|
||||||
ro=data["ro"],
|
ro=data["ro"],
|
||||||
@@ -117,7 +121,10 @@ def show_block_devices() -> Blockdevices:
|
|||||||
Abstract api method to show block devices.
|
Abstract api method to show block devices.
|
||||||
It must return a list of 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)
|
proc = run_no_stdout(cmd)
|
||||||
res = proc.stdout.strip()
|
res = proc.stdout.strip()
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
if field_name not in field_values:
|
||||||
formatted_path = " ".join(path)
|
formatted_path = " ".join(path)
|
||||||
raise ClanError(
|
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
|
return t(**field_values) # type: ignore
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import os
|
|||||||
import shutil
|
import shutil
|
||||||
import textwrap
|
import textwrap
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -19,23 +19,105 @@ from .completions import add_dynamic_completer, complete_machines
|
|||||||
from .errors import ClanError
|
from .errors import ClanError
|
||||||
from .facts.secret_modules import SecretStoreBase
|
from .facts.secret_modules import SecretStoreBase
|
||||||
from .machines.machines import Machine
|
from .machines.machines import Machine
|
||||||
from .nix import nix_shell
|
from .nix import nix_build, nix_shell
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
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
|
@API.register
|
||||||
def flash_machine(
|
def flash_machine(
|
||||||
machine: Machine,
|
machine: Machine,
|
||||||
*,
|
*,
|
||||||
mode: str,
|
mode: str,
|
||||||
disks: dict[str, str],
|
disks: dict[str, str],
|
||||||
system_config: dict[str, Any],
|
system_config: SystemConfig,
|
||||||
dry_run: bool,
|
dry_run: bool,
|
||||||
write_efi_boot_entries: bool,
|
write_efi_boot_entries: bool,
|
||||||
debug: bool,
|
debug: bool,
|
||||||
extra_args: list[str] = [],
|
extra_args: list[str] = [],
|
||||||
) -> None:
|
) -> 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_module = importlib.import_module(machine.secret_facts_module)
|
||||||
secret_facts_store: SecretStoreBase = secret_facts_module.SecretStore(
|
secret_facts_store: SecretStoreBase = secret_facts_module.SecretStore(
|
||||||
machine=machine
|
machine=machine
|
||||||
@@ -76,7 +158,7 @@ def flash_machine(
|
|||||||
disko_install.extend(
|
disko_install.extend(
|
||||||
[
|
[
|
||||||
"--system-config",
|
"--system-config",
|
||||||
json.dumps(system_config),
|
json.dumps(system_config_nix),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
disko_install.extend(["--option", "dry-run", "true"])
|
disko_install.extend(["--option", "dry-run", "true"])
|
||||||
@@ -94,15 +176,13 @@ class FlashOptions:
|
|||||||
flake: FlakeId
|
flake: FlakeId
|
||||||
machine: str
|
machine: str
|
||||||
disks: dict[str, str]
|
disks: dict[str, str]
|
||||||
ssh_keys_path: list[Path]
|
|
||||||
dry_run: bool
|
dry_run: bool
|
||||||
confirm: bool
|
confirm: bool
|
||||||
debug: bool
|
debug: bool
|
||||||
mode: str
|
mode: str
|
||||||
language: str
|
|
||||||
keymap: str
|
|
||||||
write_efi_boot_entries: bool
|
write_efi_boot_entries: bool
|
||||||
nix_options: list[str]
|
nix_options: list[str]
|
||||||
|
system_config: SystemConfig
|
||||||
|
|
||||||
|
|
||||||
class AppendDiskAction(argparse.Action):
|
class AppendDiskAction(argparse.Action):
|
||||||
@@ -126,17 +206,29 @@ def flash_command(args: argparse.Namespace) -> None:
|
|||||||
flake=args.flake,
|
flake=args.flake,
|
||||||
machine=args.machine,
|
machine=args.machine,
|
||||||
disks=args.disk,
|
disks=args.disk,
|
||||||
ssh_keys_path=args.ssh_pubkey,
|
|
||||||
dry_run=args.dry_run,
|
dry_run=args.dry_run,
|
||||||
confirm=not args.yes,
|
confirm=not args.yes,
|
||||||
debug=args.debug,
|
debug=args.debug,
|
||||||
mode=args.mode,
|
mode=args.mode,
|
||||||
|
system_config=SystemConfig(
|
||||||
language=args.language,
|
language=args.language,
|
||||||
keymap=args.keymap,
|
keymap=args.keymap,
|
||||||
|
ssh_keys_path=args.ssh_pubkey,
|
||||||
|
),
|
||||||
write_efi_boot_entries=args.write_efi_boot_entries,
|
write_efi_boot_entries=args.write_efi_boot_entries,
|
||||||
nix_options=args.option,
|
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)
|
machine = Machine(opts.machine, flake=opts.flake)
|
||||||
if opts.confirm and not opts.dry_run:
|
if opts.confirm and not opts.dry_run:
|
||||||
disk_str = ", ".join(f"{name}={device}" for name, device in opts.disks.items())
|
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":
|
if ask != "y":
|
||||||
return
|
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(
|
flash_machine(
|
||||||
machine,
|
machine,
|
||||||
mode=opts.mode,
|
mode=opts.mode,
|
||||||
disks=opts.disks,
|
disks=opts.disks,
|
||||||
system_config=extra_config,
|
system_config=opts.system_config,
|
||||||
dry_run=opts.dry_run,
|
dry_run=opts.dry_run,
|
||||||
debug=opts.debug,
|
debug=opts.debug,
|
||||||
write_efi_boot_entries=opts.write_efi_boot_entries,
|
write_efi_boot_entries=opts.write_efi_boot_entries,
|
||||||
@@ -221,6 +296,18 @@ def register_parser(parser: argparse.ArgumentParser) -> None:
|
|||||||
type=str,
|
type=str,
|
||||||
help="system language",
|
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(
|
parser.add_argument(
|
||||||
"--keymap",
|
"--keymap",
|
||||||
type=str,
|
type=str,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ from clan_cli.inventory import (
|
|||||||
ServiceBorgbackupRoleServer,
|
ServiceBorgbackupRoleServer,
|
||||||
ServiceMeta,
|
ServiceMeta,
|
||||||
)
|
)
|
||||||
|
from clan_cli.machines import machines
|
||||||
|
|
||||||
|
|
||||||
def test_simple() -> None:
|
def test_simple() -> None:
|
||||||
@@ -73,6 +74,55 @@ def test_nested() -> None:
|
|||||||
assert from_dict(Person, person_dict) == expected_person
|
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:
|
def test_simple_field_missing() -> None:
|
||||||
@dataclass
|
@dataclass
|
||||||
class Person:
|
class Person:
|
||||||
|
|||||||
@@ -1,19 +1,14 @@
|
|||||||
|
import dataclasses
|
||||||
import logging
|
import logging
|
||||||
|
import multiprocessing as mp
|
||||||
import os
|
import os
|
||||||
import signal
|
import signal
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
|
from collections.abc import Callable
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
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__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
4
pkgs/webview-ui/.vscode/settings.json
vendored
4
pkgs/webview-ui/.vscode/settings.json
vendored
@@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"typescript.tsdk": "node_modules/typescript/lib",
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
"tailwindCSS.experimental.classRegex": [["cx\\(([^)]*)\\)", "[\"'`]([^\"'`]*)[\"'`]"]],
|
"tailwindCSS.experimental.classRegex": [
|
||||||
|
["cx\\(([^)]*)\\)", "[\"'`]([^\"'`]*)[\"'`]"]
|
||||||
|
],
|
||||||
"editor.wordWrap": "on"
|
"editor.wordWrap": "on"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
## Usage
|
## 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
|
```bash
|
||||||
$ npm install # or pnpm install or yarn install
|
$ 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`
|
### `npm run dev` or `npm start`
|
||||||
|
|
||||||
Runs the app in the development mode.<br>
|
Runs the app in the development mode.<br> Open
|
||||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
[http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||||
|
|
||||||
The page will reload if you make edits.<br>
|
The page will reload if you make edits.<br>
|
||||||
|
|
||||||
### `npm run build`
|
### `npm run build`
|
||||||
|
|
||||||
Builds the app for production to the `dist` folder.<br>
|
Builds the app for production to the `dist` folder.<br> It correctly bundles
|
||||||
It correctly bundles Solid in production mode and optimizes the build for the best performance.
|
Solid in production mode and optimizes the build for the best performance.
|
||||||
|
|
||||||
The build is minified and the filenames include the hashes.<br>
|
The build is minified and the filenames include the hashes.<br> Your app is
|
||||||
Your app is ready to be deployed!
|
ready to be deployed!
|
||||||
|
|
||||||
## Deployment
|
## 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.)
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ fs.readFile(manifestPath, { encoding: "utf8" }, (err, data) => {
|
|||||||
console.log(`Rewriting CSS url(): ${asset.url} to ${res}`);
|
console.log(`Rewriting CSS url(): ${asset.url} to ${res}`);
|
||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
.process(css, {
|
.process(css, {
|
||||||
from: `dist/${cssEntry}`,
|
from: `dist/${cssEntry}`,
|
||||||
|
|||||||
@@ -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 { Layout } from "./layout/layout";
|
||||||
import { Route, Router } from "./Routes";
|
import { Route, Router } from "./Routes";
|
||||||
import { Toaster } from "solid-toast";
|
import { Toaster } from "solid-toast";
|
||||||
@@ -18,7 +18,7 @@ const [activeURI, setActiveURI] = makePersisted(
|
|||||||
{
|
{
|
||||||
name: "activeURI",
|
name: "activeURI",
|
||||||
storage: localStorage,
|
storage: localStorage,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export { activeURI, setActiveURI };
|
export { activeURI, setActiveURI };
|
||||||
|
|||||||
@@ -58,11 +58,11 @@ const registry: ObserverRegistry = operationNames.reduce(
|
|||||||
...acc,
|
...acc,
|
||||||
[opName]: {},
|
[opName]: {},
|
||||||
}),
|
}),
|
||||||
{} as ObserverRegistry
|
{} as ObserverRegistry,
|
||||||
);
|
);
|
||||||
|
|
||||||
function createFunctions<K extends OperationNames>(
|
function createFunctions<K extends OperationNames>(
|
||||||
operationName: K
|
operationName: K,
|
||||||
): {
|
): {
|
||||||
dispatch: (args: OperationArgs<K>) => void;
|
dispatch: (args: OperationArgs<K>) => void;
|
||||||
receive: (fn: (response: OperationResponse<K>) => void, id: string) => void;
|
receive: (fn: (response: OperationResponse<K>) => void, id: string) => void;
|
||||||
@@ -104,7 +104,7 @@ function download(filename: string, text: string) {
|
|||||||
const element = document.createElement("a");
|
const element = document.createElement("a");
|
||||||
element.setAttribute(
|
element.setAttribute(
|
||||||
"href",
|
"href",
|
||||||
"data:text/plain;charset=utf-8," + encodeURIComponent(text)
|
"data:text/plain;charset=utf-8," + encodeURIComponent(text),
|
||||||
);
|
);
|
||||||
element.setAttribute("download", filename);
|
element.setAttribute("download", filename);
|
||||||
|
|
||||||
@@ -118,7 +118,7 @@ function download(filename: string, text: string) {
|
|||||||
|
|
||||||
export const callApi = <K extends OperationNames>(
|
export const callApi = <K extends OperationNames>(
|
||||||
method: K,
|
method: K,
|
||||||
args: OperationArgs<K>
|
args: OperationArgs<K>,
|
||||||
) => {
|
) => {
|
||||||
return new Promise<OperationResponse<K>>((resolve, reject) => {
|
return new Promise<OperationResponse<K>>((resolve, reject) => {
|
||||||
const id = nanoid();
|
const id = nanoid();
|
||||||
@@ -134,9 +134,7 @@ export const callApi = <K extends OperationNames>(
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const deserialize =
|
const deserialize = <T>(fn: (response: T) => void) => (str: string) => {
|
||||||
<T>(fn: (response: T) => void) =>
|
|
||||||
(str: string) => {
|
|
||||||
try {
|
try {
|
||||||
const r = JSON.parse(str) as T;
|
const r = JSON.parse(str) as T;
|
||||||
console.log("Received: ", r);
|
console.log("Received: ", r);
|
||||||
@@ -148,7 +146,7 @@ const deserialize =
|
|||||||
console.error("See localStorage 'error'");
|
console.error("See localStorage 'error'");
|
||||||
alert(`Error parsing JSON: ${e}`);
|
alert(`Error parsing JSON: ${e}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create the API object
|
// Create the API object
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Match, Show, Switch, createSignal } from "solid-js";
|
import { createSignal, Match, Show, Switch } from "solid-js";
|
||||||
import { ErrorData, SuccessData, pyApi } from "../api";
|
import { ErrorData, pyApi, SuccessData } from "../api";
|
||||||
|
|
||||||
type MachineDetails = SuccessData<"list_machines">["data"][string];
|
type MachineDetails = SuccessData<"list_machines">["data"][string];
|
||||||
|
|
||||||
@@ -94,21 +94,18 @@ export const MachineListItem = (props: MachineListItemProps) => {
|
|||||||
<div class="flex flex-row flex-wrap gap-4 py-2">
|
<div class="flex flex-row flex-wrap gap-4 py-2">
|
||||||
<div class="badge badge-primary flex flex-row gap-1 py-4 align-middle">
|
<div class="badge badge-primary flex flex-row gap-1 py-4 align-middle">
|
||||||
<span>System:</span>
|
<span>System:</span>
|
||||||
{hwInfo()[name]?.system ? (
|
{hwInfo()[name]?.system
|
||||||
<span class="text-primary">{hwInfo()[name]?.system}</span>
|
? <span class="text-primary">{hwInfo()[name]?.system}</span>
|
||||||
) : (
|
: <span class="text-warning">Not set</span>}
|
||||||
<span class="text-warning">Not set</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="badge badge-ghost flex flex-row gap-1 py-4 align-middle">
|
<div class="badge badge-ghost flex flex-row gap-1 py-4 align-middle">
|
||||||
<span>Target Host:</span>
|
<span>Target Host:</span>
|
||||||
{deploymentInfo()[name] ? (
|
{deploymentInfo()[name]
|
||||||
<span class="text-primary">{deploymentInfo()[name]}</span>
|
? <span class="text-primary">{deploymentInfo()[name]}</span>
|
||||||
) : (
|
: <span class="text-warning">Not set</span>}
|
||||||
<span class="text-warning">Not set</span>
|
{
|
||||||
)}
|
/* <Show
|
||||||
{/* <Show
|
|
||||||
when={deploymentInfo()[name]}
|
when={deploymentInfo()[name]}
|
||||||
fallback={
|
fallback={
|
||||||
<Switch fallback={<div class="skeleton h-8 w-full"></div>}>
|
<Switch fallback={<div class="skeleton h-8 w-full"></div>}>
|
||||||
@@ -119,7 +116,8 @@ export const MachineListItem = (props: MachineListItemProps) => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{(i) => + i()}
|
{(i) => + i()}
|
||||||
</Show> */}
|
</Show> */
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Show only the first error at the bottom */}
|
{/* Show only the first error at the bottom */}
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ export interface UseFloatingOptions<
|
|||||||
whileElementsMounted?: (
|
whileElementsMounted?: (
|
||||||
reference: R,
|
reference: R,
|
||||||
floating: F,
|
floating: F,
|
||||||
update: () => void
|
update: () => void,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
|
) => // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
|
||||||
) => void | (() => void);
|
void | (() => void);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UseFloatingState extends Omit<ComputePositionReturn, "x" | "y"> {
|
interface UseFloatingState extends Omit<ComputePositionReturn, "x" | "y"> {
|
||||||
@@ -30,7 +30,7 @@ export interface UseFloatingResult extends UseFloatingState {
|
|||||||
export function useFloating<R extends ReferenceElement, F extends HTMLElement>(
|
export function useFloating<R extends ReferenceElement, F extends HTMLElement>(
|
||||||
reference: () => R | undefined | null,
|
reference: () => R | undefined | null,
|
||||||
floating: () => F | undefined | null,
|
floating: () => F | undefined | null,
|
||||||
options?: UseFloatingOptions<R, F>
|
options?: UseFloatingOptions<R, F>,
|
||||||
): UseFloatingResult {
|
): UseFloatingResult {
|
||||||
const placement = () => options?.placement ?? "bottom";
|
const placement = () => options?.placement ?? "bottom";
|
||||||
const strategy = () => options?.strategy ?? "absolute";
|
const strategy = () => options?.strategy ?? "absolute";
|
||||||
@@ -77,7 +77,7 @@ export function useFloating<R extends ReferenceElement, F extends HTMLElement>(
|
|||||||
},
|
},
|
||||||
(err) => {
|
(err) => {
|
||||||
setError(err);
|
setError(err);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,7 +95,7 @@ export function useFloating<R extends ReferenceElement, F extends HTMLElement>(
|
|||||||
const cleanup = options.whileElementsMounted(
|
const cleanup = options.whileElementsMounted(
|
||||||
currentReference,
|
currentReference,
|
||||||
currentFloating,
|
currentFloating,
|
||||||
update
|
update,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (cleanup) {
|
if (cleanup) {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ window.clan = window.clan || {};
|
|||||||
|
|
||||||
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
|
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
|
||||||
throw new Error(
|
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(
|
|||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
),
|
),
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
root!
|
root!,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ export const Layout: Component<LayoutProps> = (props) => {
|
|||||||
for="toplevel-drawer"
|
for="toplevel-drawer"
|
||||||
aria-label="close sidebar"
|
aria-label="close sidebar"
|
||||||
class="drawer-overlay"
|
class="drawer-overlay"
|
||||||
></label>
|
>
|
||||||
|
</label>
|
||||||
<Sidebar route={route} setRoute={setRoute} />
|
<Sidebar route={route} setRoute={setRoute} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ export const BlockDevicesView: Component = () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex max-w-screen-lg flex-col gap-4">
|
<div class="flex max-w-screen-lg flex-col gap-4">
|
||||||
{isFetching ? (
|
{isFetching
|
||||||
<span class="loading loading-bars"></span>
|
? <span class="loading loading-bars"></span>
|
||||||
) : (
|
: (
|
||||||
<Show when={devices}>
|
<Show when={devices}>
|
||||||
{(devices) => (
|
{(devices) => (
|
||||||
<For each={devices().blockdevices}>
|
<For each={devices().blockdevices}>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { OperationResponse, callApi, pyApi } from "@/src/api";
|
import { callApi, OperationResponse, pyApi } from "@/src/api";
|
||||||
import { Show } from "solid-js";
|
import { Show } from "solid-js";
|
||||||
import {
|
import {
|
||||||
SubmitHandler,
|
|
||||||
createForm,
|
createForm,
|
||||||
required,
|
required,
|
||||||
reset,
|
reset,
|
||||||
|
SubmitHandler,
|
||||||
} from "@modular-forms/solid";
|
} from "@modular-forms/solid";
|
||||||
import toast from "solid-toast";
|
import toast from "solid-toast";
|
||||||
import { setActiveURI, setRoute } from "@/src/App";
|
import { setActiveURI, setRoute } from "@/src/App";
|
||||||
@@ -43,7 +43,7 @@ export const ClanForm = () => {
|
|||||||
(async () => {
|
(async () => {
|
||||||
await callApi("create_clan", {
|
await callApi("create_clan", {
|
||||||
options: {
|
options: {
|
||||||
directory: target_dir,
|
directory: target_dir[0],
|
||||||
template_url,
|
template_url,
|
||||||
initial: {
|
initial: {
|
||||||
meta,
|
meta,
|
||||||
@@ -52,14 +52,14 @@ export const ClanForm = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
setActiveURI(target_dir);
|
setActiveURI(target_dir[0]);
|
||||||
setRoute("machines");
|
setRoute("machines");
|
||||||
})(),
|
})(),
|
||||||
{
|
{
|
||||||
loading: "Creating clan...",
|
loading: "Creating clan...",
|
||||||
success: "Clan Successfully Created",
|
success: "Clan Successfully Created",
|
||||||
error: "Failed to create clan",
|
error: "Failed to create clan",
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
reset(formStore);
|
reset(formStore);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { OperationResponse, callApi, pyApi } from "@/src/api";
|
import { callApi, OperationResponse, pyApi } from "@/src/api";
|
||||||
import { Accessor, Show, Switch, Match } from "solid-js";
|
import { Accessor, Match, Show, Switch } from "solid-js";
|
||||||
import {
|
import {
|
||||||
SubmitHandler,
|
|
||||||
createForm,
|
createForm,
|
||||||
required,
|
required,
|
||||||
reset,
|
reset,
|
||||||
|
SubmitHandler,
|
||||||
} from "@modular-forms/solid";
|
} from "@modular-forms/solid";
|
||||||
import toast from "solid-toast";
|
import toast from "solid-toast";
|
||||||
import { createQuery } from "@tanstack/solid-query";
|
import { createQuery } from "@tanstack/solid-query";
|
||||||
@@ -65,7 +65,7 @@ export const FinalEditClanForm = (props: FinalEditClanFormProps) => {
|
|||||||
loading: "Updating clan...",
|
loading: "Updating clan...",
|
||||||
success: "Clan Successfully updated",
|
success: "Clan Successfully updated",
|
||||||
error: "Failed to update clan",
|
error: "Failed to update clan",
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
props.done();
|
props.done();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
import { route } from "@/src/App";
|
import { route } from "@/src/App";
|
||||||
import { OperationArgs, OperationResponse, callApi, pyApi } from "@/src/api";
|
import { callApi, OperationArgs, OperationResponse, pyApi } from "@/src/api";
|
||||||
import { SubmitHandler, createForm, required } from "@modular-forms/solid";
|
import {
|
||||||
|
createForm,
|
||||||
|
required,
|
||||||
|
setValue,
|
||||||
|
SubmitHandler,
|
||||||
|
} from "@modular-forms/solid";
|
||||||
import { createQuery } from "@tanstack/solid-query";
|
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";
|
import { effect } from "solid-js/web";
|
||||||
|
|
||||||
// type FlashMachineArgs = {
|
|
||||||
// machine: Omit<OperationArgs<"flash_machine">["machine"], "cached_deployment">;
|
|
||||||
// } & Omit<Omit<OperationArgs<"flash_machine">, "machine">, "system_config">;
|
|
||||||
|
|
||||||
// type FlashMachineArgs = OperationArgs<"flash_machine">;
|
|
||||||
|
|
||||||
// type k = keyof FlashMachineArgs;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
|
||||||
type FlashFormValues = {
|
type FlashFormValues = {
|
||||||
machine: {
|
machine: {
|
||||||
name: string;
|
devicePath: string;
|
||||||
flake: string;
|
flake: string;
|
||||||
};
|
};
|
||||||
disk: string;
|
disk: string;
|
||||||
|
language: string;
|
||||||
|
keymap: string;
|
||||||
|
sshKeys: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type BlockDevices = Extract<
|
type BlockDevices = Extract<
|
||||||
@@ -28,10 +28,39 @@ type BlockDevices = Extract<
|
|||||||
|
|
||||||
export const Flash = () => {
|
export const Flash = () => {
|
||||||
const [formStore, { Form, Field }] = createForm<FlashFormValues>({});
|
const [formStore, { Form, Field }] = createForm<FlashFormValues>({});
|
||||||
|
const [sshKeys, setSshKeys] = createSignal<string[]>([]);
|
||||||
|
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 {
|
const {
|
||||||
data: devices,
|
data: devices,
|
||||||
refetch: loadDevices,
|
|
||||||
isFetching,
|
isFetching,
|
||||||
} = createQuery(() => ({
|
} = createQuery(() => ({
|
||||||
queryKey: ["block_devices"],
|
queryKey: ["block_devices"],
|
||||||
@@ -40,20 +69,61 @@ export const Flash = () => {
|
|||||||
if (result.status === "error") throw new Error("Failed to fetch data");
|
if (result.status === "error") throw new Error("Failed to fetch data");
|
||||||
return result.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) => {
|
const handleSubmit = async (values: FlashFormValues) => {
|
||||||
// TODO: Rework Flash machine API
|
setIsFlashing(true);
|
||||||
// Its unusable in its current state
|
try {
|
||||||
// await callApi("flash_machine", {
|
await callApi("flash_machine", {
|
||||||
// machine: {
|
machine: {
|
||||||
// name: "",
|
name: values.machine.devicePath,
|
||||||
// },
|
flake: {
|
||||||
// disks: {values.disk },
|
loc: values.machine.flake,
|
||||||
// dry_run: true,
|
},
|
||||||
// });
|
},
|
||||||
console.log("submit", values);
|
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 (
|
return (
|
||||||
@@ -70,7 +140,8 @@ export const Flash = () => {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="grow"
|
class="grow"
|
||||||
placeholder="machine.flake"
|
//placeholder="machine.flake"
|
||||||
|
value="git+https://git.clan.lol/clan/clan-core"
|
||||||
required
|
required
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@@ -86,7 +157,7 @@ export const Flash = () => {
|
|||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
<Field
|
<Field
|
||||||
name="machine.name"
|
name="machine.devicePath"
|
||||||
validate={[required("This field is required")]}
|
validate={[required("This field is required")]}
|
||||||
>
|
>
|
||||||
{(field, props) => (
|
{(field, props) => (
|
||||||
@@ -96,7 +167,7 @@ export const Flash = () => {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="grow"
|
class="grow"
|
||||||
placeholder="machine.name"
|
value="flash-installer"
|
||||||
required
|
required
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@@ -120,13 +191,11 @@ export const Flash = () => {
|
|||||||
class="select select-bordered w-full"
|
class="select select-bordered w-full"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{/* <span class="material-icons">devices</span> */}
|
<option value="" disabled>Select a disk</option>
|
||||||
<option disabled>Select a disk</option>
|
|
||||||
|
|
||||||
<For each={devices?.blockdevices}>
|
<For each={devices?.blockdevices}>
|
||||||
{(device) => (
|
{(device) => (
|
||||||
<option value={device.name}>
|
<option value={device.path}>
|
||||||
{device.name} / {device.size} bytes
|
{device.path} -- {device.size} bytes
|
||||||
</option>
|
</option>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
@@ -147,8 +216,106 @@ export const Flash = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
<button class="btn btn-error" type="submit">
|
<Field name="language" validate={[required("This field is required")]}>
|
||||||
<span class="material-icons">bolt</span>Flash Installer
|
{(field, props) => (
|
||||||
|
<>
|
||||||
|
<label class="form-control input-bordered flex w-full items-center gap-2">
|
||||||
|
<select
|
||||||
|
required
|
||||||
|
class="select select-bordered w-full"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<option>en_US.UTF-8</option>
|
||||||
|
<For each={languages}>
|
||||||
|
{(language) => (
|
||||||
|
<option value={language}>
|
||||||
|
{language}
|
||||||
|
</option>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</select>
|
||||||
|
<div class="label">
|
||||||
|
{isFetchingLanguages && (
|
||||||
|
<span class="label-text-alt">
|
||||||
|
<span class="loading loading-bars">en_US.UTF-8</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{field.error && (
|
||||||
|
<span class="label-text-alt font-bold text-error">
|
||||||
|
{field.error}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
<Field name="keymap" validate={[required("This field is required")]}>
|
||||||
|
{(field, props) => (
|
||||||
|
<>
|
||||||
|
<label class="form-control input-bordered flex w-full items-center gap-2">
|
||||||
|
<select
|
||||||
|
required
|
||||||
|
class="select select-bordered w-full"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<option>en</option>
|
||||||
|
<For each={keymaps}>
|
||||||
|
{(keymap) => (
|
||||||
|
<option value={keymap}>
|
||||||
|
{keymap}
|
||||||
|
</option>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</select>
|
||||||
|
<div class="label">
|
||||||
|
{isFetchingKeymaps && (
|
||||||
|
<span class="label-text
|
||||||
|
-alt">
|
||||||
|
<span class="loading loading-bars"></span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{field.error && (
|
||||||
|
<span class="label-text-alt font-bold text-error">
|
||||||
|
{field.error}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
<Field name="sshKeys" validate={[]} type="string[]">
|
||||||
|
{(field, props) => (
|
||||||
|
<>
|
||||||
|
<label class="input input-bordered flex items-center gap-2">
|
||||||
|
<span class="material-icons">key</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="grow"
|
||||||
|
placeholder="Select SSH Key"
|
||||||
|
value={field.value ? field.value.join(", ") : ""}
|
||||||
|
readOnly
|
||||||
|
onClick={() => selectSshPubkey()}
|
||||||
|
required
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<div class="label">
|
||||||
|
{field.error && (
|
||||||
|
<span class="label-text-alt font-bold text-error">
|
||||||
|
{field.error}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
<button class="btn btn-error" type="submit" disabled={isFlashing()}>
|
||||||
|
{isFlashing()
|
||||||
|
? <span class="loading loading-spinner"></span>
|
||||||
|
: <span class="material-icons">bolt</span>}
|
||||||
|
{isFlashing() ? "Flashing..." : "Flash Installer"}
|
||||||
</button>
|
</button>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
For,
|
type Component,
|
||||||
Show,
|
|
||||||
createEffect,
|
createEffect,
|
||||||
createSignal,
|
createSignal,
|
||||||
type Component,
|
For,
|
||||||
|
Show,
|
||||||
} from "solid-js";
|
} from "solid-js";
|
||||||
import { route } from "@/src/App";
|
import { route } from "@/src/App";
|
||||||
import { OperationResponse, pyApi } from "@/src/api";
|
import { OperationResponse, pyApi } from "@/src/api";
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import {
|
import {
|
||||||
|
type Component,
|
||||||
|
createEffect,
|
||||||
|
createSignal,
|
||||||
For,
|
For,
|
||||||
Match,
|
Match,
|
||||||
Show,
|
Show,
|
||||||
Switch,
|
Switch,
|
||||||
createEffect,
|
|
||||||
createSignal,
|
|
||||||
type Component,
|
|
||||||
} from "solid-js";
|
} from "solid-js";
|
||||||
import { activeURI, route, setActiveURI, setRoute } from "@/src/App";
|
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 toast from "solid-toast";
|
||||||
import { MachineListItem } from "@/src/components/MachineListItem";
|
import { MachineListItem } from "@/src/components/MachineListItem";
|
||||||
|
|
||||||
@@ -91,7 +91,8 @@ export const MachineListView: Component = () => {
|
|||||||
<span class="material-icons ">add</span>
|
<span class="material-icons ">add</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/* <Show when={services()}>
|
{
|
||||||
|
/* <Show when={services()}>
|
||||||
{(services) => (
|
{(services) => (
|
||||||
<For each={Object.values(services())}>
|
<For each={Object.values(services())}>
|
||||||
{(service) => (
|
{(service) => (
|
||||||
@@ -137,7 +138,8 @@ export const MachineListView: Component = () => {
|
|||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
)}
|
)}
|
||||||
</Show> */}
|
</Show> */
|
||||||
|
}
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={loading()}>
|
<Match when={loading()}>
|
||||||
{/* Loading skeleton */}
|
{/* Loading skeleton */}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { callApi } from "@/src/api";
|
import { callApi } from "@/src/api";
|
||||||
import {
|
import {
|
||||||
SubmitHandler,
|
|
||||||
createForm,
|
createForm,
|
||||||
required,
|
required,
|
||||||
setValue,
|
setValue,
|
||||||
|
SubmitHandler,
|
||||||
} from "@modular-forms/solid";
|
} from "@modular-forms/solid";
|
||||||
import {
|
import {
|
||||||
activeURI,
|
activeURI,
|
||||||
setClanList,
|
|
||||||
setActiveURI,
|
|
||||||
setRoute,
|
|
||||||
clanList,
|
clanList,
|
||||||
|
setActiveURI,
|
||||||
|
setClanList,
|
||||||
|
setRoute,
|
||||||
} from "@/src/App";
|
} from "@/src/App";
|
||||||
import {
|
import {
|
||||||
createEffect,
|
createEffect,
|
||||||
@@ -140,7 +140,7 @@ const ClanDetails = (props: ClanDetailsProps) => {
|
|||||||
s.filter((v, idx) => {
|
s.filter((v, idx) => {
|
||||||
if (v == clan_dir) {
|
if (v == clan_dir) {
|
||||||
setActiveURI(
|
setActiveURI(
|
||||||
clanList()[idx - 1] || clanList()[idx + 1] || null
|
clanList()[idx - 1] || clanList()[idx + 1] || null,
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { describe, it, expectTypeOf } from "vitest";
|
import { describe, expectTypeOf, it } from "vitest";
|
||||||
|
|
||||||
import { OperationNames, pyApi } from "@/src/api";
|
import { OperationNames, pyApi } from "@/src/api";
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./*"]}
|
"@/*": ["./*"]
|
||||||
},
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,8 +20,7 @@ export function isValidHostname(value: string | null | undefined) {
|
|||||||
const isValid = labels.every(function (label) {
|
const isValid = labels.every(function (label) {
|
||||||
const validLabelChars = /^([a-zA-Z0-9-]+)$/g;
|
const validLabelChars = /^([a-zA-Z0-9-]+)$/g;
|
||||||
|
|
||||||
const validLabel =
|
const validLabel = validLabelChars.test(label) &&
|
||||||
validLabelChars.test(label) &&
|
|
||||||
label.length < 64 &&
|
label.length < 64 &&
|
||||||
!label.startsWith("-") &&
|
!label.startsWith("-") &&
|
||||||
!label.endsWith("-");
|
!label.endsWith("-");
|
||||||
|
|||||||
Reference in New Issue
Block a user