From c7c47b65270218eb7ea5f3472656e3822513bf77 Mon Sep 17 00:00:00 2001 From: Qubasa Date: Sun, 22 Oct 2023 21:03:06 +0200 Subject: [PATCH] Befor fixing linting problem --- pkgs/clan-cli/.envrc | 1 + pkgs/clan-cli/clan_cli/__init__.py | 8 + pkgs/clan-cli/clan_cli/config/__init__.py | 18 +- pkgs/clan-cli/clan_cli/config/machine.py | 2 +- pkgs/clan-cli/clan_cli/custom_logger.py | 57 +- pkgs/clan-cli/clan_cli/debug.py | 66 ++ pkgs/clan-cli/clan_cli/dirs.py | 16 +- pkgs/clan-cli/clan_cli/flakes/create.py | 26 +- pkgs/clan-cli/clan_cli/flakes/types.py | 3 - pkgs/clan-cli/clan_cli/machines/create.py | 4 +- pkgs/clan-cli/clan_cli/machines/facts.py | 2 +- pkgs/clan-cli/clan_cli/machines/list.py | 2 +- pkgs/clan-cli/clan_cli/secrets/folders.py | 2 +- pkgs/clan-cli/clan_cli/secrets/groups.py | 45 +- pkgs/clan-cli/clan_cli/secrets/machines.py | 38 +- pkgs/clan-cli/clan_cli/secrets/secrets.py | 29 +- pkgs/clan-cli/clan_cli/secrets/sops.py | 2 +- .../clan_cli/secrets/sops_generate.py | 6 +- pkgs/clan-cli/clan_cli/secrets/users.py | 12 +- pkgs/clan-cli/clan_cli/task_manager.py | 22 +- pkgs/clan-cli/clan_cli/types.py | 23 + pkgs/clan-cli/clan_cli/vms/create.py | 192 +++--- pkgs/clan-cli/clan_cli/webui/api_inputs.py | 16 +- .../clan_cli/webui/routers/machines.py | 2 +- pkgs/clan-cli/default.nix | 10 +- pkgs/clan-cli/pyproject.toml | 7 +- pkgs/clan-cli/shell.nix | 57 +- pkgs/clan-cli/tests/fixtures_flakes.py | 99 +-- pkgs/clan-cli/tests/helpers/cli.py | 8 + pkgs/clan-cli/tests/temporary_dir.py | 17 +- pkgs/clan-cli/tests/test_config.py | 13 +- pkgs/clan-cli/tests/test_create_flake.py | 45 +- .../tests/test_flake_with_core/flake.nix | 1 + .../test_flake_with_core_and_pass/flake.nix | 1 + .../flake.nix | 1 + pkgs/clan-cli/tests/test_machines_config.py | 4 +- pkgs/clan-cli/tests/test_secrets_cli.py | 140 ++-- pkgs/clan-cli/tests/test_secrets_generate.py | 4 +- .../tests/test_secrets_password_store.py | 4 +- pkgs/clan-cli/tests/test_vms_api_create.py | 25 +- pkgs/ui/.eslintrc.json | 4 +- pkgs/ui/nix/update-ui-assets.sh | 8 +- pkgs/ui/src/app/favicon.ico | Bin 25931 -> 0 bytes pkgs/ui/src/app/join/page.tsx | 5 - pkgs/ui/src/app/layout.tsx | 27 +- pkgs/ui/src/app/machines/add/page.tsx | 7 - pkgs/ui/src/app/machines/edit/[name]/page.tsx | 10 - pkgs/ui/src/app/machines/layout.tsx | 5 - pkgs/ui/src/app/machines/page.tsx | 12 - pkgs/ui/src/app/page.tsx | 48 +- pkgs/ui/src/app/templates/[id]/page.tsx | 174 ----- pkgs/ui/src/app/templates/page.tsx | 61 -- pkgs/ui/src/components/background.tsx | 4 +- .../createMachineForm/customConfig.tsx | 176 ----- .../components/createMachineForm/index.tsx | 155 ----- .../createMachineForm/interfaces.ts | 23 - .../createMachineForm/saveConfig.tsx | 0 .../createMachineForm/selectModules.tsx | 0 .../createMachineForm/selectTemplate.tsx | 0 .../dashboard/NetworkOverview/index.tsx | 73 --- .../dashboard/appOverview/index.tsx | 91 --- .../dashboard/notifications/index.tsx | 68 -- .../dashboard/quickActions/index.tsx | 64 -- .../components/dashboard/taskQueue/index.tsx | 56 -- .../src/components/flakeBadge/flakeBadge.tsx | 21 - .../ui/src/components/hooks/useAppContext.tsx | 20 +- pkgs/ui/src/components/hooks/useMachines.tsx | 95 --- pkgs/ui/src/components/hooks/useVms.tsx | 52 -- pkgs/ui/src/components/join/configureVM.tsx | 165 ----- pkgs/ui/src/components/join/confirm.tsx | 63 -- pkgs/ui/src/components/join/confirmVM.tsx | 61 -- pkgs/ui/src/components/join/layout.tsx | 20 - pkgs/ui/src/components/sidebar/index.tsx | 60 +- .../components/table/enhancedTableToolbar.tsx | 82 --- pkgs/ui/src/components/table/index.tsx | 1 - pkgs/ui/src/components/table/nodePieChart.tsx | 57 -- pkgs/ui/src/components/table/nodeRow.tsx | 135 ---- pkgs/ui/src/components/table/nodeTable.tsx | 96 --- .../components/table/nodeTableContainer.tsx | 197 ------ pkgs/ui/src/components/table/pieCards.tsx | 73 --- pkgs/ui/src/components/table/searchBar.tsx | 99 --- .../src/components/table/stickySpeedDial.tsx | 85 --- pkgs/ui/src/data/_schema.ts | 88 --- pkgs/ui/src/data/_schema2.ts | 111 ---- pkgs/ui/src/data/dashboardData.tsx | 170 ----- pkgs/ui/src/data/nodeData.tsx | 178 ------ pkgs/ui/src/data/nodeDataStatic.tsx | 602 ------------------ 87 files changed, 703 insertions(+), 3929 deletions(-) create mode 100644 pkgs/clan-cli/clan_cli/debug.py delete mode 100644 pkgs/clan-cli/clan_cli/flakes/types.py create mode 100644 pkgs/clan-cli/clan_cli/types.py delete mode 100644 pkgs/ui/src/app/favicon.ico delete mode 100644 pkgs/ui/src/app/join/page.tsx delete mode 100644 pkgs/ui/src/app/machines/add/page.tsx delete mode 100644 pkgs/ui/src/app/machines/edit/[name]/page.tsx delete mode 100644 pkgs/ui/src/app/machines/layout.tsx delete mode 100644 pkgs/ui/src/app/machines/page.tsx delete mode 100644 pkgs/ui/src/app/templates/[id]/page.tsx delete mode 100644 pkgs/ui/src/app/templates/page.tsx delete mode 100644 pkgs/ui/src/components/createMachineForm/customConfig.tsx delete mode 100644 pkgs/ui/src/components/createMachineForm/index.tsx delete mode 100644 pkgs/ui/src/components/createMachineForm/interfaces.ts delete mode 100644 pkgs/ui/src/components/createMachineForm/saveConfig.tsx delete mode 100644 pkgs/ui/src/components/createMachineForm/selectModules.tsx delete mode 100644 pkgs/ui/src/components/createMachineForm/selectTemplate.tsx delete mode 100644 pkgs/ui/src/components/dashboard/NetworkOverview/index.tsx delete mode 100644 pkgs/ui/src/components/dashboard/appOverview/index.tsx delete mode 100644 pkgs/ui/src/components/dashboard/notifications/index.tsx delete mode 100644 pkgs/ui/src/components/dashboard/quickActions/index.tsx delete mode 100644 pkgs/ui/src/components/dashboard/taskQueue/index.tsx delete mode 100644 pkgs/ui/src/components/flakeBadge/flakeBadge.tsx delete mode 100644 pkgs/ui/src/components/hooks/useMachines.tsx delete mode 100644 pkgs/ui/src/components/hooks/useVms.tsx delete mode 100644 pkgs/ui/src/components/join/configureVM.tsx delete mode 100644 pkgs/ui/src/components/join/confirm.tsx delete mode 100644 pkgs/ui/src/components/join/confirmVM.tsx delete mode 100644 pkgs/ui/src/components/join/layout.tsx delete mode 100644 pkgs/ui/src/components/table/enhancedTableToolbar.tsx delete mode 100644 pkgs/ui/src/components/table/index.tsx delete mode 100644 pkgs/ui/src/components/table/nodePieChart.tsx delete mode 100644 pkgs/ui/src/components/table/nodeRow.tsx delete mode 100644 pkgs/ui/src/components/table/nodeTable.tsx delete mode 100644 pkgs/ui/src/components/table/nodeTableContainer.tsx delete mode 100644 pkgs/ui/src/components/table/pieCards.tsx delete mode 100644 pkgs/ui/src/components/table/searchBar.tsx delete mode 100644 pkgs/ui/src/components/table/stickySpeedDial.tsx delete mode 100644 pkgs/ui/src/data/_schema.ts delete mode 100644 pkgs/ui/src/data/_schema2.ts delete mode 100644 pkgs/ui/src/data/dashboardData.tsx delete mode 100644 pkgs/ui/src/data/nodeData.tsx delete mode 100644 pkgs/ui/src/data/nodeDataStatic.tsx diff --git a/pkgs/clan-cli/.envrc b/pkgs/clan-cli/.envrc index 53d6aa3..0ded761 100644 --- a/pkgs/clan-cli/.envrc +++ b/pkgs/clan-cli/.envrc @@ -2,6 +2,7 @@ source_up + if type nix_direnv_watch_file &>/dev/null; then nix_direnv_watch_file flake-module.nix nix_direnv_watch_file default.nix diff --git a/pkgs/clan-cli/clan_cli/__init__.py b/pkgs/clan-cli/clan_cli/__init__.py index 68a07ae..8e77efc 100644 --- a/pkgs/clan-cli/clan_cli/__init__.py +++ b/pkgs/clan-cli/clan_cli/__init__.py @@ -1,11 +1,15 @@ import argparse +import logging import sys from types import ModuleType from typing import Optional from . import config, flakes, join, machines, secrets, vms, webui +from .custom_logger import register from .ssh import cli as ssh_cli +log = logging.getLogger(__name__) + argcomplete: Optional[ModuleType] = None try: import argcomplete # type: ignore[no-redef] @@ -52,6 +56,10 @@ def create_parser(prog: Optional[str] = None) -> argparse.ArgumentParser: parser_vms = subparsers.add_parser("vms", help="manage virtual machines") vms.register_parser(parser_vms) + # if args.debug: + register(logging.DEBUG) + log.debug("Debug log activated") + if argcomplete: argcomplete.autocomplete(parser) diff --git a/pkgs/clan-cli/clan_cli/config/__init__.py b/pkgs/clan-cli/clan_cli/config/__init__.py index a3f4ad6..b5f349e 100644 --- a/pkgs/clan-cli/clan_cli/config/__init__.py +++ b/pkgs/clan-cli/clan_cli/config/__init__.py @@ -11,9 +11,9 @@ from typing import Any, Optional, Tuple, get_origin from clan_cli.dirs import machine_settings_file, specific_flake_dir from clan_cli.errors import ClanError -from clan_cli.flakes.types import FlakeName from clan_cli.git import commit_file from clan_cli.nix import nix_eval +from clan_cli.types import FlakeName script_dir = Path(__file__).parent @@ -161,7 +161,11 @@ def read_machine_option_value( def get_or_set_option(args: argparse.Namespace) -> None: if args.value == []: - print(read_machine_option_value(args.machine, args.option, args.show_trace)) + print( + read_machine_option_value( + args.flake, args.machine, args.option, args.show_trace + ) + ) else: # load options if args.options_file is None: @@ -308,11 +312,6 @@ def register_parser( # inject callback function to process the input later parser.set_defaults(func=get_or_set_option) - parser.add_argument( - "flake", - type=str, - help="name of the flake to set machine options for", - ) parser.add_argument( "--machine", "-m", @@ -356,6 +355,11 @@ def register_parser( nargs="*", help="option value to set (if omitted, the current value is printed)", ) + parser.add_argument( + "flake", + type=str, + help="name of the flake to set machine options for", + ) def main(argv: Optional[list[str]] = None) -> None: diff --git a/pkgs/clan-cli/clan_cli/config/machine.py b/pkgs/clan-cli/clan_cli/config/machine.py index b6f0e2d..f6b571b 100644 --- a/pkgs/clan-cli/clan_cli/config/machine.py +++ b/pkgs/clan-cli/clan_cli/config/machine.py @@ -14,7 +14,7 @@ from clan_cli.dirs import ( from clan_cli.git import commit_file, find_git_repo_root from clan_cli.nix import nix_eval -from ..flakes.types import FlakeName +from ..types import FlakeName def config_for_machine(flake_name: FlakeName, machine_name: str) -> dict: diff --git a/pkgs/clan-cli/clan_cli/custom_logger.py b/pkgs/clan-cli/clan_cli/custom_logger.py index d8b7f9f..f9f324e 100644 --- a/pkgs/clan-cli/clan_cli/custom_logger.py +++ b/pkgs/clan-cli/clan_cli/custom_logger.py @@ -1,5 +1,7 @@ +import inspect import logging -from typing import Any +from pathlib import Path +from typing import Any, Callable grey = "\x1b[38;20m" yellow = "\x1b[33;20m" @@ -9,11 +11,20 @@ green = "\u001b[32m" blue = "\u001b[34m" -def get_formatter(color: str) -> logging.Formatter: - reset = "\x1b[0m" - return logging.Formatter( - f"{color}%(levelname)s{reset}:(%(filename)s:%(lineno)d): %(message)s" - ) +def get_formatter(color: str) -> Callable[[logging.LogRecord, bool], logging.Formatter]: + def myformatter( + record: logging.LogRecord, with_location: bool + ) -> logging.Formatter: + reset = "\x1b[0m" + filepath = Path(record.pathname).resolve() + if not with_location: + return logging.Formatter(f"{color}%(levelname)s{reset}: %(message)s") + + return logging.Formatter( + f"{color}%(levelname)s{reset}: %(message)s\n {filepath}:%(lineno)d::%(funcName)s\n" + ) + + return myformatter FORMATTER = { @@ -26,12 +37,34 @@ FORMATTER = { class CustomFormatter(logging.Formatter): - def format(self, record: Any) -> str: - return FORMATTER[record.levelno].format(record) + def format(self, record: logging.LogRecord) -> str: + return FORMATTER[record.levelno](record, True).format(record) + + +class ThreadFormatter(logging.Formatter): + def format(self, record: logging.LogRecord) -> str: + return FORMATTER[record.levelno](record, False).format(record) + + +def get_caller() -> str: + frame = inspect.currentframe() + if frame is None: + return "unknown" + caller_frame = frame.f_back + if caller_frame is None: + return "unknown" + caller_frame = caller_frame.f_back + if caller_frame is None: + return "unknown" + frame_info = inspect.getframeinfo(caller_frame) + ret = f"{frame_info.filename}:{frame_info.lineno}::{frame_info.function}" + return ret def register(level: Any) -> None: - ch = logging.StreamHandler() - ch.setLevel(level) - ch.setFormatter(CustomFormatter()) - logging.basicConfig(level=level, handlers=[ch]) + handler = logging.StreamHandler() + handler.setLevel(level) + handler.setFormatter(CustomFormatter()) + logger = logging.getLogger("registerHandler") + logger.addHandler(handler) + # logging.basicConfig(level=level, handlers=[handler]) diff --git a/pkgs/clan-cli/clan_cli/debug.py b/pkgs/clan-cli/clan_cli/debug.py new file mode 100644 index 0000000..72bdfc0 --- /dev/null +++ b/pkgs/clan-cli/clan_cli/debug.py @@ -0,0 +1,66 @@ +from typing import Dict, Optional, Tuple, Callable, Any, Mapping, List +from pathlib import Path +import ipdb +import os +import stat +import subprocess +from .dirs import find_git_repo_root +import multiprocessing as mp +from .types import FlakeName +import logging +import sys +import shlex +import time + +log = logging.getLogger(__name__) + +def command_exec(cmd: List[str], work_dir:Path, env: Dict[str, str]) -> None: + subprocess.run(cmd, check=True, env=env, cwd=work_dir.resolve()) + +def repro_env_break(work_dir: Path, env: Optional[Dict[str, str]] = None, cmd: Optional[List[str]] = None) -> None: + if env is None: + env = os.environ.copy() + else: + env = env.copy() + + # Error checking + if "bash" in env["SHELL"]: + raise Exception("I assumed you use zsh, not bash") + + # Cmd appending + args = ["xterm", "-e", "zsh", "-df"] + if cmd is not None: + mycommand = shlex.join(cmd) + write_command(mycommand, work_dir / "cmd.sh") + print(f"Adding to zsh history the command: {mycommand}", file=sys.stderr) + proc = spawn_process(func=command_exec, cmd=args, work_dir=work_dir, env=env) + + try: + ipdb.set_trace() + finally: + proc.terminate() + +def write_command(command: str, loc:Path) -> None: + with open(loc, "w") as f: + f.write("#!/usr/bin/env bash\n") + f.write(command) + st = os.stat(loc) + os.chmod(loc, st.st_mode | stat.S_IEXEC) + +def spawn_process(func: Callable, **kwargs:Any) -> mp.Process: + mp.set_start_method(method="spawn") + proc = mp.Process(target=func, kwargs=kwargs) + proc.start() + return proc + + +def dump_env(env: Dict[str, str], loc: Path) -> None: + cenv = env.copy() + with open(loc, "w") as f: + f.write("#!/usr/bin/env bash\n") + for k, v in cenv.items(): + if v.count('\n') > 0 or v.count("\"") > 0 or v.count("'") > 0: + continue + f.write(f"export {k}='{v}'\n") + st = os.stat(loc) + os.chmod(loc, st.st_mode | stat.S_IEXEC) diff --git a/pkgs/clan-cli/clan_cli/dirs.py b/pkgs/clan-cli/clan_cli/dirs.py index 17a0bc1..5f11577 100644 --- a/pkgs/clan-cli/clan_cli/dirs.py +++ b/pkgs/clan-cli/clan_cli/dirs.py @@ -1,10 +1,13 @@ +import logging import os import sys from pathlib import Path from typing import Optional from .errors import ClanError -from .flakes.types import FlakeName +from .types import FlakeName + +log = logging.getLogger(__name__) def _get_clan_flake_toplevel() -> Path: @@ -51,28 +54,31 @@ def user_data_dir() -> Path: def clan_data_dir() -> Path: path = user_data_dir() / "clan" if not path.exists(): - path.mkdir() + log.debug(f"Creating path with parents {path}") + path.mkdir(parents=True) return path.resolve() def clan_config_dir() -> Path: path = user_config_dir() / "clan" if not path.exists(): - path.mkdir() + log.debug(f"Creating path with parents {path}") + path.mkdir(parents=True) return path.resolve() def clan_flakes_dir() -> Path: path = clan_data_dir() / "flake" if not path.exists(): - path.mkdir() + log.debug(f"Creating path with parents {path}") + path.mkdir(parents=True) return path.resolve() def specific_flake_dir(flake_name: FlakeName) -> Path: flake_dir = clan_flakes_dir() / flake_name if not flake_dir.exists(): - raise ClanError(f"Flake {flake_name} does not exist") + raise ClanError(f"Flake '{flake_name}' does not exist") return flake_dir diff --git a/pkgs/clan-cli/clan_cli/flakes/create.py b/pkgs/clan-cli/clan_cli/flakes/create.py index e35626d..ebdaff8 100644 --- a/pkgs/clan-cli/clan_cli/flakes/create.py +++ b/pkgs/clan-cli/clan_cli/flakes/create.py @@ -8,16 +8,20 @@ from pydantic.tools import parse_obj_as from ..async_cmd import CmdOut, run, runforcli from ..dirs import clan_flakes_dir +from ..errors import ClanError from ..nix import nix_command, nix_shell DEFAULT_URL: AnyUrl = parse_obj_as( - AnyUrl, "git+https://git.clan.lol/clan/clan-core#new-clan" + AnyUrl, + "git+https://git.clan.lol/clan/clan-core?ref=Qubasa-main#new-clan", # TODO: Change me back to main branch ) async def create_flake(directory: Path, url: AnyUrl) -> Dict[str, CmdOut]: if not directory.exists(): directory.mkdir() + else: + raise ClanError(f"Flake at '{directory}' already exists") response = {} command = nix_command( [ @@ -27,27 +31,27 @@ async def create_flake(directory: Path, url: AnyUrl) -> Dict[str, CmdOut]: url, ] ) - out = await run(command, directory) + out = await run(command, cwd=directory) response["flake init"] = out command = nix_shell(["git"], ["git", "init"]) - out = await run(command, directory) + out = await run(command, cwd=directory) response["git init"] = out command = nix_shell(["git"], ["git", "add", "."]) - out = await run(command, directory) + out = await run(command, cwd=directory) response["git add"] = out command = nix_shell(["git"], ["git", "config", "user.name", "clan-tool"]) - out = await run(command, directory) + out = await run(command, cwd=directory) response["git config"] = out command = nix_shell(["git"], ["git", "config", "user.email", "clan@example.com"]) - out = await run(command, directory) + out = await run(command, cwd=directory) response["git config"] = out command = nix_shell(["git"], ["git", "commit", "-a", "-m", "Initial commit"]) - out = await run(command, directory) + out = await run(command, cwd=directory) response["git commit"] = out return response @@ -55,7 +59,7 @@ async def create_flake(directory: Path, url: AnyUrl) -> Dict[str, CmdOut]: def create_flake_command(args: argparse.Namespace) -> None: flake_dir = clan_flakes_dir() / args.name - runforcli(create_flake, flake_dir, DEFAULT_URL) + runforcli(create_flake, flake_dir, args.url) # takes a (sub)parser and configures it @@ -65,5 +69,11 @@ def register_create_parser(parser: argparse.ArgumentParser) -> None: type=str, help="name for the flake", ) + parser.add_argument( + "--url", + type=str, + help="url for the flake", + default=DEFAULT_URL, + ) # parser.add_argument("name", type=str, help="name of the flake") parser.set_defaults(func=create_flake_command) diff --git a/pkgs/clan-cli/clan_cli/flakes/types.py b/pkgs/clan-cli/clan_cli/flakes/types.py deleted file mode 100644 index 16e38c8..0000000 --- a/pkgs/clan-cli/clan_cli/flakes/types.py +++ /dev/null @@ -1,3 +0,0 @@ -from typing import NewType - -FlakeName = NewType("FlakeName", str) diff --git a/pkgs/clan-cli/clan_cli/machines/create.py b/pkgs/clan-cli/clan_cli/machines/create.py index a4880f0..9a2e39b 100644 --- a/pkgs/clan-cli/clan_cli/machines/create.py +++ b/pkgs/clan-cli/clan_cli/machines/create.py @@ -5,14 +5,16 @@ from typing import Dict from ..async_cmd import CmdOut, run, runforcli from ..dirs import specific_flake_dir, specific_machine_dir from ..errors import ClanError -from ..flakes.types import FlakeName from ..nix import nix_shell +from ..types import FlakeName log = logging.getLogger(__name__) async def create_machine(flake_name: FlakeName, machine_name: str) -> Dict[str, CmdOut]: folder = specific_machine_dir(flake_name, machine_name) + if folder.exists(): + raise ClanError(f"Machine '{machine_name}' already exists") folder.mkdir(parents=True, exist_ok=True) # create empty settings.json file inside the folder diff --git a/pkgs/clan-cli/clan_cli/machines/facts.py b/pkgs/clan-cli/clan_cli/machines/facts.py index 7b665b5..3f148cc 100644 --- a/pkgs/clan-cli/clan_cli/machines/facts.py +++ b/pkgs/clan-cli/clan_cli/machines/facts.py @@ -1,5 +1,5 @@ from ..dirs import specific_machine_dir -from ..flakes.types import FlakeName +from ..types import FlakeName def machine_has_fact(flake_name: FlakeName, machine: str, fact: str) -> bool: diff --git a/pkgs/clan-cli/clan_cli/machines/list.py b/pkgs/clan-cli/clan_cli/machines/list.py index d78adb2..079bcd7 100644 --- a/pkgs/clan-cli/clan_cli/machines/list.py +++ b/pkgs/clan-cli/clan_cli/machines/list.py @@ -3,7 +3,7 @@ import logging import os from ..dirs import machines_dir -from ..flakes.types import FlakeName +from ..types import FlakeName from .types import validate_hostname log = logging.getLogger(__name__) diff --git a/pkgs/clan-cli/clan_cli/secrets/folders.py b/pkgs/clan-cli/clan_cli/secrets/folders.py index 8a551cc..3f23c12 100644 --- a/pkgs/clan-cli/clan_cli/secrets/folders.py +++ b/pkgs/clan-cli/clan_cli/secrets/folders.py @@ -5,7 +5,7 @@ from typing import Callable from ..dirs import specific_flake_dir from ..errors import ClanError -from ..flakes.types import FlakeName +from ..types import FlakeName def get_sops_folder(flake_name: FlakeName) -> Path: diff --git a/pkgs/clan-cli/clan_cli/secrets/groups.py b/pkgs/clan-cli/clan_cli/secrets/groups.py index b944560..a969d41 100644 --- a/pkgs/clan-cli/clan_cli/secrets/groups.py +++ b/pkgs/clan-cli/clan_cli/secrets/groups.py @@ -3,8 +3,8 @@ import os from pathlib import Path from ..errors import ClanError -from ..flakes.types import FlakeName from ..machines.types import machine_name_type, validate_hostname +from ..types import FlakeName from . import secrets from .folders import ( sops_groups_folder, @@ -204,9 +204,17 @@ def register_groups_parser(parser: argparse.ArgumentParser) -> None: help="the command to run", required=True, ) + + # List groups list_parser = subparser.add_parser("list", help="list groups") + list_parser.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) list_parser.set_defaults(func=list_command) + # Add user add_machine_parser = subparser.add_parser( "add-machine", help="add a machine to group" ) @@ -214,8 +222,14 @@ def register_groups_parser(parser: argparse.ArgumentParser) -> None: add_machine_parser.add_argument( "machine", help="the name of the machines to add", type=machine_name_type ) + add_machine_parser.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) add_machine_parser.set_defaults(func=add_machine_command) + # Remove machine remove_machine_parser = subparser.add_parser( "remove-machine", help="remove a machine from group" ) @@ -223,15 +237,27 @@ def register_groups_parser(parser: argparse.ArgumentParser) -> None: remove_machine_parser.add_argument( "machine", help="the name of the machines to remove", type=machine_name_type ) + remove_machine_parser.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) remove_machine_parser.set_defaults(func=remove_machine_command) + # Add user add_user_parser = subparser.add_parser("add-user", help="add a user to group") add_group_argument(add_user_parser) add_user_parser.add_argument( "user", help="the name of the user to add", type=user_name_type ) + add_user_parser.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) add_user_parser.set_defaults(func=add_user_command) + # Remove user remove_user_parser = subparser.add_parser( "remove-user", help="remove a user from group" ) @@ -239,8 +265,14 @@ def register_groups_parser(parser: argparse.ArgumentParser) -> None: remove_user_parser.add_argument( "user", help="the name of the user to remove", type=user_name_type ) + remove_user_parser.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) remove_user_parser.set_defaults(func=remove_user_command) + # Add secret add_secret_parser = subparser.add_parser( "add-secret", help="allow a user to access a secret" ) @@ -250,8 +282,14 @@ def register_groups_parser(parser: argparse.ArgumentParser) -> None: add_secret_parser.add_argument( "secret", help="the name of the secret", type=secret_name_type ) + add_secret_parser.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) add_secret_parser.set_defaults(func=add_secret_command) + # Remove secret remove_secret_parser = subparser.add_parser( "remove-secret", help="remove a group's access to a secret" ) @@ -261,4 +299,9 @@ def register_groups_parser(parser: argparse.ArgumentParser) -> None: remove_secret_parser.add_argument( "secret", help="the name of the secret", type=secret_name_type ) + remove_secret_parser.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) remove_secret_parser.set_defaults(func=remove_secret_command) diff --git a/pkgs/clan-cli/clan_cli/secrets/machines.py b/pkgs/clan-cli/clan_cli/secrets/machines.py index bb01212..316f101 100644 --- a/pkgs/clan-cli/clan_cli/secrets/machines.py +++ b/pkgs/clan-cli/clan_cli/secrets/machines.py @@ -1,7 +1,7 @@ import argparse -from ..flakes.types import FlakeName from ..machines.types import machine_name_type, validate_hostname +from ..types import FlakeName from . import secrets from .folders import list_objects, remove_object, sops_machines_folder from .sops import read_key, write_key @@ -96,11 +96,6 @@ def register_machines_parser(parser: argparse.ArgumentParser) -> None: action="store_true", default=False, ) - add_parser.add_argument( - "flake", - type=str, - help="name of the flake to create machine for", - ) add_parser.add_argument( "machine", help="the name of the machine", type=machine_name_type ) @@ -109,6 +104,11 @@ def register_machines_parser(parser: argparse.ArgumentParser) -> None: help="public key or private key of the user", type=public_or_private_age_key_type, ) + add_parser.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) add_parser.set_defaults(func=add_command) # Parser @@ -125,46 +125,46 @@ def register_machines_parser(parser: argparse.ArgumentParser) -> None: # Parser remove_parser = subparser.add_parser("remove", help="remove a machine") + remove_parser.add_argument( + "machine", help="the name of the machine", type=machine_name_type + ) remove_parser.add_argument( "flake", type=str, help="name of the flake to create machine for", ) - remove_parser.add_argument( - "machine", help="the name of the machine", type=machine_name_type - ) remove_parser.set_defaults(func=remove_command) # Parser add_secret_parser = subparser.add_parser( "add-secret", help="allow a machine to access a secret" ) - add_secret_parser.add_argument( - "flake", - type=str, - help="name of the flake to create machine for", - ) add_secret_parser.add_argument( "machine", help="the name of the machine", type=machine_name_type ) add_secret_parser.add_argument( "secret", help="the name of the secret", type=secret_name_type ) + add_secret_parser.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) add_secret_parser.set_defaults(func=add_secret_command) # Parser remove_secret_parser = subparser.add_parser( "remove-secret", help="remove a group's access to a secret" ) - remove_secret_parser.add_argument( - "flake", - type=str, - help="name of the flake to create machine for", - ) remove_secret_parser.add_argument( "machine", help="the name of the group", type=machine_name_type ) remove_secret_parser.add_argument( "secret", help="the name of the secret", type=secret_name_type ) + remove_secret_parser.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) remove_secret_parser.set_defaults(func=remove_secret_command) diff --git a/pkgs/clan-cli/clan_cli/secrets/secrets.py b/pkgs/clan-cli/clan_cli/secrets/secrets.py index d7a2954..64a1abf 100644 --- a/pkgs/clan-cli/clan_cli/secrets/secrets.py +++ b/pkgs/clan-cli/clan_cli/secrets/secrets.py @@ -8,7 +8,7 @@ from typing import IO from .. import tty from ..errors import ClanError -from ..flakes.types import FlakeName +from ..types import FlakeName from .folders import ( list_objects, sops_groups_folder, @@ -253,24 +253,24 @@ def rename_command(args: argparse.Namespace) -> None: def register_secrets_parser(subparser: argparse._SubParsersAction) -> None: parser_list = subparser.add_parser("list", help="list secrets") + parser_list.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) parser_list.set_defaults(func=list_command) parser_get = subparser.add_parser("get", help="get a secret") add_secret_argument(parser_get) - parser_get.set_defaults(func=get_command) parser_get.add_argument( "flake", type=str, help="name of the flake to create machine for", ) + parser_get.set_defaults(func=get_command) parser_set = subparser.add_parser("set", help="set a secret") add_secret_argument(parser_set) - parser_set.add_argument( - "flake", - type=str, - help="name of the flake to create machine for", - ) parser_set.add_argument( "--group", type=str, @@ -299,13 +299,28 @@ def register_secrets_parser(subparser: argparse._SubParsersAction) -> None: default=False, help="edit the secret with $EDITOR instead of pasting it", ) + parser_set.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) parser_set.set_defaults(func=set_command) parser_rename = subparser.add_parser("rename", help="rename a secret") add_secret_argument(parser_rename) parser_rename.add_argument("new_name", type=str, help="the new name of the secret") + parser_rename.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) parser_rename.set_defaults(func=rename_command) parser_remove = subparser.add_parser("remove", help="remove a secret") add_secret_argument(parser_remove) + parser_remove.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) parser_remove.set_defaults(func=remove_command) diff --git a/pkgs/clan-cli/clan_cli/secrets/sops.py b/pkgs/clan-cli/clan_cli/secrets/sops.py index c6f2b2b..f89edd3 100644 --- a/pkgs/clan-cli/clan_cli/secrets/sops.py +++ b/pkgs/clan-cli/clan_cli/secrets/sops.py @@ -9,8 +9,8 @@ from typing import IO, Iterator from ..dirs import user_config_dir from ..errors import ClanError -from ..flakes.types import FlakeName from ..nix import nix_shell +from ..types import FlakeName from .folders import sops_machines_folder, sops_users_folder diff --git a/pkgs/clan-cli/clan_cli/secrets/sops_generate.py b/pkgs/clan-cli/clan_cli/secrets/sops_generate.py index fdac72d..a87328f 100644 --- a/pkgs/clan-cli/clan_cli/secrets/sops_generate.py +++ b/pkgs/clan-cli/clan_cli/secrets/sops_generate.py @@ -6,17 +6,19 @@ import sys from pathlib import Path from tempfile import TemporaryDirectory from typing import Any +import logging from clan_cli.nix import nix_shell from ..dirs import specific_flake_dir from ..errors import ClanError -from ..flakes.types import FlakeName +from ..types import FlakeName from .folders import sops_secrets_folder from .machines import add_machine, has_machine from .secrets import decrypt_secret, encrypt_secret, has_secret from .sops import generate_private_key +log = logging.getLogger(__name__) def generate_host_key(flake_name: FlakeName, machine_name: str) -> None: if has_machine(flake_name, machine_name): @@ -95,6 +97,7 @@ def generate_secrets_from_nix( ) -> None: generate_host_key(flake_name, machine_name) errors = {} + log.debug("Generating secrets for machine %s and flake %s", machine_name, flake_name) with TemporaryDirectory() as d: # if any of the secrets are missing, we regenerate all connected facts/secrets for secret_group, secret_options in secret_submodules.items(): @@ -116,6 +119,7 @@ def upload_age_key_from_nix( flake_name: FlakeName, machine_name: str, ) -> None: + log.debug("Uploading secrets for machine %s and flake %s", machine_name, flake_name) secret_name = f"{machine_name}-age.key" if not has_secret( flake_name, secret_name diff --git a/pkgs/clan-cli/clan_cli/secrets/users.py b/pkgs/clan-cli/clan_cli/secrets/users.py index c5b51e9..5dcd1ce 100644 --- a/pkgs/clan-cli/clan_cli/secrets/users.py +++ b/pkgs/clan-cli/clan_cli/secrets/users.py @@ -1,6 +1,6 @@ import argparse -from ..flakes.types import FlakeName +from ..types import FlakeName from . import secrets from .folders import list_objects, remove_object, sops_users_folder from .sops import read_key, write_key @@ -131,6 +131,11 @@ def register_users_parser(parser: argparse.ArgumentParser) -> None: add_secret_parser.add_argument( "secret", help="the name of the secret", type=secret_name_type ) + add_secret_parser.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) add_secret_parser.set_defaults(func=add_secret_command) remove_secret_parser = subparser.add_parser( @@ -142,4 +147,9 @@ def register_users_parser(parser: argparse.ArgumentParser) -> None: remove_secret_parser.add_argument( "secret", help="the name of the secret", type=secret_name_type ) + remove_secret_parser.add_argument( + "flake", + type=str, + help="name of the flake to create machine for", + ) remove_secret_parser.set_defaults(func=remove_secret_command) diff --git a/pkgs/clan-cli/clan_cli/task_manager.py b/pkgs/clan-cli/clan_cli/task_manager.py index 67fdabc..3e659cb 100644 --- a/pkgs/clan-cli/clan_cli/task_manager.py +++ b/pkgs/clan-cli/clan_cli/task_manager.py @@ -12,6 +12,7 @@ from pathlib import Path from typing import Any, Iterator, Optional, Type, TypeVar from uuid import UUID, uuid4 +from .custom_logger import ThreadFormatter, get_caller from .errors import ClanError @@ -38,7 +39,8 @@ class Command: cwd: Optional[Path] = None, ) -> None: self.running = True - self.log.debug(f"Running command: {shlex.join(cmd)}") + self.log.debug(f"Command: {shlex.join(cmd)}") + self.log.debug(f"Caller: {get_caller()}") cwd_res = None if cwd is not None: @@ -68,10 +70,10 @@ class Command: try: for line in fd: if fd == self.p.stderr: - print(f"[{cmd[0]}] stderr: {line}") + self.log.debug(f"[{cmd[0]}] stderr: {line}") self.stderr.append(line) else: - print(f"[{cmd[0]}] stdout: {line}") + self.log.debug(f"[{cmd[0]}] stdout: {line}") self.stdout.append(line) self._output.put(line) except BlockingIOError: @@ -80,8 +82,6 @@ class Command: if self.p.returncode != 0: raise ClanError(f"Failed to run command: {shlex.join(cmd)}") - self.log.debug("Successfully ran command") - class TaskStatus(str, Enum): NOTSTARTED = "NOTSTARTED" @@ -94,7 +94,13 @@ class BaseTask: def __init__(self, uuid: UUID, num_cmds: int) -> None: # constructor self.uuid: UUID = uuid - self.log = logging.getLogger(__name__) + handler = logging.StreamHandler() + handler.setLevel(logging.DEBUG) + handler.setFormatter(ThreadFormatter()) + logger = logging.getLogger(__name__) + logger.addHandler(handler) + self.log = logger + self.log = logger self.procs: list[Command] = [] self.status = TaskStatus.NOTSTARTED self.logs_lock = threading.Lock() @@ -108,6 +114,10 @@ class BaseTask: self.status = TaskStatus.RUNNING try: self.run() + # TODO: We need to check, if too many commands have been initialized, + # but not run. This would deadlock the log_lines() function. + # Idea: Run next(cmds) and check if it raises StopIteration if not, + # we have too many commands except Exception as e: # FIXME: fix exception handling here traceback.print_exception(*sys.exc_info()) diff --git a/pkgs/clan-cli/clan_cli/types.py b/pkgs/clan-cli/clan_cli/types.py new file mode 100644 index 0000000..a56c051 --- /dev/null +++ b/pkgs/clan-cli/clan_cli/types.py @@ -0,0 +1,23 @@ +import logging +from pathlib import Path +from typing import NewType + +log = logging.getLogger(__name__) + +FlakeName = NewType("FlakeName", str) + + +def validate_path(base_dir: Path, value: Path) -> Path: + user_path = (base_dir / value).resolve() + + # Check if the path is within the data directory + if not str(user_path).startswith(str(base_dir)): + if not str(user_path).startswith("/tmp/pytest"): + raise ValueError( + f"Destination out of bounds. Expected {user_path} to start with {base_dir}" + ) + else: + log.warning( + f"Detected pytest tmpdir. Skipping path validation for {user_path}" + ) + return user_path diff --git a/pkgs/clan-cli/clan_cli/vms/create.py b/pkgs/clan-cli/clan_cli/vms/create.py index 2df7b9a..8c9930d 100644 --- a/pkgs/clan-cli/clan_cli/vms/create.py +++ b/pkgs/clan-cli/clan_cli/vms/create.py @@ -4,20 +4,22 @@ import json import os import shlex import sys -import tempfile from pathlib import Path -from typing import Iterator +from typing import Iterator, Dict from uuid import UUID -from ..dirs import specific_flake_dir -from ..nix import nix_build, nix_config, nix_shell +from ..dirs import clan_flakes_dir, specific_flake_dir +from ..nix import nix_build, nix_config, nix_eval, nix_shell from ..task_manager import BaseTask, Command, create_task +from ..types import validate_path from .inspect import VmConfig, inspect_vm +from ..errors import ClanError +from ..debug import repro_env_break class BuildVmTask(BaseTask): def __init__(self, uuid: UUID, vm: VmConfig) -> None: - super().__init__(uuid, num_cmds=6) + super().__init__(uuid, num_cmds=7) self.vm = vm def get_vm_create_info(self, cmds: Iterator[Command]) -> dict: @@ -34,11 +36,18 @@ class BuildVmTask(BaseTask): ] ) ) - vm_json = "".join(cmd.stdout) + vm_json = "".join(cmd.stdout).strip() self.log.debug(f"VM JSON path: {vm_json}") - with open(vm_json.strip()) as f: + with open(vm_json) as f: return json.load(f) + def get_clan_name(self, cmds: Iterator[Command]) -> str: + clan_dir = self.vm.flake_url + cmd = next(cmds) + cmd.run(nix_eval([f"{clan_dir}#clanInternals.clanName"])) + clan_name = cmd.stdout[0].strip().strip('"') + return clan_name + def run(self) -> None: cmds = self.commands() @@ -47,99 +56,106 @@ class BuildVmTask(BaseTask): # TODO: We should get this from the vm argument vm_config = self.get_vm_create_info(cmds) + clan_name = self.get_clan_name(cmds) - with tempfile.TemporaryDirectory() as tmpdir_: - tmpdir = Path(tmpdir_) - xchg_dir = tmpdir / "xchg" - xchg_dir.mkdir() - secrets_dir = tmpdir / "secrets" - secrets_dir.mkdir() - disk_img = f"{tmpdir_}/disk.img" + self.log.debug(f"Building VM for clan name: {clan_name}") - env = os.environ.copy() - env["CLAN_DIR"] = str(self.vm.flake_url) - env["PYTHONPATH"] = str( - ":".join(sys.path) - ) # TODO do this in the clanCore module - env["SECRETS_DIR"] = str(secrets_dir) + flake_dir = clan_flakes_dir() / clan_name + validate_path(clan_flakes_dir(), flake_dir) + flake_dir.mkdir(exist_ok=True) - cmd = next(cmds) - if Path(self.vm.flake_url).is_dir(): - cmd.run( - [vm_config["generateSecrets"]], - env=env, - ) - else: - cmd.run(["echo", "won't generate secrets for non local clan"]) + xchg_dir = flake_dir / "xchg" + xchg_dir.mkdir() + secrets_dir = flake_dir / "secrets" + secrets_dir.mkdir() + disk_img = f"{flake_dir}/disk.img" - cmd = next(cmds) + env = os.environ.copy() + env["CLAN_DIR"] = str(self.vm.flake_url) + + env["PYTHONPATH"] = str( + ":".join(sys.path) + ) # TODO do this in the clanCore module + env["SECRETS_DIR"] = str(secrets_dir) + + cmd = next(cmds) + repro_env_break(work_dir=flake_dir, env=env, cmd=[vm_config["generateSecrets"], clan_name]) + if Path(self.vm.flake_url).is_dir(): cmd.run( - [vm_config["uploadSecrets"]], + [vm_config["generateSecrets"], clan_name], env=env, ) + else: + self.log.warning("won't generate secrets for non local clan") - cmd = next(cmds) - cmd.run( - nix_shell( - ["qemu"], - [ - "qemu-img", - "create", - "-f", - "raw", - disk_img, - "1024M", - ], - ) + cmd = next(cmds) + cmd.run( + [vm_config["uploadSecrets"]], + env=env, + ) + + cmd = next(cmds) + cmd.run( + nix_shell( + ["qemu"], + [ + "qemu-img", + "create", + "-f", + "raw", + disk_img, + "1024M", + ], ) + ) - cmd = next(cmds) - cmd.run( - nix_shell( - ["e2fsprogs"], - [ - "mkfs.ext4", - "-L", - "nixos", - disk_img, - ], - ) + cmd = next(cmds) + cmd.run( + nix_shell( + ["e2fsprogs"], + [ + "mkfs.ext4", + "-L", + "nixos", + disk_img, + ], ) + ) - cmd = next(cmds) - cmdline = [ - (Path(vm_config["toplevel"]) / "kernel-params").read_text(), - f'init={vm_config["toplevel"]}/init', - f'regInfo={vm_config["regInfo"]}/registration', - "console=ttyS0,115200n8", - "console=tty0", - ] - qemu_command = [ - # fmt: off - "qemu-kvm", - "-name", machine, - "-m", f'{vm_config["memorySize"]}M', - "-smp", str(vm_config["cores"]), - "-device", "virtio-rng-pci", - "-net", "nic,netdev=user.0,model=virtio", "-netdev", "user,id=user.0", - "-virtfs", "local,path=/nix/store,security_model=none,mount_tag=nix-store", - "-virtfs", f"local,path={xchg_dir},security_model=none,mount_tag=shared", - "-virtfs", f"local,path={xchg_dir},security_model=none,mount_tag=xchg", - "-virtfs", f"local,path={secrets_dir},security_model=none,mount_tag=secrets", - "-drive", f'cache=writeback,file={disk_img},format=raw,id=drive1,if=none,index=1,werror=report', - "-device", "virtio-blk-pci,bootindex=1,drive=drive1,serial=root", - "-device", "virtio-keyboard", - "-usb", - "-device", "usb-tablet,bus=usb-bus.0", - "-kernel", f'{vm_config["toplevel"]}/kernel', - "-initrd", vm_config["initrd"], - "-append", " ".join(cmdline), - # fmt: on - ] - if not self.vm.graphics: - qemu_command.append("-nographic") - print("$ " + shlex.join(qemu_command)) - cmd.run(nix_shell(["qemu"], qemu_command)) + cmd = next(cmds) + cmdline = [ + (Path(vm_config["toplevel"]) / "kernel-params").read_text(), + f'init={vm_config["toplevel"]}/init', + f'regInfo={vm_config["regInfo"]}/registration', + "console=ttyS0,115200n8", + "console=tty0", + ] + qemu_command = [ + # fmt: off + "qemu-kvm", + "-name", machine, + "-m", f'{vm_config["memorySize"]}M', + "-smp", str(vm_config["cores"]), + "-device", "virtio-rng-pci", + "-net", "nic,netdev=user.0,model=virtio", "-netdev", "user,id=user.0", + "-virtfs", "local,path=/nix/store,security_model=none,mount_tag=nix-store", + "-virtfs", f"local,path={xchg_dir},security_model=none,mount_tag=shared", + "-virtfs", f"local,path={xchg_dir},security_model=none,mount_tag=xchg", + "-virtfs", f"local,path={secrets_dir},security_model=none,mount_tag=secrets", + "-drive", f'cache=writeback,file={disk_img},format=raw,id=drive1,if=none,index=1,werror=report', + "-device", "virtio-blk-pci,bootindex=1,drive=drive1,serial=root", + "-device", "virtio-keyboard", + "-usb", + "-device", "usb-tablet,bus=usb-bus.0", + "-kernel", f'{vm_config["toplevel"]}/kernel', + "-initrd", vm_config["initrd"], + "-append", " ".join(cmdline), + # fmt: on + ] + if not self.vm.graphics: + qemu_command.append("-nographic") + print("$ " + shlex.join(qemu_command)) + cmd.run(nix_shell(["qemu"], qemu_command)) def create_vm(vm: VmConfig) -> BuildVmTask: diff --git a/pkgs/clan-cli/clan_cli/webui/api_inputs.py b/pkgs/clan-cli/clan_cli/webui/api_inputs.py index d3a9545..94a27b8 100644 --- a/pkgs/clan-cli/clan_cli/webui/api_inputs.py +++ b/pkgs/clan-cli/clan_cli/webui/api_inputs.py @@ -6,25 +6,11 @@ from pydantic import AnyUrl, BaseModel, validator from ..dirs import clan_data_dir, clan_flakes_dir from ..flakes.create import DEFAULT_URL +from ..types import validate_path log = logging.getLogger(__name__) -def validate_path(base_dir: Path, value: Path) -> Path: - user_path = (base_dir / value).resolve() - # Check if the path is within the data directory - if not str(user_path).startswith(str(base_dir)): - if not str(user_path).startswith("/tmp/pytest"): - raise ValueError( - f"Destination out of bounds. Expected {user_path} to start with {base_dir}" - ) - else: - log.warning( - f"Detected pytest tmpdir. Skipping path validation for {user_path}" - ) - return user_path - - class ClanDataPath(BaseModel): dest: Path diff --git a/pkgs/clan-cli/clan_cli/webui/routers/machines.py b/pkgs/clan-cli/clan_cli/webui/routers/machines.py index 42a9c43..e673426 100644 --- a/pkgs/clan-cli/clan_cli/webui/routers/machines.py +++ b/pkgs/clan-cli/clan_cli/webui/routers/machines.py @@ -9,9 +9,9 @@ from ...config.machine import ( schema_for_machine, set_config_for_machine, ) -from ...flakes.types import FlakeName from ...machines.create import create_machine as _create_machine from ...machines.list import list_machines as _list_machines +from ...types import FlakeName from ..api_outputs import ( ConfigResponse, Machine, diff --git a/pkgs/clan-cli/default.nix b/pkgs/clan-cli/default.nix index 0e40404..4a087e8 100644 --- a/pkgs/clan-cli/default.nix +++ b/pkgs/clan-cli/default.nix @@ -8,9 +8,11 @@ , openssh , pytest , pytest-cov +, pytest-xdist , pytest-subprocess -, pytest-parallel , pytest-timeout +, remote-pdb +, ipdb , python3 , runCommand , setuptools @@ -45,8 +47,10 @@ let pytest pytest-cov pytest-subprocess - pytest-parallel + pytest-xdist pytest-timeout + remote-pdb + ipdb openssh git gnupg @@ -80,9 +84,7 @@ let source = runCommand "clan-cli-source" { } '' cp -r ${./.} $out chmod -R +w $out - rm $out/clan_cli/config/jsonschema ln -s ${nixpkgs'} $out/clan_cli/nixpkgs - cp -r ${../../lib/jsonschema} $out/clan_cli/config/jsonschema ln -s ${ui-assets} $out/clan_cli/webui/assets ''; nixpkgs' = runCommand "nixpkgs" { nativeBuildInputs = [ nix ]; } '' diff --git a/pkgs/clan-cli/pyproject.toml b/pkgs/clan-cli/pyproject.toml index a79978c..92d58ce 100644 --- a/pkgs/clan-cli/pyproject.toml +++ b/pkgs/clan-cli/pyproject.toml @@ -14,9 +14,14 @@ exclude = ["clan_cli.nixpkgs*"] [tool.setuptools.package-data] clan_cli = [ "config/jsonschema/*", "webui/assets/**/*"] + + [tool.pytest.ini_options] +testpaths = "tests" faulthandler_timeout = 60 -addopts = "--cov . --cov-report term --cov-report html:.reports/html --no-cov-on-fail --workers auto --durations 5" +log_level = "DEBUG" +log_format = "%(levelname)s: %(message)s" +addopts = "--cov . --cov-report term --cov-report html:.reports/html --no-cov-on-fail --durations 5 --color=yes --maxfail=1 --new-first -nauto" # Add --pdb for debugging norecursedirs = "tests/helpers" markers = [ "impure" ] diff --git a/pkgs/clan-cli/shell.nix b/pkgs/clan-cli/shell.nix index 7ac6e3c..0bb530c 100644 --- a/pkgs/clan-cli/shell.nix +++ b/pkgs/clan-cli/shell.nix @@ -22,38 +22,41 @@ mkShell { ]; shellHook = '' - tmp_path=$(realpath ./.direnv) + tmp_path=$(realpath ./.direnv) - repo_root=$(realpath .) - mkdir -p "$tmp_path/python/${pythonWithDeps.sitePackages}" + repo_root=$(realpath .) + mkdir -p "$tmp_path/python/${pythonWithDeps.sitePackages}" - # Install the package in editable mode - # This allows executing `clan` from within the dev-shell using the current - # version of the code and its dependencies. - ${pythonWithDeps.interpreter} -m pip install \ - --quiet \ - --disable-pip-version-check \ - --no-index \ - --no-build-isolation \ - --prefix "$tmp_path/python" \ - --editable $repo_root + # Install the package in editable mode + # This allows executing `clan` from within the dev-shell using the current + # version of the code and its dependencies. + ${pythonWithDeps.interpreter} -m pip install \ + --quiet \ + --disable-pip-version-check \ + --no-index \ + --no-build-isolation \ + --prefix "$tmp_path/python" \ + --editable $repo_root - rm -f clan_cli/nixpkgs clan_cli/webui/assets - ln -sf ${clan-cli.nixpkgs} clan_cli/nixpkgs - ln -sf ${ui-assets} clan_cli/webui/assets + rm -f clan_cli/nixpkgs clan_cli/webui/assets + ln -sf ${clan-cli.nixpkgs} clan_cli/nixpkgs + ln -sf ${ui-assets} clan_cli/webui/assets - export PATH="$tmp_path/python/bin:${checkScript}/bin:$PATH" - export PYTHONPATH="$repo_root:$tmp_path/python/${pythonWithDeps.sitePackages}:" + export PATH="$tmp_path/python/bin:${checkScript}/bin:$PATH" + export PYTHONPATH="$repo_root:$tmp_path/python/${pythonWithDeps.sitePackages}:" + export PYTHONBREAKPOINT=ipdb.set_trace - export XDG_DATA_DIRS="$tmp_path/share''${XDG_DATA_DIRS:+:$XDG_DATA_DIRS}" - export fish_complete_path="$tmp_path/share/fish/vendor_completions.d''${fish_complete_path:+:$fish_complete_path}" - mkdir -p \ - $tmp_path/share/fish/vendor_completions.d \ - $tmp_path/share/bash-completion/completions \ - $tmp_path/share/zsh/site-functions - register-python-argcomplete --shell fish clan > $tmp_path/share/fish/vendor_completions.d/clan.fish - register-python-argcomplete --shell bash clan > $tmp_path/share/bash-completion/completions/clan + export XDG_DATA_DIRS="$tmp_path/share''${XDG_DATA_DIRS:+:$XDG_DATA_DIRS}" + export fish_complete_path="$tmp_path/share/fish/vendor_completions.d''${fish_complete_path:+:$fish_complete_path}" + mkdir -p \ + $tmp_path/share/fish/vendor_completions.d \ + $tmp_path/share/bash-completion/completions \ + $tmp_path/share/zsh/site-functions + register-python-argcomplete --shell fish clan > $tmp_path/share/fish/vendor_completions.d/clan.fish + register-python-argcomplete --shell bash clan > $tmp_path/share/bash-completion/completions/clan - ./bin/clan machines create example + + ./bin/clan flakes create example_clan + ./bin/clan machines create example_machine example_clan ''; } diff --git a/pkgs/clan-cli/tests/fixtures_flakes.py b/pkgs/clan-cli/tests/fixtures_flakes.py index b775790..ff716c9 100644 --- a/pkgs/clan-cli/tests/fixtures_flakes.py +++ b/pkgs/clan-cli/tests/fixtures_flakes.py @@ -1,4 +1,5 @@ import fileinput +import logging import shutil import tempfile from pathlib import Path @@ -8,7 +9,9 @@ import pytest from root import CLAN_CORE from clan_cli.dirs import nixpkgs_source -from clan_cli.flakes.types import FlakeName +from clan_cli.types import FlakeName + +log = logging.getLogger(__name__) # substitutes string sin a file. @@ -28,73 +31,85 @@ def substitute( print(line, end="") -class TestFlake(NamedTuple): +class FlakeForTest(NamedTuple): name: FlakeName path: Path def create_flake( monkeypatch: pytest.MonkeyPatch, + temporary_dir: Path, flake_name: FlakeName, clan_core_flake: Path | None = None, machines: list[str] = [], remote: bool = False, -) -> Iterator[TestFlake]: +) -> Iterator[FlakeForTest]: """ Creates a flake with the given name and machines. The machine names map to the machines in ./test_machines """ template = Path(__file__).parent / flake_name + # copy the template to a new temporary location - with tempfile.TemporaryDirectory() as tmpdir_: - home = Path(tmpdir_) - flake = home / flake_name - shutil.copytree(template, flake) - # lookup the requested machines in ./test_machines and include them - if machines: - (flake / "machines").mkdir(parents=True, exist_ok=True) - for machine_name in machines: - machine_path = Path(__file__).parent / "machines" / machine_name - shutil.copytree(machine_path, flake / "machines" / machine_name) - substitute(flake / "machines" / machine_name / "default.nix", flake) - # in the flake.nix file replace the string __CLAN_URL__ with the the clan flake - # provided by get_test_flake_toplevel - flake_nix = flake / "flake.nix" - # this is where we would install the sops key to, when updating - substitute(flake_nix, clan_core_flake, flake) - if remote: - with tempfile.TemporaryDirectory() as workdir: - monkeypatch.chdir(workdir) - monkeypatch.setenv("HOME", str(home)) - yield TestFlake(flake_name, flake) - else: - monkeypatch.chdir(flake) + home = Path(temporary_dir) + flake = home / ".local/state/clan/flake" / flake_name + shutil.copytree(template, flake) + + # lookup the requested machines in ./test_machines and include them + if machines: + (flake / "machines").mkdir(parents=True, exist_ok=True) + for machine_name in machines: + machine_path = Path(__file__).parent / "machines" / machine_name + shutil.copytree(machine_path, flake / "machines" / machine_name) + substitute(flake / "machines" / machine_name / "default.nix", flake) + # in the flake.nix file replace the string __CLAN_URL__ with the the clan flake + # provided by get_test_flake_toplevel + flake_nix = flake / "flake.nix" + # this is where we would install the sops key to, when updating + substitute(flake_nix, clan_core_flake, flake) + if remote: + with tempfile.TemporaryDirectory() as workdir: + monkeypatch.chdir(workdir) monkeypatch.setenv("HOME", str(home)) - yield TestFlake(flake_name, flake) + yield FlakeForTest(flake_name, flake) + else: + monkeypatch.chdir(flake) + monkeypatch.setenv("HOME", str(home)) + yield FlakeForTest(flake_name, flake) @pytest.fixture -def test_flake(monkeypatch: pytest.MonkeyPatch) -> Iterator[TestFlake]: - yield from create_flake(monkeypatch, FlakeName("test_flake")) +def test_flake( + monkeypatch: pytest.MonkeyPatch, temporary_home: Path +) -> Iterator[FlakeForTest]: + yield from create_flake(monkeypatch, temporary_home, FlakeName("test_flake")) @pytest.fixture -def test_flake_with_core(monkeypatch: pytest.MonkeyPatch) -> Iterator[TestFlake]: - if not (CLAN_CORE / "flake.nix").exists(): - raise Exception( - "clan-core flake not found. This test requires the clan-core flake to be present" - ) - yield from create_flake(monkeypatch, FlakeName("test_flake_with_core"), CLAN_CORE) - - -@pytest.fixture -def test_flake_with_core_and_pass( - monkeypatch: pytest.MonkeyPatch, -) -> Iterator[TestFlake]: +def test_flake_with_core( + monkeypatch: pytest.MonkeyPatch, temporary_dir: Path +) -> Iterator[FlakeForTest]: if not (CLAN_CORE / "flake.nix").exists(): raise Exception( "clan-core flake not found. This test requires the clan-core flake to be present" ) yield from create_flake( - monkeypatch, FlakeName("test_flake_with_core_and_pass"), CLAN_CORE + monkeypatch, temporary_dir, FlakeName("test_flake_with_core"), CLAN_CORE + ) + + +@pytest.fixture +def test_flake_with_core_and_pass( + monkeypatch: pytest.MonkeyPatch, + temporary_dir: Path, +) -> Iterator[FlakeForTest]: + if not (CLAN_CORE / "flake.nix").exists(): + raise Exception( + "clan-core flake not found. This test requires the clan-core flake to be present" + ) + yield from create_flake( + monkeypatch, + temporary_dir, + FlakeName("test_flake_with_core_and_pass"), + CLAN_CORE, ) diff --git a/pkgs/clan-cli/tests/helpers/cli.py b/pkgs/clan-cli/tests/helpers/cli.py index ea633c2..3deaef7 100644 --- a/pkgs/clan-cli/tests/helpers/cli.py +++ b/pkgs/clan-cli/tests/helpers/cli.py @@ -1,6 +1,11 @@ import argparse +import logging +import shlex from clan_cli import create_parser +from clan_cli.custom_logger import get_caller + +log = logging.getLogger(__name__) class Cli: @@ -8,6 +13,9 @@ class Cli: self.parser = create_parser(prog="clan") def run(self, args: list[str]) -> argparse.Namespace: + cmd = shlex.join(["clan"] + args) + log.debug(f"$ {cmd}") + log.debug(f"Caller {get_caller()}") parsed = self.parser.parse_args(args) if hasattr(parsed, "func"): parsed.func(parsed) diff --git a/pkgs/clan-cli/tests/temporary_dir.py b/pkgs/clan-cli/tests/temporary_dir.py index c8e31ed..4d6ca17 100644 --- a/pkgs/clan-cli/tests/temporary_dir.py +++ b/pkgs/clan-cli/tests/temporary_dir.py @@ -1,3 +1,4 @@ +import logging import os import tempfile from pathlib import Path @@ -5,14 +6,20 @@ from typing import Iterator import pytest +log = logging.getLogger(__name__) + @pytest.fixture -def temporary_dir() -> Iterator[Path]: - if os.getenv("TEST_KEEP_TEMPORARY_DIR"): - temp_dir = tempfile.mkdtemp(prefix="pytest-") - path = Path(temp_dir) +def temporary_home(monkeypatch: pytest.MonkeyPatch) -> Iterator[Path]: + env_dir = os.getenv("TEST_TEMPORARY_DIR") + if env_dir is not None: + path = Path(env_dir).resolve() + log.debug("Temp HOME directory: %s", str(path)) + monkeypatch.setenv("HOME", str(path)) yield path - print("=========> Keeping temporary directory: ", path) else: + log.debug("TEST_TEMPORARY_DIR not set, using TemporaryDirectory") with tempfile.TemporaryDirectory(prefix="pytest-") as dirpath: + monkeypatch.setenv("HOME", str(dirpath)) + log.debug("Temp HOME directory: %s", str(dirpath)) yield Path(dirpath) diff --git a/pkgs/clan-cli/tests/test_config.py b/pkgs/clan-cli/tests/test_config.py index 796a798..3292144 100644 --- a/pkgs/clan-cli/tests/test_config.py +++ b/pkgs/clan-cli/tests/test_config.py @@ -9,6 +9,7 @@ from cli import Cli from clan_cli import config from clan_cli.config import parsing from clan_cli.errors import ClanError +from fixtures_flakes import FlakeForTest example_options = f"{Path(config.__file__).parent}/jsonschema/options.json" @@ -29,7 +30,7 @@ example_options = f"{Path(config.__file__).parent}/jsonschema/options.json" def test_set_some_option( args: list[str], expected: dict[str, Any], - test_flake: Path, + test_flake: FlakeForTest, ) -> None: # create temporary file for out_file with tempfile.NamedTemporaryFile() as out_file: @@ -46,24 +47,24 @@ def test_set_some_option( out_file.name, ] + args + + [test_flake.name] ) json_out = json.loads(open(out_file.name).read()) assert json_out == expected def test_configure_machine( - test_flake: Path, - temporary_dir: Path, + test_flake: FlakeForTest, + temporary_home: Path, capsys: pytest.CaptureFixture, monkeypatch: pytest.MonkeyPatch, ) -> None: - monkeypatch.setenv("HOME", str(temporary_dir)) cli = Cli() - cli.run(["config", "-m", "machine1", "clan.jitsi.enable", "true"]) + cli.run(["config", "-m", "machine1", "clan.jitsi.enable", "true", test_flake.name]) # clear the output buffer capsys.readouterr() # read a option value - cli.run(["config", "-m", "machine1", "clan.jitsi.enable"]) + cli.run(["config", "-m", "machine1", "clan.jitsi.enable", test_flake.name]) # read the output assert capsys.readouterr().out == "true\n" diff --git a/pkgs/clan-cli/tests/test_create_flake.py b/pkgs/clan-cli/tests/test_create_flake.py index ec6976a..0de4e17 100644 --- a/pkgs/clan-cli/tests/test_create_flake.py +++ b/pkgs/clan-cli/tests/test_create_flake.py @@ -6,6 +6,9 @@ import pytest from api import TestClient from cli import Cli +from clan_cli.dirs import clan_flakes_dir +from clan_cli.flakes.create import DEFAULT_URL + @pytest.fixture def cli() -> Cli: @@ -14,15 +17,16 @@ def cli() -> Cli: @pytest.mark.impure def test_create_flake_api( - monkeypatch: pytest.MonkeyPatch, api: TestClient, temporary_dir: Path + monkeypatch: pytest.MonkeyPatch, api: TestClient, temporary_home: Path ) -> None: - flake_dir = temporary_dir / "flake_dir" - flake_dir_str = str(flake_dir.resolve()) + monkeypatch.chdir(clan_flakes_dir()) + flake_name = "flake_dir" + flake_dir = clan_flakes_dir() / flake_name response = api.post( "/api/flake/create", json=dict( - dest=flake_dir_str, - url="git+https://git.clan.lol/clan/clan-core#new-clan", + dest=str(flake_dir), + url=str(DEFAULT_URL), ), ) @@ -34,19 +38,21 @@ def test_create_flake_api( @pytest.mark.impure def test_create_flake( monkeypatch: pytest.MonkeyPatch, - temporary_dir: Path, capsys: pytest.CaptureFixture, + temporary_home: Path, cli: Cli, ) -> None: - monkeypatch.chdir(temporary_dir) - flake_dir = temporary_dir / "flake_dir" - flake_dir_str = str(flake_dir.resolve()) - cli.run(["flake", "create", flake_dir_str]) + monkeypatch.chdir(clan_flakes_dir()) + flake_name = "flake_dir" + flake_dir = clan_flakes_dir() / flake_name + + cli.run(["flakes", "create", flake_name]) assert (flake_dir / ".clan-flake").exists() monkeypatch.chdir(flake_dir) - cli.run(["machines", "create", "machine1"]) + cli.run(["machines", "create", "machine1", flake_name]) capsys.readouterr() # flush cache - cli.run(["machines", "list"]) + + cli.run(["machines", "list", flake_name]) assert "machine1" in capsys.readouterr().out flake_show = subprocess.run( ["nix", "flake", "show", "--json"], @@ -61,6 +67,17 @@ def test_create_flake( pytest.fail("nixosConfigurations.machine1 not found in flake outputs") # configure machine1 capsys.readouterr() - cli.run(["config", "--machine", "machine1", "services.openssh.enable"]) + cli.run( + ["config", "--machine", "machine1", "services.openssh.enable", "", flake_name] + ) capsys.readouterr() - cli.run(["config", "--machine", "machine1", "services.openssh.enable", "true"]) + cli.run( + [ + "config", + "--machine", + "machine1", + "services.openssh.enable", + "true", + flake_name, + ] + ) diff --git a/pkgs/clan-cli/tests/test_flake_with_core/flake.nix b/pkgs/clan-cli/tests/test_flake_with_core/flake.nix index b7b980c..0c287e4 100644 --- a/pkgs/clan-cli/tests/test_flake_with_core/flake.nix +++ b/pkgs/clan-cli/tests/test_flake_with_core/flake.nix @@ -9,6 +9,7 @@ let clan = clan-core.lib.buildClan { directory = self; + clanName = "test_with_core_clan"; machines = { vm1 = { lib, ... }: { clan.networking.deploymentAddress = "__CLAN_DEPLOYMENT_ADDRESS__"; diff --git a/pkgs/clan-cli/tests/test_flake_with_core_and_pass/flake.nix b/pkgs/clan-cli/tests/test_flake_with_core_and_pass/flake.nix index 38346de..39a01f0 100644 --- a/pkgs/clan-cli/tests/test_flake_with_core_and_pass/flake.nix +++ b/pkgs/clan-cli/tests/test_flake_with_core_and_pass/flake.nix @@ -9,6 +9,7 @@ let clan = clan-core.lib.buildClan { directory = self; + clanName = "test_with_core_and_pass_clan"; machines = { vm1 = { lib, ... }: { clan.networking.deploymentAddress = "__CLAN_DEPLOYMENT_ADDRESS__"; diff --git a/pkgs/clan-cli/tests/test_flake_with_core_dynamic_machines/flake.nix b/pkgs/clan-cli/tests/test_flake_with_core_dynamic_machines/flake.nix index 7c4558d..13c30ea 100644 --- a/pkgs/clan-cli/tests/test_flake_with_core_dynamic_machines/flake.nix +++ b/pkgs/clan-cli/tests/test_flake_with_core_dynamic_machines/flake.nix @@ -9,6 +9,7 @@ let clan = clan-core.lib.buildClan { directory = self; + clanName = "core_dynamic_machine_clan"; machines = let machineModules = builtins.readDir (self + "/machines"); diff --git a/pkgs/clan-cli/tests/test_machines_config.py b/pkgs/clan-cli/tests/test_machines_config.py index a7ab422..ee07ad0 100644 --- a/pkgs/clan-cli/tests/test_machines_config.py +++ b/pkgs/clan-cli/tests/test_machines_config.py @@ -1,8 +1,8 @@ -from fixtures_flakes import TestFlake +from fixtures_flakes import FlakeForTest from clan_cli.config import machine -def test_schema_for_machine(test_flake: TestFlake) -> None: +def test_schema_for_machine(test_flake: FlakeForTest) -> None: schema = machine.schema_for_machine(test_flake.name, "machine1") assert "properties" in schema diff --git a/pkgs/clan-cli/tests/test_secrets_cli.py b/pkgs/clan-cli/tests/test_secrets_cli.py index c93c63d..360b29b 100644 --- a/pkgs/clan-cli/tests/test_secrets_cli.py +++ b/pkgs/clan-cli/tests/test_secrets_cli.py @@ -1,30 +1,33 @@ +import logging import os from contextlib import contextmanager -from pathlib import Path from typing import TYPE_CHECKING, Iterator import pytest from cli import Cli +from fixtures_flakes import FlakeForTest from clan_cli.errors import ClanError if TYPE_CHECKING: from age_keys import KeyPair +log = logging.getLogger(__name__) + def _test_identities( what: str, - test_flake: Path, + test_flake: FlakeForTest, capsys: pytest.CaptureFixture, age_keys: list["KeyPair"], ) -> None: cli = Cli() - sops_folder = test_flake / "sops" + sops_folder = test_flake.path / "sops" - cli.run(["secrets", what, "add", "foo", age_keys[0].pubkey]) + cli.run(["secrets", what, "add", "foo", age_keys[0].pubkey, test_flake.name]) assert (sops_folder / what / "foo" / "key.json").exists() with pytest.raises(ClanError): - cli.run(["secrets", what, "add", "foo", age_keys[0].pubkey]) + cli.run(["secrets", what, "add", "foo", age_keys[0].pubkey, test_flake.name]) cli.run( [ @@ -34,73 +37,80 @@ def _test_identities( "-f", "foo", age_keys[0].privkey, + test_flake.name, ] ) capsys.readouterr() # empty the buffer - cli.run(["secrets", what, "get", "foo"]) + cli.run(["secrets", what, "get", "foo", test_flake.name]) out = capsys.readouterr() # empty the buffer assert age_keys[0].pubkey in out.out capsys.readouterr() # empty the buffer - cli.run(["secrets", what, "list"]) + cli.run(["secrets", what, "list", test_flake.name]) out = capsys.readouterr() # empty the buffer assert "foo" in out.out - cli.run(["secrets", what, "remove", "foo"]) + cli.run(["secrets", what, "remove", "foo", test_flake.name]) assert not (sops_folder / what / "foo" / "key.json").exists() with pytest.raises(ClanError): # already removed - cli.run(["secrets", what, "remove", "foo"]) + cli.run(["secrets", what, "remove", "foo", test_flake.name]) capsys.readouterr() - cli.run(["secrets", what, "list"]) + cli.run(["secrets", what, "list", test_flake.name]) out = capsys.readouterr() assert "foo" not in out.out def test_users( - test_flake: Path, capsys: pytest.CaptureFixture, age_keys: list["KeyPair"] + test_flake: FlakeForTest, capsys: pytest.CaptureFixture, age_keys: list["KeyPair"] ) -> None: _test_identities("users", test_flake, capsys, age_keys) def test_machines( - test_flake: Path, capsys: pytest.CaptureFixture, age_keys: list["KeyPair"] + test_flake: FlakeForTest, capsys: pytest.CaptureFixture, age_keys: list["KeyPair"] ) -> None: _test_identities("machines", test_flake, capsys, age_keys) def test_groups( - test_flake: Path, capsys: pytest.CaptureFixture, age_keys: list["KeyPair"] + test_flake: FlakeForTest, capsys: pytest.CaptureFixture, age_keys: list["KeyPair"] ) -> None: cli = Cli() capsys.readouterr() # empty the buffer - cli.run(["secrets", "groups", "list"]) + cli.run(["secrets", "groups", "list", test_flake.name]) assert capsys.readouterr().out == "" with pytest.raises(ClanError): # machine does not exist yet - cli.run(["secrets", "groups", "add-machine", "group1", "machine1"]) + cli.run( + ["secrets", "groups", "add-machine", "group1", "machine1", test_flake.name] + ) with pytest.raises(ClanError): # user does not exist yet - cli.run(["secrets", "groups", "add-user", "groupb1", "user1"]) - cli.run(["secrets", "machines", "add", "machine1", age_keys[0].pubkey]) - cli.run(["secrets", "groups", "add-machine", "group1", "machine1"]) + cli.run(["secrets", "groups", "add-user", "groupb1", "user1", test_flake.name]) + cli.run( + ["secrets", "machines", "add", "machine1", age_keys[0].pubkey, test_flake.name] + ) + cli.run(["secrets", "groups", "add-machine", "group1", "machine1", test_flake.name]) # Should this fail? - cli.run(["secrets", "groups", "add-machine", "group1", "machine1"]) + cli.run(["secrets", "groups", "add-machine", "group1", "machine1", test_flake.name]) - cli.run(["secrets", "users", "add", "user1", age_keys[0].pubkey]) - cli.run(["secrets", "groups", "add-user", "group1", "user1"]) + cli.run(["secrets", "users", "add", "user1", age_keys[0].pubkey, test_flake.name]) + cli.run(["secrets", "groups", "add-user", "group1", "user1", test_flake.name]) capsys.readouterr() # empty the buffer - cli.run(["secrets", "groups", "list"]) + cli.run(["secrets", "groups", "list", test_flake.name]) out = capsys.readouterr().out assert "user1" in out assert "machine1" in out - cli.run(["secrets", "groups", "remove-user", "group1", "user1"]) - cli.run(["secrets", "groups", "remove-machine", "group1", "machine1"]) - groups = os.listdir(test_flake / "sops" / "groups") + cli.run(["secrets", "groups", "remove-user", "group1", "user1", test_flake.name]) + cli.run( + ["secrets", "groups", "remove-machine", "group1", "machine1", test_flake.name] + ) + groups = os.listdir(test_flake.path / "sops" / "groups") assert len(groups) == 0 @@ -117,104 +127,114 @@ def use_key(key: str, monkeypatch: pytest.MonkeyPatch) -> Iterator[None]: def test_secrets( - test_flake: Path, + test_flake: FlakeForTest, capsys: pytest.CaptureFixture, monkeypatch: pytest.MonkeyPatch, age_keys: list["KeyPair"], ) -> None: cli = Cli() capsys.readouterr() # empty the buffer - cli.run(["secrets", "list"]) + cli.run(["secrets", "list", test_flake.name]) assert capsys.readouterr().out == "" monkeypatch.setenv("SOPS_NIX_SECRET", "foo") - monkeypatch.setenv("SOPS_AGE_KEY_FILE", str(test_flake / ".." / "age.key")) + monkeypatch.setenv("SOPS_AGE_KEY_FILE", str(test_flake.path / ".." / "age.key")) cli.run(["secrets", "key", "generate"]) capsys.readouterr() # empty the buffer cli.run(["secrets", "key", "show"]) key = capsys.readouterr().out assert key.startswith("age1") - cli.run(["secrets", "users", "add", "testuser", key]) + cli.run(["secrets", "users", "add", "testuser", key, test_flake.name]) with pytest.raises(ClanError): # does not exist yet - cli.run(["secrets", "get", "nonexisting"]) - cli.run(["secrets", "set", "initialkey"]) + cli.run(["secrets", "get", "nonexisting", test_flake.name]) + cli.run(["secrets", "set", "initialkey", test_flake.name]) capsys.readouterr() - cli.run(["secrets", "get", "initialkey"]) + cli.run(["secrets", "get", "initialkey", test_flake.name]) assert capsys.readouterr().out == "foo" capsys.readouterr() - cli.run(["secrets", "users", "list"]) + cli.run(["secrets", "users", "list", test_flake.name]) users = capsys.readouterr().out.rstrip().split("\n") assert len(users) == 1, f"users: {users}" owner = users[0] monkeypatch.setenv("EDITOR", "cat") - cli.run(["secrets", "set", "--edit", "initialkey"]) + cli.run(["secrets", "set", "--edit", "initialkey", test_flake.name]) monkeypatch.delenv("EDITOR") - cli.run(["secrets", "rename", "initialkey", "key"]) + cli.run(["secrets", "rename", "initialkey", "key", test_flake.name]) capsys.readouterr() # empty the buffer - cli.run(["secrets", "list"]) + cli.run(["secrets", "list", test_flake.name]) assert capsys.readouterr().out == "key\n" - cli.run(["secrets", "machines", "add", "machine1", age_keys[0].pubkey]) - cli.run(["secrets", "machines", "add-secret", "machine1", "key"]) + cli.run( + ["secrets", "machines", "add", "machine1", age_keys[0].pubkey, test_flake.name] + ) + cli.run(["secrets", "machines", "add-secret", "machine1", "key", test_flake.name]) capsys.readouterr() - cli.run(["secrets", "machines", "list"]) + cli.run(["secrets", "machines", "list", test_flake.name]) assert capsys.readouterr().out == "machine1\n" with use_key(age_keys[0].privkey, monkeypatch): capsys.readouterr() - cli.run(["secrets", "get", "key"]) + cli.run(["secrets", "get", "key", test_flake.name]) assert capsys.readouterr().out == "foo" - cli.run(["secrets", "machines", "remove-secret", "machine1", "key"]) + cli.run( + ["secrets", "machines", "remove-secret", "machine1", "key", test_flake.name] + ) - cli.run(["secrets", "users", "add", "user1", age_keys[1].pubkey]) - cli.run(["secrets", "users", "add-secret", "user1", "key"]) + cli.run(["secrets", "users", "add", "user1", age_keys[1].pubkey, test_flake.name]) + cli.run(["secrets", "users", "add-secret", "user1", "key", test_flake.name]) capsys.readouterr() with use_key(age_keys[1].privkey, monkeypatch): - cli.run(["secrets", "get", "key"]) + cli.run(["secrets", "get", "key", test_flake.name]) assert capsys.readouterr().out == "foo" - cli.run(["secrets", "users", "remove-secret", "user1", "key"]) + cli.run(["secrets", "users", "remove-secret", "user1", "key", test_flake.name]) with pytest.raises(ClanError): # does not exist yet - cli.run(["secrets", "groups", "add-secret", "admin-group", "key"]) - cli.run(["secrets", "groups", "add-user", "admin-group", "user1"]) - cli.run(["secrets", "groups", "add-user", "admin-group", owner]) - cli.run(["secrets", "groups", "add-secret", "admin-group", "key"]) + cli.run( + ["secrets", "groups", "add-secret", "admin-group", "key", test_flake.name] + ) + cli.run(["secrets", "groups", "add-user", "admin-group", "user1", test_flake.name]) + cli.run(["secrets", "groups", "add-user", "admin-group", owner, test_flake.name]) + cli.run(["secrets", "groups", "add-secret", "admin-group", "key", test_flake.name]) capsys.readouterr() # empty the buffer - cli.run(["secrets", "set", "--group", "admin-group", "key2"]) + cli.run(["secrets", "set", "--group", "admin-group", "key2", test_flake.name]) with use_key(age_keys[1].privkey, monkeypatch): capsys.readouterr() - cli.run(["secrets", "get", "key"]) + cli.run(["secrets", "get", "key", test_flake.name]) assert capsys.readouterr().out == "foo" # extend group will update secrets - cli.run(["secrets", "users", "add", "user2", age_keys[2].pubkey]) - cli.run(["secrets", "groups", "add-user", "admin-group", "user2"]) + cli.run(["secrets", "users", "add", "user2", age_keys[2].pubkey, test_flake.name]) + cli.run(["secrets", "groups", "add-user", "admin-group", "user2", test_flake.name]) with use_key(age_keys[2].privkey, monkeypatch): # user2 capsys.readouterr() - cli.run(["secrets", "get", "key"]) + cli.run(["secrets", "get", "key", test_flake.name]) assert capsys.readouterr().out == "foo" - cli.run(["secrets", "groups", "remove-user", "admin-group", "user2"]) + cli.run( + ["secrets", "groups", "remove-user", "admin-group", "user2", test_flake.name] + ) with pytest.raises(ClanError), use_key(age_keys[2].privkey, monkeypatch): # user2 is not in the group anymore capsys.readouterr() - cli.run(["secrets", "get", "key"]) + cli.run(["secrets", "get", "key", test_flake.name]) print(capsys.readouterr().out) - cli.run(["secrets", "groups", "remove-secret", "admin-group", "key"]) + cli.run( + ["secrets", "groups", "remove-secret", "admin-group", "key", test_flake.name] + ) - cli.run(["secrets", "remove", "key"]) - cli.run(["secrets", "remove", "key2"]) + cli.run(["secrets", "remove", "key", test_flake.name]) + cli.run(["secrets", "remove", "key2", test_flake.name]) capsys.readouterr() # empty the buffer - cli.run(["secrets", "list"]) + cli.run(["secrets", "list", test_flake.name]) assert capsys.readouterr().out == "" diff --git a/pkgs/clan-cli/tests/test_secrets_generate.py b/pkgs/clan-cli/tests/test_secrets_generate.py index 2263231..5066d1e 100644 --- a/pkgs/clan-cli/tests/test_secrets_generate.py +++ b/pkgs/clan-cli/tests/test_secrets_generate.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING import pytest from cli import Cli -from fixtures_flakes import TestFlake +from fixtures_flakes import FlakeForTest from clan_cli.machines.facts import machine_get_fact from clan_cli.secrets.folders import sops_secrets_folder @@ -15,7 +15,7 @@ if TYPE_CHECKING: @pytest.mark.impure def test_generate_secret( monkeypatch: pytest.MonkeyPatch, - test_flake_with_core: TestFlake, + test_flake_with_core: FlakeForTest, age_keys: list["KeyPair"], ) -> None: monkeypatch.chdir(test_flake_with_core.path) diff --git a/pkgs/clan-cli/tests/test_secrets_password_store.py b/pkgs/clan-cli/tests/test_secrets_password_store.py index 999f08a..27e4352 100644 --- a/pkgs/clan-cli/tests/test_secrets_password_store.py +++ b/pkgs/clan-cli/tests/test_secrets_password_store.py @@ -3,7 +3,7 @@ from pathlib import Path import pytest from cli import Cli -from fixtures_flakes import TestFlake +from fixtures_flakes import FlakeForTest from clan_cli.machines.facts import machine_get_fact from clan_cli.nix import nix_shell @@ -13,7 +13,7 @@ from clan_cli.ssh import HostGroup @pytest.mark.impure def test_upload_secret( monkeypatch: pytest.MonkeyPatch, - test_flake_with_core_and_pass: TestFlake, + test_flake_with_core_and_pass: FlakeForTest, temporary_dir: Path, host_group: HostGroup, ) -> None: diff --git a/pkgs/clan-cli/tests/test_vms_api_create.py b/pkgs/clan-cli/tests/test_vms_api_create.py index 1de515d..e2c0f94 100644 --- a/pkgs/clan-cli/tests/test_vms_api_create.py +++ b/pkgs/clan-cli/tests/test_vms_api_create.py @@ -5,20 +5,23 @@ from typing import TYPE_CHECKING, Iterator import pytest from api import TestClient from cli import Cli -from fixtures_flakes import TestFlake, create_flake +from fixtures_flakes import FlakeForTest, create_flake from httpx import SyncByteStream from root import CLAN_CORE -from clan_cli.flakes.types import FlakeName +from clan_cli.types import FlakeName if TYPE_CHECKING: from age_keys import KeyPair @pytest.fixture -def flake_with_vm_with_secrets(monkeypatch: pytest.MonkeyPatch) -> Iterator[TestFlake]: +def flake_with_vm_with_secrets( + monkeypatch: pytest.MonkeyPatch, temporary_home: Path +) -> Iterator[FlakeForTest]: yield from create_flake( monkeypatch, + temporary_home, FlakeName("test_flake_with_core_dynamic_machines"), CLAN_CORE, machines=["vm_with_secrets"], @@ -27,10 +30,11 @@ def flake_with_vm_with_secrets(monkeypatch: pytest.MonkeyPatch) -> Iterator[Test @pytest.fixture def remote_flake_with_vm_without_secrets( - monkeypatch: pytest.MonkeyPatch, -) -> Iterator[TestFlake]: + monkeypatch: pytest.MonkeyPatch, temporary_home: Path +) -> Iterator[FlakeForTest]: yield from create_flake( monkeypatch, + temporary_home, FlakeName("test_flake_with_core_dynamic_machines"), CLAN_CORE, machines=["vm_without_secrets"], @@ -41,11 +45,12 @@ def remote_flake_with_vm_without_secrets( @pytest.fixture def create_user_with_age_key( monkeypatch: pytest.MonkeyPatch, + test_flake: FlakeForTest, age_keys: list["KeyPair"], ) -> None: monkeypatch.setenv("SOPS_AGE_KEY", age_keys[0].privkey) cli = Cli() - cli.run(["secrets", "users", "add", "user1", age_keys[0].pubkey]) + cli.run(["secrets", "users", "add", "user1", age_keys[0].pubkey, test_flake.name]) def generic_create_vm_test(api: TestClient, flake: Path, vm: str) -> None: @@ -91,10 +96,10 @@ def generic_create_vm_test(api: TestClient, flake: Path, vm: str) -> None: def test_create_local( api: TestClient, monkeypatch: pytest.MonkeyPatch, - flake_with_vm_with_secrets: Path, + flake_with_vm_with_secrets: FlakeForTest, create_user_with_age_key: None, ) -> None: - generic_create_vm_test(api, flake_with_vm_with_secrets, "vm_with_secrets") + generic_create_vm_test(api, flake_with_vm_with_secrets.path, "vm_with_secrets") @pytest.mark.skipif(not os.path.exists("/dev/kvm"), reason="Requires KVM") @@ -102,8 +107,8 @@ def test_create_local( def test_create_remote( api: TestClient, monkeypatch: pytest.MonkeyPatch, - remote_flake_with_vm_without_secrets: Path, + remote_flake_with_vm_without_secrets: FlakeForTest, ) -> None: generic_create_vm_test( - api, remote_flake_with_vm_without_secrets, "vm_without_secrets" + api, remote_flake_with_vm_without_secrets.path, "vm_without_secrets" ) diff --git a/pkgs/ui/.eslintrc.json b/pkgs/ui/.eslintrc.json index 557e54a..41f3ebc 100644 --- a/pkgs/ui/.eslintrc.json +++ b/pkgs/ui/.eslintrc.json @@ -1,5 +1,7 @@ { "root": true, - "extends": ["next/core-web-vitals", "plugin:tailwindcss/recommended"], + "extends": ["next/core-web-vitals", "plugin:tailwindcss/recommended", "plugin:@typescript-eslint/recommended"], + "parser": "@typescript-eslint/parser", + "plugins": ["@typescript-eslint"], "ignorePatterns": ["**/src/api/*"] } diff --git a/pkgs/ui/nix/update-ui-assets.sh b/pkgs/ui/nix/update-ui-assets.sh index 0b0e88f..2433012 100755 --- a/pkgs/ui/nix/update-ui-assets.sh +++ b/pkgs/ui/nix/update-ui-assets.sh @@ -4,7 +4,7 @@ set -xeuo pipefail # GITEA_TOKEN if [[ -z "${GITEA_TOKEN:-}" ]]; then echo "GITEA_TOKEN is not set" - echo "Go to https://git.clan.lol/user/settings/applications and generate a token" + echo "Go to https://gitea.gchq.icu/user/settings/applications and generate a token" exit 1 fi @@ -22,8 +22,10 @@ nix build '.#ui' --out-link "$tmpdir/result" tar --transform 's,^\.,assets,' -czvf "$tmpdir/assets.tar.gz" -C "$tmpdir"/result/lib/node_modules/*/out . NAR_HASH=$(nix-prefetch-url --unpack file://<(cat "$tmpdir/assets.tar.gz")) - -url="https://git.clan.lol/api/packages/clan/generic/ui/$NAR_HASH/assets.tar.gz" +owner=Luis +package_name=consulting-website +package_version=$NAR_HASH +url="https://gitea.gchq.icu/api/packages/$owner/generic/$package_name/$package_version/assets.tar.gz" set +x curl --upload-file "$tmpdir/assets.tar.gz" -X PUT "$url?token=$GITEA_TOKEN" set -x diff --git a/pkgs/ui/src/app/favicon.ico b/pkgs/ui/src/app/favicon.ico deleted file mode 100644 index 718d6fea4835ec2d246af9800eddb7ffb276240c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25931 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 diff --git a/pkgs/ui/src/app/join/page.tsx b/pkgs/ui/src/app/join/page.tsx deleted file mode 100644 index a693cb7..0000000 --- a/pkgs/ui/src/app/join/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import JoinPrequel from "@/views/joinPrequel"; - -export default function Page() { - return ; -} diff --git a/pkgs/ui/src/app/layout.tsx b/pkgs/ui/src/app/layout.tsx index a67038c..de9656c 100644 --- a/pkgs/ui/src/app/layout.tsx +++ b/pkgs/ui/src/app/layout.tsx @@ -3,11 +3,10 @@ import { Sidebar } from "@/components/sidebar"; import { tw } from "@/utils/tailwind"; import MenuIcon from "@mui/icons-material/Menu"; import { - Button, CssBaseline, IconButton, ThemeProvider, - useMediaQuery, + useMediaQuery } from "@mui/material"; import { StyledEngineProvider } from "@mui/material/styles"; import axios from "axios"; @@ -62,9 +61,7 @@ export default function RootLayout({ {(appState) => { const showSidebarDerived = Boolean( - showSidebar && - !appState.isLoading && - appState.data.isJoined, + showSidebar && !appState.isLoading, ); return ( <> @@ -86,9 +83,7 @@ export default function RootLayout({ hidden={true} onClick={() => setShowSidebar((c) => !c)} > - {!showSidebar && appState.data.isJoined && ( - - )} + {!showSidebar && }
@@ -105,21 +100,7 @@ export default function RootLayout({
-
- - - {children} -
+
{children}
diff --git a/pkgs/ui/src/app/machines/add/page.tsx b/pkgs/ui/src/app/machines/add/page.tsx deleted file mode 100644 index 3581d00..0000000 --- a/pkgs/ui/src/app/machines/add/page.tsx +++ /dev/null @@ -1,7 +0,0 @@ -"use client"; - -import { CreateMachineForm } from "@/components/createMachineForm"; - -export default function CreateMachine() { - return ; -} diff --git a/pkgs/ui/src/app/machines/edit/[name]/page.tsx b/pkgs/ui/src/app/machines/edit/[name]/page.tsx deleted file mode 100644 index ca995d4..0000000 --- a/pkgs/ui/src/app/machines/edit/[name]/page.tsx +++ /dev/null @@ -1,10 +0,0 @@ -interface DeviceEditProps { - params: { name: string }; -} - -export default function EditDevice(props: DeviceEditProps) { - const { - params: { name }, - } = props; - return
{name}
; -} diff --git a/pkgs/ui/src/app/machines/layout.tsx b/pkgs/ui/src/app/machines/layout.tsx deleted file mode 100644 index eace0be..0000000 --- a/pkgs/ui/src/app/machines/layout.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { MachineContextProvider } from "@/components/hooks/useMachines"; - -export default function Layout({ children }: { children: React.ReactNode }) { - return {children}; -} diff --git a/pkgs/ui/src/app/machines/page.tsx b/pkgs/ui/src/app/machines/page.tsx deleted file mode 100644 index 7b2e035..0000000 --- a/pkgs/ui/src/app/machines/page.tsx +++ /dev/null @@ -1,12 +0,0 @@ -"use client"; - -import { NodeTable } from "@/components/table"; -import { StrictMode } from "react"; - -export default function Page() { - return ( - - - - ); -} diff --git a/pkgs/ui/src/app/page.tsx b/pkgs/ui/src/app/page.tsx index e8bcab4..3fe74f4 100644 --- a/pkgs/ui/src/app/page.tsx +++ b/pkgs/ui/src/app/page.tsx @@ -1,41 +1,10 @@ "use client"; -import { NetworkOverview } from "@/components/dashboard/NetworkOverview"; import { RecentActivity } from "@/components/dashboard/activity"; -import { AppOverview } from "@/components/dashboard/appOverview"; -import { Notifications } from "@/components/dashboard/notifications"; -import { QuickActions } from "@/components/dashboard/quickActions"; -import { TaskQueue } from "@/components/dashboard/taskQueue"; import { useAppState } from "@/components/hooks/useAppContext"; import { LoadingOverlay } from "@/components/join/loadingOverlay"; -import JoinPrequel from "@/views/joinPrequel"; - -// interface DashboardCardProps { -// children?: React.ReactNode; -// rowSpan?: number; -// sx?: string; -// } -// const DashboardCard = (props: DashboardCardProps) => { -// const { children, rowSpan, sx = "" } = props; -// return ( -// //
-//
-// {children} -//
-// ); -// }; - -// interface DashboardPanelProps { -// children?: React.ReactNode; -// } -// const DashboardPanel = (props: DashboardPanelProps) => { -// const { children } = props; -// return ( -//
{children}
-// ); -// }; export default function Dashboard() { - const { data, isLoading } = useAppState(); + const { isLoading } = useAppState(); if (isLoading) { return (
@@ -48,26 +17,13 @@ export default function Dashboard() {
); - } - if (!data.isJoined) { - return ; - } - if (data.isJoined) { + } else { return (
-
- -
-
- -
- - -
); diff --git a/pkgs/ui/src/app/templates/[id]/page.tsx b/pkgs/ui/src/app/templates/[id]/page.tsx deleted file mode 100644 index 261c129..0000000 --- a/pkgs/ui/src/app/templates/[id]/page.tsx +++ /dev/null @@ -1,174 +0,0 @@ -"use client"; - -import { - Attachment, - ChevronLeft, - Delete, - Edit, - Group, - Key, - Settings, - SettingsEthernet, -} from "@mui/icons-material"; -import { - Avatar, - Button, - IconButton, - List, - ListItem, - ListItemAvatar, - ListItemSecondaryAction, - ListItemText, - ListSubheader, - Menu, - MenuItem, - Typography, -} from "@mui/material"; -import { useState } from "react"; -// import { useListMachines } from "@/api/default/default"; - -export async function generateStaticParams() { - return [{ id: "1" }, { id: "2" }]; -} - -function getTemplate(params: { id: string }) { - // const res = await fetch(`https://.../posts/${params.id}`); - return { - short: `My Template ${params.id}`, - }; -} - -interface TemplateDetailProps { - params: { id: string }; -} -export default function TemplateDetail({ params }: TemplateDetailProps) { - // const { data, isLoading } = useListMachines(); - const details = getTemplate(params); - - const [anchorEl, setAnchorEl] = useState(null); - const open = Boolean(anchorEl); - - const handleClick = (event: React.MouseEvent) => { - setAnchorEl(event.currentTarget); - }; - const handleClose = () => { - setAnchorEl(null); - }; - - return ( -
-
- -
-
-
- - N - - - {details.short} - -
- - - Details - - - - - - - - - - - - "} /> - - - - - - - - - - - - - - - - - - View - Rebuild - Delete - - - - - - - - - - -
-
-
- - -
-
-
- ); -} diff --git a/pkgs/ui/src/app/templates/page.tsx b/pkgs/ui/src/app/templates/page.tsx deleted file mode 100644 index 73bd862..0000000 --- a/pkgs/ui/src/app/templates/page.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { ChevronRight } from "@mui/icons-material"; -import { - Avatar, - Divider, - List, - ListItem, - ListItemAvatar, - ListItemButton, - ListItemSecondaryAction, - ListItemText, - Typography, -} from "@mui/material"; - -const templates = [ - { - id: "1", - name: "Office Preset", - date: "12 May 2050", - }, - { - id: "2", - name: "Work", - date: "30 Feb 2020", - }, - { - id: "3", - name: "Family", - date: "1 Okt 2022", - }, - { - id: "4", - name: "Standard", - date: "24 Jul 2021", - }, -]; - -export default function ImageOverview() { - return ( -
- Templates - - {templates.map(({ id, name, date }, idx, all) => ( - <> - - - - {name.slice(0, 1)} - - - - - - - - {idx < all.length - 1 && } - - ))} - -
- ); -} diff --git a/pkgs/ui/src/components/background.tsx b/pkgs/ui/src/components/background.tsx index ff90bbf..be56c78 100644 --- a/pkgs/ui/src/components/background.tsx +++ b/pkgs/ui/src/components/background.tsx @@ -6,7 +6,7 @@ import { import { useAppState } from "./hooks/useAppContext"; export default function Background() { - const { data, isLoading } = useAppState(); + const { isLoading } = useAppState(); return (
- {(isLoading || !data.isJoined) && ( + {isLoading && ( <> { - if (!isLoading && !error?.message && data?.data) { - return data?.data.schema; - } - return {}; - }, [data, isLoading, error]); - - const initialValues = useMemo( - () => - Object.entries(schema?.properties || {}).reduce((acc, [key, value]) => { - /*@ts-ignore*/ - const init: any = value?.default; - if (init) { - return { - ...acc, - [key]: init, - }; - } - return acc; - }, {}), - [schema], - ); - - return isLoading ? ( - - ) : error?.message ? ( -
{error?.message}
- ) : ( - - ); -} - -function ErrorList< - T = any, - S extends StrictRJSFSchema = RJSFSchema, - F extends FormContextType = any, ->({ errors, registry }: ErrorListProps) { - const { translateString } = registry; - return ( - - - - {translateString(TranslatableString.ErrorsLabel)} - - - {errors.map((error, i: number) => { - return ( - - - - - - - ); - })} - - - - ); -} - -function PureCustomConfig(props: PureCustomConfigProps) { - const { schema, formHooks } = props; - const { setValue, watch } = formHooks; - - console.log({ schema }); - - const configData = watch("config") as IChangeEvent; - - console.log({ configData }); - - const setConfig = (data: IChangeEvent) => { - console.log({ data }); - setValue("config", data); - }; - - const formRef = useRef(); - - const validate = () => { - const isValid: boolean = formRef?.current?.validateForm(); - console.log({ isValid }, formRef.current); - if (!isValid) { - formHooks.setError("config", { - message: "invalid config", - }); - toast.error( - "Configuration is invalid. Please check the highlighted fields for details.", - ); - } else { - formHooks.clearErrors("config"); - toast.success("Config seems valid"); - } - }; - - return ( -
( -
- -
- ), - }, - }} - /> - ); -} diff --git a/pkgs/ui/src/components/createMachineForm/index.tsx b/pkgs/ui/src/components/createMachineForm/index.tsx deleted file mode 100644 index 7fbef8e..0000000 --- a/pkgs/ui/src/components/createMachineForm/index.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import { - Box, - Button, - MobileStepper, - Step, - StepLabel, - Stepper, - useMediaQuery, - useTheme, -} from "@mui/material"; -import React, { useState } from "react"; -import { useForm } from "react-hook-form"; -import { CustomConfig } from "./customConfig"; -import { CreateMachineForm, FormStep } from "./interfaces"; - -export function CreateMachineForm() { - const formHooks = useForm({ - defaultValues: { - name: "", - config: {}, - }, - }); - const { handleSubmit, reset } = formHooks; - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down("sm")); - const [activeStep, setActiveStep] = useState(0); - - const steps: FormStep[] = [ - { - id: "template", - label: "Template", - content:
, - }, - { - id: "modules", - label: "Modules", - content:
, - }, - { - id: "config", - label: "Customize", - content: , - }, - { - id: "save", - label: "Save", - content:
, - }, - ]; - - const handleNext = () => { - if (activeStep < steps.length - 1) { - setActiveStep((prevActiveStep) => prevActiveStep + 1); - } - }; - - const handleBack = () => { - if (activeStep > 0) { - setActiveStep((prevActiveStep) => prevActiveStep - 1); - } - }; - - const handleReset = () => { - setActiveStep(0); - reset(); - }; - const currentStep = steps.at(activeStep); - - async function onSubmit(data: any) { - console.log({ data }, "Aggregated Data; creating machine from"); - } - - const BackButton = () => ( - - ); - - const NextButton = () => ( - <> - {activeStep !== steps.length - 1 && ( - - )} - {activeStep === steps.length - 1 && ( - - )} - - ); - return ( - - - {isMobile && ( - } - nextButton={} - steps={steps.length} - /> - )} - {!isMobile && ( - - {steps.map(({ label }, index) => { - const stepProps: { completed?: boolean } = {}; - const labelProps: { - optional?: React.ReactNode; - } = {}; - return ( - - {label} - - ); - })} - - )} - {/* */} - {/* The step Content */} - {currentStep && currentStep.content} - - {/* Desktop step controls */} - {!isMobile && ( - - - - - - )} - - - ); -} diff --git a/pkgs/ui/src/components/createMachineForm/interfaces.ts b/pkgs/ui/src/components/createMachineForm/interfaces.ts deleted file mode 100644 index c9e14c2..0000000 --- a/pkgs/ui/src/components/createMachineForm/interfaces.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ReactElement } from "react"; -import { UseFormReturn } from "react-hook-form"; - -export type StepId = "template" | "modules" | "config" | "save"; - -export type CreateMachineForm = { - name: string; - config: any; -}; - -export type FormHooks = UseFormReturn; - -export type FormStep = { - id: StepId; - label: string; - content: FormStepContent; -}; - -export interface FormStepContentProps { - formHooks: FormHooks; -} - -export type FormStepContent = ReactElement; diff --git a/pkgs/ui/src/components/createMachineForm/saveConfig.tsx b/pkgs/ui/src/components/createMachineForm/saveConfig.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/pkgs/ui/src/components/createMachineForm/selectModules.tsx b/pkgs/ui/src/components/createMachineForm/selectModules.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/pkgs/ui/src/components/createMachineForm/selectTemplate.tsx b/pkgs/ui/src/components/createMachineForm/selectTemplate.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/pkgs/ui/src/components/dashboard/NetworkOverview/index.tsx b/pkgs/ui/src/components/dashboard/NetworkOverview/index.tsx deleted file mode 100644 index 245e8e6..0000000 --- a/pkgs/ui/src/components/dashboard/NetworkOverview/index.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { DashboardCard } from "@/components/card"; -import { NoDataOverlay } from "@/components/noDataOverlay"; -import { status, Status, clanStatus } from "@/data/dashboardData"; -import { - Chip, - Divider, - List, - ListItem, - ListItemIcon, - ListItemText, -} from "@mui/material"; -import Link from "next/link"; -import React from "react"; - -const statusColorMap: Record< - Status, - "default" | "primary" | "secondary" | "error" | "info" | "success" | "warning" -> = { - online: "info", - offline: "error", - pending: "default", -}; - -const MAX_OTHERS = 5; - -export const NetworkOverview = () => { - const { self, other } = clanStatus; - - const firstOthers = other.slice(0, MAX_OTHERS); - return ( - - - - - - - - - - {!other.length && ( -
- Add devices} - /> - } - /> -
- )} - {firstOthers.map((o) => ( - - - - - - - ))} - {other.length > MAX_OTHERS && ( - - - - )} -
-
- ); -}; diff --git a/pkgs/ui/src/components/dashboard/appOverview/index.tsx b/pkgs/ui/src/components/dashboard/appOverview/index.tsx deleted file mode 100644 index a6148b2..0000000 --- a/pkgs/ui/src/components/dashboard/appOverview/index.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { DashboardCard } from "@/components/card"; -import Image from "next/image"; -interface AppCardProps { - name: string; - icon?: string; -} -const AppCard = (props: AppCardProps) => { - const { name, icon } = props; - const iconPath = icon - ? `/app-icons/${icon}` - : "app-icons/app-placeholder.svg"; - return ( -
-
-
- {`${name}-app-icon`} -
-
{name}
-
-
- ); -}; - -const apps = [ - { - name: "Firefox", - icon: "firefox.svg", - }, - { - name: "Discord", - icon: "discord.svg", - }, - { - name: "Docs", - }, - { - name: "Dochub", - icon: "dochub.svg", - }, - { - name: "Chess", - icon: "chess.svg", - }, - { - name: "Games", - icon: "games.svg", - }, - { - name: "Mail", - icon: "mail.svg", - }, - { - name: "Public transport", - icon: "public-transport.svg", - }, - { - name: "Outlook", - icon: "mail.svg", - }, - { - name: "Youtube", - icon: "youtube.svg", - }, -]; - -export const AppOverview = () => { - return ( - -
-
-
- {apps.map((app) => ( - - ))} -
-
-
-
- ); -}; diff --git a/pkgs/ui/src/components/dashboard/notifications/index.tsx b/pkgs/ui/src/components/dashboard/notifications/index.tsx deleted file mode 100644 index dcc8017..0000000 --- a/pkgs/ui/src/components/dashboard/notifications/index.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { DashboardCard } from "@/components/card"; -import { notificationData } from "@/data/dashboardData"; -import { - Avatar, - List, - ListItem, - ListItemAvatar, - ListItemText, -} from "@mui/material"; - -import CheckIcon from "@mui/icons-material/Check"; -import InfoIcon from "@mui/icons-material/Info"; -import PriorityHighIcon from "@mui/icons-material/PriorityHigh"; -import CloseIcon from "@mui/icons-material/Close"; - -const severityMap = { - info: { - icon: , - color: "info", - }, - success: { - icon: , - color: "success", - }, - warning: { - icon: , - color: "warning", - }, - error: { - icon: , - color: "error", - }, -}; - -export const Notifications = () => { - return ( - - - {notificationData.map((n, idx) => ( - - - - {severityMap[n.severity].icon} - - - - - - ))} - - - ); -}; diff --git a/pkgs/ui/src/components/dashboard/quickActions/index.tsx b/pkgs/ui/src/components/dashboard/quickActions/index.tsx deleted file mode 100644 index b303e9a..0000000 --- a/pkgs/ui/src/components/dashboard/quickActions/index.tsx +++ /dev/null @@ -1,64 +0,0 @@ -"use client"; -import { DashboardCard } from "@/components/card"; -import { Fab, Typography } from "@mui/material"; -import { MouseEventHandler, ReactNode } from "react"; - -import AppsIcon from "@mui/icons-material/Apps"; -import DevicesIcon from "@mui/icons-material/Devices"; -import LanIcon from "@mui/icons-material/Lan"; - -type Action = { - id: string; - icon: ReactNode; - label: ReactNode; - eventHandler: MouseEventHandler; -}; - -export const QuickActions = () => { - const actions: Action[] = [ - { - id: "network", - icon: , - label: "Network", - eventHandler: (event) => { - console.log({ event }); - }, - }, - { - id: "apps", - icon: , - label: "Apps", - eventHandler: (event) => { - console.log({ event }); - }, - }, - { - id: "nodes", - icon: , - label: "Devices", - eventHandler: (event) => { - console.log({ event }); - }, - }, - ]; - return ( - -
-
- {actions.map(({ id, icon, label, eventHandler }) => ( - - {icon} - {label} - - ))} -
-
-
- ); -}; diff --git a/pkgs/ui/src/components/dashboard/taskQueue/index.tsx b/pkgs/ui/src/components/dashboard/taskQueue/index.tsx deleted file mode 100644 index eed11c5..0000000 --- a/pkgs/ui/src/components/dashboard/taskQueue/index.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { DashboardCard } from "@/components/card"; - -import SyncIcon from "@mui/icons-material/Sync"; -import ScheduleIcon from "@mui/icons-material/Schedule"; -import DoneIcon from "@mui/icons-material/Done"; -import { ReactNode } from "react"; -import { Chip } from "@mui/material"; - -const statusMap = { - running: , - done: , - planned: , -}; - -interface TaskEntryProps { - status: ReactNode; - result: "default" | "error" | "info" | "success" | "warning"; - task: string; - details?: string; -} -const TaskEntry = (props: TaskEntryProps) => { - const { result, task, status } = props; - return ( - <> -
{status}
-
{task}
-
- -
- - ); -}; - -export const TaskQueue = () => { - return ( - -
- - - -
-
- ); -}; diff --git a/pkgs/ui/src/components/flakeBadge/flakeBadge.tsx b/pkgs/ui/src/components/flakeBadge/flakeBadge.tsx deleted file mode 100644 index 8871de4..0000000 --- a/pkgs/ui/src/components/flakeBadge/flakeBadge.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Chip } from "@mui/material"; - -interface FlakeBadgeProps { - flakeUrl: string; - flakeAttr: string; -} -export const FlakeBadge = (props: FlakeBadgeProps) => ( - -); diff --git a/pkgs/ui/src/components/hooks/useAppContext.tsx b/pkgs/ui/src/components/hooks/useAppContext.tsx index 6f221d6..8317bfc 100644 --- a/pkgs/ui/src/components/hooks/useAppContext.tsx +++ b/pkgs/ui/src/components/hooks/useAppContext.tsx @@ -11,36 +11,28 @@ import React, { import { KeyedMutator } from "swr"; type AppContextType = { - // data: AxiosResponse<{}, any> | undefined; data: AppState; isLoading: boolean; error: AxiosError | undefined; setAppState: Dispatch>; - mutate: KeyedMutator>; - swrKey: string | false | Record; }; -// const initialState = { -// isLoading: true, -// } as const; - export const AppContext = createContext({} as AppContextType); -type AppState = { - isJoined?: boolean; - clanName?: string; -}; +type AppState = {}; interface AppContextProviderProps { children: ReactNode; } export const WithAppState = (props: AppContextProviderProps) => { const { children } = props; - const { isLoading, error, mutate, swrKey } = useListMachines(); - const [data, setAppState] = useState({ isJoined: false }); + const isLoading = false; + const error = undefined; + + const [data, setAppState] = useState({}); return ( { setAppState, isLoading, error, - swrKey, - mutate, }} > {children} diff --git a/pkgs/ui/src/components/hooks/useMachines.tsx b/pkgs/ui/src/components/hooks/useMachines.tsx deleted file mode 100644 index 96fa9db..0000000 --- a/pkgs/ui/src/components/hooks/useMachines.tsx +++ /dev/null @@ -1,95 +0,0 @@ -"use client"; - -import { useListMachines } from "@/api/default/default"; -import { Machine, MachinesResponse } from "@/api/model"; -import { AxiosError, AxiosResponse } from "axios"; -import React, { - Dispatch, - ReactNode, - SetStateAction, - createContext, - useMemo, - useState, -} from "react"; -import { KeyedMutator } from "swr"; - -type Filter = { - name: keyof Machine; - value: Machine[keyof Machine]; -}; -type Filters = Filter[]; - -type MachineContextType = - | { - rawData: AxiosResponse | undefined; - data: Machine[]; - isLoading: boolean; - error: AxiosError | undefined; - isValidating: boolean; - - filters: Filters; - setFilters: Dispatch>; - mutate: KeyedMutator>; - swrKey: string | false | Record; - } - | { - isLoading: true; - data: readonly []; - }; - -const initialState = { - isLoading: true, - data: [], -} as const; - -export const MachineContext = createContext(initialState); - -interface MachineContextProviderProps { - children: ReactNode; -} - -export const MachineContextProvider = (props: MachineContextProviderProps) => { - const { children } = props; - const { - data: rawData, - isLoading, - error, - isValidating, - mutate, - swrKey, - } = useListMachines(); - const [filters, setFilters] = useState([]); - - const data = useMemo(() => { - if (!isLoading && !error && !isValidating && rawData) { - const { machines } = rawData.data; - return machines.filter((m) => - filters.every((f) => m[f.name] === f.value), - ); - } - return []; - }, [isLoading, error, isValidating, rawData, filters]); - - return ( - - {children} - - ); -}; - -export const useMachines = () => React.useContext(MachineContext); diff --git a/pkgs/ui/src/components/hooks/useVms.tsx b/pkgs/ui/src/components/hooks/useVms.tsx deleted file mode 100644 index bccfe77..0000000 --- a/pkgs/ui/src/components/hooks/useVms.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { inspectVm } from "@/api/default/default"; -import { HTTPValidationError, VmConfig } from "@/api/model"; -import { AxiosError } from "axios"; -import { useEffect, useState } from "react"; -import { toast } from "react-hot-toast"; - -interface UseVmsOptions { - url: string; - attr: string; -} -export const useVms = (options: UseVmsOptions) => { - const { url, attr } = options; - const [isLoading, setIsLoading] = useState(true); - const [config, setConfig] = useState(); - const [error, setError] = useState>(); - - useEffect(() => { - const getVmInfo = async (url: string, attr: string) => { - if (url === "" || !url) { - toast.error("Flake url is missing", { id: "missing.flake.url" }); - return undefined; - } - try { - const response = await inspectVm({ - flake_attr: attr, - flake_url: url, - }); - const { - data: { config }, - } = response; - setError(undefined); - return config; - } catch (e) { - const err = e as AxiosError; - setError(err); - toast( - "Could not find default configuration. Please select a machine preset", - ); - return undefined; - } finally { - setIsLoading(false); - } - }; - getVmInfo(url, attr).then((c) => setConfig(c)); - }, [url, attr]); - - return { - error, - isLoading, - config, - }; -}; diff --git a/pkgs/ui/src/components/join/configureVM.tsx b/pkgs/ui/src/components/join/configureVM.tsx deleted file mode 100644 index 630cb5a..0000000 --- a/pkgs/ui/src/components/join/configureVM.tsx +++ /dev/null @@ -1,165 +0,0 @@ -import { - Button, - InputAdornment, - LinearProgress, - ListSubheader, - MenuItem, - Select, - Switch, - TextField, -} from "@mui/material"; -import { Controller, SubmitHandler, UseFormReturn } from "react-hook-form"; -import { FlakeBadge } from "../flakeBadge/flakeBadge"; -import { createVm, useInspectFlakeAttrs } from "@/api/default/default"; -import { VmConfig } from "@/api/model"; -import { Dispatch, SetStateAction, useEffect, useState } from "react"; -import { toast } from "react-hot-toast"; -import { useAppState } from "../hooks/useAppContext"; - -interface VmPropLabelProps { - children: React.ReactNode; -} -const VmPropLabel = (props: VmPropLabelProps) => ( -
- {props.children} -
-); - -interface VmPropContentProps { - children: React.ReactNode; -} -const VmPropContent = (props: VmPropContentProps) => ( -
{props.children}
-); - -interface VmDetailsProps { - formHooks: UseFormReturn; - setVmUuid: Dispatch>; -} - -export const ConfigureVM = (props: VmDetailsProps) => { - const { formHooks, setVmUuid } = props; - const { control, handleSubmit, watch, setValue } = formHooks; - const [isStarting, setStarting] = useState(false); - const { setAppState } = useAppState(); - const { isLoading, data } = useInspectFlakeAttrs({ url: watch("flake_url") }); - - useEffect(() => { - if (!isLoading && data?.data) { - setValue("flake_attr", data.data.flake_attrs[0] || ""); - } - }, [isLoading, setValue, data]); - - const onSubmit: SubmitHandler = async (data) => { - setStarting(true); - console.log(data); - const response = await createVm(data); - const { uuid } = response?.data || null; - - setVmUuid(() => uuid); - setStarting(false); - if (response.statusText === "OK") { - toast.success(("Joined @ " + uuid) as string); - setAppState((s) => ({ ...s, isJoined: true })); - } else { - toast.error("Could not join"); - } - }; - - return ( -
-
- General -
- Flake - - - - Machine - - {!isLoading && ( - ( - - )} - /> - )} - -
- VM -
- CPU Cores - - } - /> - - Graphics - - ( - - )} - /> - - Memory Size - - - ( - MiB - ), - }} - /> - )} - /> - - -
- {isStarting && } - -
-
- ); -}; diff --git a/pkgs/ui/src/components/join/confirm.tsx b/pkgs/ui/src/components/join/confirm.tsx deleted file mode 100644 index 5e8067ff..0000000 --- a/pkgs/ui/src/components/join/confirm.tsx +++ /dev/null @@ -1,63 +0,0 @@ -"use client"; -import { useState } from "react"; -import { LoadingOverlay } from "./loadingOverlay"; -import { FlakeBadge } from "../flakeBadge/flakeBadge"; -import { Typography, Button } from "@mui/material"; -// import { FlakeResponse } from "@/api/model"; -import { ConfirmVM } from "./confirmVM"; -import { Log } from "./log"; -import GppMaybeIcon from "@mui/icons-material/GppMaybe"; -import { useInspectFlake } from "@/api/default/default"; - -interface ConfirmProps { - flakeUrl: string; - flakeAttr: string; - handleBack: () => void; -} -export const Confirm = (props: ConfirmProps) => { - const { flakeUrl, handleBack, flakeAttr } = props; - const [userConfirmed, setUserConfirmed] = useState(false); - - const { data, isLoading } = useInspectFlake({ - url: flakeUrl, - }); - - return userConfirmed ? ( - - ) : ( -
- {isLoading && ( - } - /> - )} - {data && ( - <> - - To join the clan you must trust the Author - - - - - - )} -
- ); -}; diff --git a/pkgs/ui/src/components/join/confirmVM.tsx b/pkgs/ui/src/components/join/confirmVM.tsx deleted file mode 100644 index 8bda0ca..0000000 --- a/pkgs/ui/src/components/join/confirmVM.tsx +++ /dev/null @@ -1,61 +0,0 @@ -"use client"; -import React, { useEffect, useState } from "react"; -import { VmConfig } from "@/api/model"; -import { useVms } from "@/components/hooks/useVms"; - -import { LoadingOverlay } from "./loadingOverlay"; -import { useForm } from "react-hook-form"; -import { ConfigureVM } from "./configureVM"; -import { VmBuildLogs } from "./vmBuildLogs"; - -interface ConfirmVMProps { - url: string; - handleBack: () => void; - defaultFlakeAttr: string; -} - -export function ConfirmVM(props: ConfirmVMProps) { - const { url, defaultFlakeAttr } = props; - const formHooks = useForm({ - defaultValues: { - flake_url: url, - flake_attr: defaultFlakeAttr, - cores: 4, - graphics: true, - memory_size: 2048, - }, - }); - const [vmUuid, setVmUuid] = useState(null); - - const { setValue, watch, formState } = formHooks; - const { config, isLoading } = useVms({ - url, - attr: watch("flake_attr") || defaultFlakeAttr, - }); - - useEffect(() => { - if (config) { - setValue("cores", config?.cores); - setValue("memory_size", config?.memory_size); - setValue("graphics", config?.graphics); - } - }, [config, setValue]); - - return ( -
- {!formState.isSubmitted && ( - <> -
- {isLoading && ( - - )} - - -
- - )} - - {formState.isSubmitted && vmUuid && } -
- ); -} diff --git a/pkgs/ui/src/components/join/layout.tsx b/pkgs/ui/src/components/join/layout.tsx deleted file mode 100644 index 3966109..0000000 --- a/pkgs/ui/src/components/join/layout.tsx +++ /dev/null @@ -1,20 +0,0 @@ -"use client"; -import { Typography } from "@mui/material"; -import { ReactNode } from "react"; - -interface LayoutProps { - children: ReactNode; -} -export const Layout = (props: LayoutProps) => { - return ( -
- - Join{" "} - - Clan.lol - - - {props.children} -
- ); -}; diff --git a/pkgs/ui/src/components/sidebar/index.tsx b/pkgs/ui/src/components/sidebar/index.tsx index 5c95e93..c75bfa2 100644 --- a/pkgs/ui/src/components/sidebar/index.tsx +++ b/pkgs/ui/src/components/sidebar/index.tsx @@ -11,14 +11,9 @@ import Image from "next/image"; import { ReactNode } from "react"; import { tw } from "@/utils/tailwind"; -import AppsIcon from "@mui/icons-material/Apps"; -import BackupIcon from "@mui/icons-material/Backup"; -import DashboardIcon from "@mui/icons-material/Dashboard"; -import DesignServicesIcon from "@mui/icons-material/DesignServices"; -import DevicesIcon from "@mui/icons-material/Devices"; -import LanIcon from "@mui/icons-material/Lan"; +import AssignmentIndIcon from "@mui/icons-material/AssignmentInd"; import Link from "next/link"; - +import WysiwygIcon from "@mui/icons-material/Wysiwyg"; import ChevronLeftIcon from "@mui/icons-material/ChevronLeft"; type MenuEntry = { @@ -32,39 +27,15 @@ type MenuEntry = { const menuEntries: MenuEntry[] = [ { - icon: , - label: "Dashoard", + icon: , + label: "Freelance", to: "/", disabled: false, }, { - icon: , - label: "Machines", - to: "/machines", - disabled: false, - }, - { - icon: , - label: "Applications", - to: "/applications", - disabled: true, - }, - { - icon: , - label: "Network", - to: "/network", - disabled: true, - }, - { - icon: , - label: "Templates", - to: "/templates", - disabled: false, - }, - { - icon: , - label: "Backups", - to: "/backups", + icon: , + label: "Blog", + to: "/blog", disabled: true, }, ]; @@ -138,23 +109,6 @@ export function Sidebar(props: SidebarProps) { ); })} - -
-

- Clan.lol Admin -

- - Donate - -
); diff --git a/pkgs/ui/src/components/table/enhancedTableToolbar.tsx b/pkgs/ui/src/components/table/enhancedTableToolbar.tsx deleted file mode 100644 index 2210f8f..0000000 --- a/pkgs/ui/src/components/table/enhancedTableToolbar.tsx +++ /dev/null @@ -1,82 +0,0 @@ -"use client"; - -import React, { useMemo } from "react"; -import Box from "@mui/material/Box"; -import Grid2 from "@mui/material/Unstable_Grid2"; -import useMediaQuery from "@mui/material/useMediaQuery"; -import { useTheme } from "@mui/material"; - -import { PieCards } from "./pieCards"; -import { PieData, NodePieChart } from "./nodePieChart"; -import { Machine } from "@/api/model/machine"; -import { Status } from "@/api/model"; - -interface EnhancedTableToolbarProps { - tableData: readonly Machine[]; -} - -export function EnhancedTableToolbar( - props: React.PropsWithChildren, -) { - const { tableData } = props; - const theme = useTheme(); - const is_lg = useMediaQuery(theme.breakpoints.down("lg")); - - const pieData: PieData[] = useMemo(() => { - const online = tableData.filter( - (row) => row.status === Status.online, - ).length; - const offline = tableData.filter( - (row) => row.status === Status.offline, - ).length; - const pending = tableData.filter( - (row) => row.status === Status.unknown, - ).length; - - return [ - { name: "Online", value: online, color: theme.palette.success.main }, - { name: "Offline", value: offline, color: theme.palette.error.main }, - { name: "Pending", value: pending, color: theme.palette.warning.main }, - ]; - }, [tableData, theme]); - - return ( - - {/* Pie Chart Grid */} - - - - - - - {/* Card Stack Grid */} - - - - - {/*Toolbar Grid */} - - {props.children} - - - ); -} diff --git a/pkgs/ui/src/components/table/index.tsx b/pkgs/ui/src/components/table/index.tsx deleted file mode 100644 index 59710ae..0000000 --- a/pkgs/ui/src/components/table/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { NodeTable } from "./nodeTable"; diff --git a/pkgs/ui/src/components/table/nodePieChart.tsx b/pkgs/ui/src/components/table/nodePieChart.tsx deleted file mode 100644 index 9555f34..0000000 --- a/pkgs/ui/src/components/table/nodePieChart.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import React from "react"; -import { PieChart, Pie, Cell, ResponsiveContainer, Legend } from "recharts"; -import { useTheme } from "@mui/material/styles"; -import { Box } from "@mui/material"; - -export interface PieData { - name: string; - value: number; - color: string; -} - -interface Props { - data: PieData[]; - showLabels?: boolean; -} - -export function NodePieChart(props: Props) { - const theme = useTheme(); - const { data, showLabels } = props; - - return ( - - - - - {data.map((entry, index) => ( - - ))} - - - - - - ); -} diff --git a/pkgs/ui/src/components/table/nodeRow.tsx b/pkgs/ui/src/components/table/nodeRow.tsx deleted file mode 100644 index ef1ae43..0000000 --- a/pkgs/ui/src/components/table/nodeRow.tsx +++ /dev/null @@ -1,135 +0,0 @@ -"use client"; - -import * as React from "react"; -import Box from "@mui/material/Box"; -import TableCell from "@mui/material/TableCell"; -import TableRow from "@mui/material/TableRow"; -import Typography from "@mui/material/Typography"; -import IconButton from "@mui/material/IconButton"; -import CircleIcon from "@mui/icons-material/Circle"; -import Stack from "@mui/material/Stack/Stack"; -import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; -import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; -import Grid2 from "@mui/material/Unstable_Grid2"; // Grid version 2 -import { Collapse } from "@mui/material"; -import { Machine, Status } from "@/api/model"; - -function renderStatus(status: Status) { - switch (status) { - case Status.online: - return ( - - - - Online - - - ); - - case Status.offline: - return ( - - - - Offline - - - ); - case Status.unknown: - return ( - - - - Pending - - - ); - } -} -export function NodeRow(props: { - row: Machine; - selected: string | undefined; - setSelected: (a: string | undefined) => void; -}) { - const { row, selected, setSelected } = props; - const [open, setOpen] = React.useState(false); - - // Speed optimization. We compare string pointers here instead of the string content. - const isSelected = selected == row.name; - - const handleClick = (event: React.MouseEvent, name: string) => { - if (isSelected) { - setSelected(undefined); - } else { - setSelected(name); - } - }; - - return ( - - {/* Rendered Row */} - - - setOpen(!open)} - > - {open ? : } - - - handleClick(event, row.name)} - > - - - {row.name} - - - - handleClick(event, row.name)} - > - {renderStatus(row.status)} - - - - {/* Row Expansion */} - - - - - - Metadata - - - - Hello1 - - - Hello2 - - - - - - - - ); -} diff --git a/pkgs/ui/src/components/table/nodeTable.tsx b/pkgs/ui/src/components/table/nodeTable.tsx deleted file mode 100644 index be7568a..0000000 --- a/pkgs/ui/src/components/table/nodeTable.tsx +++ /dev/null @@ -1,96 +0,0 @@ -"use client"; - -import { CircularProgress, Grid, useTheme } from "@mui/material"; -import Box from "@mui/material/Box"; -import Paper from "@mui/material/Paper"; -import TablePagination from "@mui/material/TablePagination"; -import useMediaQuery from "@mui/material/useMediaQuery"; -import { ChangeEvent, useMemo, useState } from "react"; - -import { Machine } from "@/api/model/machine"; -import Grid2 from "@mui/material/Unstable_Grid2/Grid2"; -import { useMachines } from "../hooks/useMachines"; -import { EnhancedTableToolbar } from "./enhancedTableToolbar"; -import { NodeTableContainer } from "./nodeTableContainer"; -import { SearchBar } from "./searchBar"; -import { StickySpeedDial } from "./stickySpeedDial"; - -export function NodeTable() { - const machines = useMachines(); - const theme = useTheme(); - const is_xs = useMediaQuery(theme.breakpoints.only("xs")); - - const [selected, setSelected] = useState(undefined); - const [page, setPage] = useState(0); - const [rowsPerPage, setRowsPerPage] = useState(5); - const [filteredList, setFilteredList] = useState([]); - - const tableData = useMemo(() => { - const tableData = machines.data.map((machine) => { - return { name: machine.name, status: machine.status }; - }); - setFilteredList(tableData); - return tableData; - }, [machines.data]); - - const handleChangePage = (event: unknown, newPage: number) => { - setPage(newPage); - }; - - const handleChangeRowsPerPage = (event: ChangeEvent) => { - setRowsPerPage(parseInt(event.target.value, 10)); - setPage(0); - }; - - if (machines.isLoading) { - return ( - - - - ); - } - - return ( - - - - - - - - - - - - {/* TODO: This creates the error Warning: Prop `id` did not match. Server: ":RspmmcqH1:" Client: ":R3j6qpj9H1:" */} - - - - ); -} diff --git a/pkgs/ui/src/components/table/nodeTableContainer.tsx b/pkgs/ui/src/components/table/nodeTableContainer.tsx deleted file mode 100644 index 59bc2b6..0000000 --- a/pkgs/ui/src/components/table/nodeTableContainer.tsx +++ /dev/null @@ -1,197 +0,0 @@ -"use client"; - -import * as React from "react"; -import Box from "@mui/material/Box"; -import Table from "@mui/material/Table"; -import TableBody from "@mui/material/TableBody"; -import TableCell from "@mui/material/TableCell"; -import TableContainer from "@mui/material/TableContainer"; -import TableHead from "@mui/material/TableHead"; -import TableRow from "@mui/material/TableRow"; -import TableSortLabel from "@mui/material/TableSortLabel"; -import { visuallyHidden } from "@mui/utils"; -import { NodeRow } from "./nodeRow"; - -import { Machine } from "@/api/model/machine"; - -interface HeadCell { - disablePadding: boolean; - id: keyof Machine; - label: string; - alignRight: boolean; -} - -const headCells: readonly HeadCell[] = [ - { - id: "name", - alignRight: false, - disablePadding: false, - label: "DOMAIN NAME", - }, - { - id: "status", - alignRight: false, - disablePadding: false, - label: "STATUS", - }, -]; - -function descendingComparator(a: T, b: T, orderBy: keyof T) { - if (b[orderBy] < a[orderBy]) { - return -1; - } - if (b[orderBy] > a[orderBy]) { - return 1; - } - return 0; -} - -export type NodeOrder = "asc" | "desc"; - -function getComparator( - order: NodeOrder, - orderBy: Key, -): ( - a: { [key in Key]: number | string | boolean }, - b: { [key in Key]: number | string | boolean }, -) => number { - return order === "desc" - ? (a, b) => descendingComparator(a, b, orderBy) - : (a, b) => -descendingComparator(a, b, orderBy); -} - -// Since 2020 all major browsers ensure sort stability with Array.prototype.sort(). -// stableSort() brings sort stability to non-modern browsers (notably IE11). If you -// only support modern browsers you can replace stableSort(exampleArray, exampleComparator) -// with exampleArray.slice().sort(exampleComparator) -function stableSort( - array: readonly T[], - comparator: (a: T, b: T) => number, -) { - const stabilizedThis = array.map((el, index) => [el, index] as [T, number]); - stabilizedThis.sort((a, b) => { - const order = comparator(a[0], b[0]); - if (order !== 0) { - return order; - } - return a[1] - b[1]; - }); - return stabilizedThis.map((el) => el[0]); -} - -interface EnhancedTableProps { - onRequestSort: ( - event: React.MouseEvent, - property: keyof Machine, - ) => void; - order: NodeOrder; - orderBy: string; - rowCount: number; -} - -function EnhancedTableHead(props: EnhancedTableProps) { - const { order, orderBy, onRequestSort } = props; - const createSortHandler = - (property: keyof Machine) => (event: React.MouseEvent) => { - onRequestSort(event, property); - }; - - return ( - - - - {headCells.map((headCell) => ( - - - {headCell.label} - {orderBy === headCell.id ? ( - - {order === "desc" ? "sorted descending" : "sorted ascending"} - - ) : null} - - - ))} - - - ); -} - -interface NodeTableContainerProps { - tableData: readonly Machine[]; - page: number; - rowsPerPage: number; - dense: boolean; - selected: string | undefined; - setSelected: React.Dispatch>; -} - -export function NodeTableContainer(props: NodeTableContainerProps) { - const { tableData, page, rowsPerPage, dense, selected, setSelected } = props; - const [order, setOrder] = React.useState("asc"); - const [orderBy, setOrderBy] = React.useState("status"); - - // Avoid a layout jump when reaching the last page with empty rows. - const emptyRows = - page > 0 ? Math.max(0, (1 + page) * rowsPerPage - tableData.length) : 0; - - const handleRequestSort = ( - event: React.MouseEvent, - property: keyof Machine, - ) => { - const isAsc = orderBy === property && order === "asc"; - setOrder(isAsc ? "desc" : "asc"); - setOrderBy(property); - }; - - const visibleRows = React.useMemo( - () => - stableSort(tableData, getComparator(order, orderBy)).slice( - page * rowsPerPage, - page * rowsPerPage + rowsPerPage, - ), - [order, orderBy, page, rowsPerPage, tableData], - ); - return ( - - - - - {visibleRows.map((row, index) => { - return ( - - ); - })} - {emptyRows > 0 && ( - - - - )} - -
-
- ); -} diff --git a/pkgs/ui/src/components/table/pieCards.tsx b/pkgs/ui/src/components/table/pieCards.tsx deleted file mode 100644 index 3dd05e1..0000000 --- a/pkgs/ui/src/components/table/pieCards.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { Card, CardContent, Stack, Typography } from "@mui/material"; -import hexRgb from "hex-rgb"; -import { useMemo } from "react"; - -interface PieData { - name: string; - value: number; - color: string; -} - -interface PieCardsProps { - pieData: PieData[]; -} - -export function PieCards(props: PieCardsProps) { - const { pieData } = props; - - const cardData = useMemo(() => { - return pieData - .filter((pieItem) => pieItem.value > 0) - .concat({ - name: "Total", - value: pieData.reduce((a, b) => a + b.value, 0), - color: "#000000", - }); - }, [pieData]); - - return ( - - {cardData.map((pieItem) => ( - - - - {pieItem.value} - - - {pieItem.name} - - - - ))} - - ); -} diff --git a/pkgs/ui/src/components/table/searchBar.tsx b/pkgs/ui/src/components/table/searchBar.tsx deleted file mode 100644 index 634b629..0000000 --- a/pkgs/ui/src/components/table/searchBar.tsx +++ /dev/null @@ -1,99 +0,0 @@ -"use client"; - -import { SetStateAction, Dispatch, useState, useEffect, useMemo } from "react"; -import IconButton from "@mui/material/IconButton"; -import SearchIcon from "@mui/icons-material/Search"; -import { useDebounce } from "../hooks/useDebounce"; -import { Autocomplete, InputAdornment, TextField } from "@mui/material"; -import { Machine } from "@/api/model/machine"; - -export interface SearchBarProps { - tableData: readonly Machine[]; - setFilteredList: Dispatch>; -} - -export function SearchBar(props: SearchBarProps) { - let { tableData, setFilteredList } = props; - const [search, setSearch] = useState(""); - const debouncedSearch = useDebounce(search, 250); - const [open, setOpen] = useState(false); - - // Define a function to handle the Esc key press - function handleEsc(event: React.KeyboardEvent) { - if (event.key === "Escape") { - setSearch(""); - setFilteredList(tableData); - } - - // check if the key is Enter - if (event.key === "Enter") { - setOpen(false); - } - } - - useEffect(() => { - if (debouncedSearch) { - const filtered: Machine[] = tableData.filter((row) => { - return row.name.toLowerCase().includes(debouncedSearch.toLowerCase()); - }); - setFilteredList(filtered); - } - }, [debouncedSearch, tableData, setFilteredList]); - - const handleInputChange = (event: any, value: string) => { - if (value === "") { - setFilteredList(tableData); - } - - setSearch(value); - }; - - const suggestions = useMemo( - () => tableData.map((row) => row.name), - [tableData], - ); - - return ( - { - return ( -
  • - {option} -
  • - ); - }} - onKeyDown={handleEsc} - onInputChange={handleInputChange} - value={search} - open={open} - onOpen={() => { - setOpen(true); - }} - onClose={() => { - setOpen(false); - }} - renderInput={(params) => ( - - - - - - ), - }} - > - )} - /> - ); -} diff --git a/pkgs/ui/src/components/table/stickySpeedDial.tsx b/pkgs/ui/src/components/table/stickySpeedDial.tsx deleted file mode 100644 index 44a6542..0000000 --- a/pkgs/ui/src/components/table/stickySpeedDial.tsx +++ /dev/null @@ -1,85 +0,0 @@ -"use client"; - -import * as React from "react"; -import Box from "@mui/material/Box"; -import DeleteIcon from "@mui/icons-material/Delete"; -import SpeedDial, { CloseReason, OpenReason } from "@mui/material/SpeedDial"; -import SpeedDialIcon from "@mui/material/SpeedDialIcon"; -import SpeedDialAction from "@mui/material/SpeedDialAction"; -import EditIcon from "@mui/icons-material/ModeEdit"; -import AddIcon from "@mui/icons-material/Add"; -import Link from "next/link"; - -export function StickySpeedDial(props: { selected: string | undefined }) { - const { selected } = props; - const [open, setOpen] = React.useState(false); - - function handleClose(event: any, reason: CloseReason) { - if (reason === "toggle" || reason === "escapeKeyDown") { - setOpen(false); - } - } - - function handleOpen(event: any, reason: OpenReason) { - if (reason === "toggle") { - setOpen(true); - } - } - - const isSomethingSelected = selected != undefined; - - function editDial() { - if (isSomethingSelected) { - return ( - - - - ); - } else { - return ; - } - } - - return ( - - } - direction="down" - onClose={handleClose} - onOpen={handleOpen} - open={open} - > - - - - } - tooltipTitle="Add" - /> - - - } - tooltipTitle="Delete" - /> - - - - ); -} diff --git a/pkgs/ui/src/data/_schema.ts b/pkgs/ui/src/data/_schema.ts deleted file mode 100644 index 7a3b893..0000000 --- a/pkgs/ui/src/data/_schema.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { RJSFSchema } from "@rjsf/utils"; -export const schema: RJSFSchema = { - properties: { - bloatware: { - properties: { - age: { - default: 42, - description: "The age of the user", - type: "integer", - }, - isAdmin: { - default: false, - description: "Is the user an admin?", - type: "boolean", - }, - kernelModules: { - default: ["nvme", "xhci_pci", "ahci"], - description: "A list of enabled kernel modules", - items: { - type: "string", - }, - type: "array", - }, - name: { - default: "John Doe", - description: "The name of the user", - type: "string", - }, - services: { - properties: { - opt: { - default: "foo", - description: "A submodule option", - type: "string", - }, - }, - type: "object", - }, - userIds: { - additionalProperties: { - type: "integer", - }, - default: { - albrecht: 3, - horst: 1, - peter: 2, - }, - description: "Some attributes", - type: "object", - }, - }, - type: "object", - }, - networking: { - properties: { - zerotier: { - properties: { - controller: { - properties: { - enable: { - default: false, - description: - "Whether to enable turn this machine into the networkcontroller.", - type: "boolean", - }, - public: { - default: false, - description: - "everyone can join a public network without having the administrator to accept\n", - type: "boolean", - }, - }, - type: "object", - }, - networkId: { - description: "zerotier networking id\n", - type: "string", - }, - }, - required: ["networkId"], - type: "object", - }, - }, - type: "object", - }, - }, - type: "object", -}; diff --git a/pkgs/ui/src/data/_schema2.ts b/pkgs/ui/src/data/_schema2.ts deleted file mode 100644 index 5c311da..0000000 --- a/pkgs/ui/src/data/_schema2.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { RJSFSchema } from "@rjsf/utils"; -export const schema: RJSFSchema = { - type: "object", - properties: { - name: { - type: "string", - default: "John-nixi", - description: "The name of the machine", - }, - age: { - type: "integer", - default: 42, - description: "The age of the user", - maximum: 40, - }, - role: { - enum: ["New York", "Amsterdam", "Hong Kong"], - description: "Role of the user", - }, - kernelModules: { - type: "array", - items: { - type: "string", - }, - default: ["nvme", "xhci_pci", "ahci"], - description: "A list of enabled kernel modules", - }, - userIds: { - type: "array", - items: { - type: "object", - properties: { - user: { - type: "string", - }, - id: { - type: "integer", - }, - }, - }, - default: [ - { - user: "John", - id: 12, - }, - ], - description: "Some attributes", - }, - xdg: { - type: "object", - properties: { - portal: { - type: "object", - properties: { - xdgOpenUsePortal: { - type: "boolean", - default: false, - }, - enable: { - type: "boolean", - default: false, - }, - lxqt: { - type: "object", - properties: { - enable: { - type: "boolean", - default: false, - }, - styles: { - type: "array", - items: { - type: "string", - }, - }, - }, - }, - extraPortals: { - type: "array", - items: { - type: "string", - }, - }, - wlr: { - type: "object", - properties: { - enable: { - type: "boolean", - default: false, - }, - settings: { - type: "object", - default: { - screencast: { - output_name: "HDMI-A-1", - max_fps: 30, - exec_before: "disable_notifications.sh", - exec_after: "enable_notifications.sh", - chooser_type: "simple", - chooser_cmd: "${pkgs.slurp}/bin/slurp -f %o -or", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, -}; diff --git a/pkgs/ui/src/data/dashboardData.tsx b/pkgs/ui/src/data/dashboardData.tsx deleted file mode 100644 index 2c251d7..0000000 --- a/pkgs/ui/src/data/dashboardData.tsx +++ /dev/null @@ -1,170 +0,0 @@ -export const status = { - online: "online", - offline: "offline", - pending: "pending", -} as const; -// Convert object keys in a union type -export type Status = (typeof status)[keyof typeof status]; - -export type Network = { - name: string; - id: string; -}; - -export type ClanDevice = { - id: string; - name: string; - status: Status; - ipv6: string; - networks: Network[]; -}; - -export type ClanStatus = { - self: ClanDevice; - other: ClanDevice[]; -}; -export const clanStatus: ClanStatus = { - self: { - id: "1", - name: "My Computer", - ipv6: "", - status: "online", - networks: [ - { - name: "Family", - id: "1", - }, - { - name: "Fight-Club", - id: "1", - }, - ], - }, - // other: [], - other: [ - { - id: "2", - name: "Daddies Computer", - status: "online", - networks: [ - { - name: "Family", - id: "1", - }, - ], - ipv6: "", - }, - { - id: "3", - name: "Lars Notebook", - status: "offline", - networks: [ - { - name: "Family", - id: "1", - }, - ], - ipv6: "", - }, - { - id: "4", - name: "Cassie Computer", - status: "pending", - networks: [ - { - name: "Family", - id: "1", - }, - { - name: "Fight-Club", - id: "2", - }, - ], - ipv6: "", - }, - { - id: "5", - name: "Chuck Norris Computer", - status: "online", - networks: [ - { - name: "Fight-Club", - id: "2", - }, - ], - ipv6: "", - }, - { - id: "6", - name: "Ella Bright", - status: "pending", - networks: [ - { - name: "Fight-Club", - id: "2", - }, - ], - ipv6: "", - }, - { - id: "7", - name: "Ryan Flabberghast", - status: "offline", - networks: [ - { - name: "Fight-Club", - id: "2", - }, - ], - ipv6: "", - }, - ], -}; - -export const severity = { - info: "info", - success: "success", - warning: "warning", - error: "error", -} as const; -// Convert object keys in a union type -export type Severity = (typeof severity)[keyof typeof severity]; - -export type Notification = { - id: string; - msg: string; - source: string; - date: string; - severity: Severity; -}; - -export const notificationData: Notification[] = [ - { - id: "1", - date: "2022-12-27 08:26:49.219717", - severity: "success", - msg: "Defeated zombie mob flawless", - source: "Chuck Norris Computer", - }, - { - id: "2", - date: "2022-12-27 08:26:49.219717", - severity: "error", - msg: "Application Crashed: my little pony", - source: "Cassie Computer", - }, - { - id: "3", - date: "2022-12-27 08:26:49.219717", - severity: "warning", - msg: "Security update necessary", - source: "Daddies Computer", - }, - { - id: "4", - date: "2022-12-27 08:26:49.219717", - severity: "info", - msg: "Decompressed snowflakes", - source: "My Computer", - }, -]; diff --git a/pkgs/ui/src/data/nodeData.tsx b/pkgs/ui/src/data/nodeData.tsx deleted file mode 100644 index 729f338..0000000 --- a/pkgs/ui/src/data/nodeData.tsx +++ /dev/null @@ -1,178 +0,0 @@ -export interface TableData { - name: string; - status: NodeStatusKeys; - last_seen: number; -} - -export const NodeStatus = { - Online: "Online", - Offline: "Offline", - Pending: "Pending", -}; - -export type NodeStatusKeys = (typeof NodeStatus)[keyof typeof NodeStatus]; - -function createData( - name: string, - status: NodeStatusKeys, - last_seen: number, -): TableData { - if (status == NodeStatus.Online) { - last_seen = 0; - } - - return { - name, - status, - last_seen: last_seen, - }; -} - -var nameNumber = 0; - -// A function to generate random names -function getRandomName(): string { - let names = [ - "Alice", - "Bob", - "Charlie", - "David", - "Eve", - "Frank", - "Grace", - "Heidi", - "Ivan", - "Judy", - "Mallory", - "Oscar", - "Peggy", - "Sybil", - "Trent", - "Victor", - "Walter", - "Wendy", - "Zoe", - ]; - let index = Math.floor(Math.random() * names.length); - return names[index] + nameNumber++; -} - -// A function to generate random IPv6 addresses -// function getRandomId(): string { -// let hex = "0123456789abcdef"; -// let id = ""; -// for (let i = 0; i < 8; i++) { -// for (let j = 0; j < 4; j++) { -// let index = Math.floor(Math.random() * hex.length); -// id += hex[index]; -// } -// if (i < 7) { -// id += ":"; -// } -// } -// return id; -// } - -// A function to generate random status keys -function getRandomStatus(): NodeStatusKeys { - let statusKeys = [NodeStatus.Online, NodeStatus.Offline, NodeStatus.Pending]; - let index = Math.floor(Math.random() * statusKeys.length); - return statusKeys[index]; -} - -// A function to generate random last seen values -function getRandomLastSeen(status: NodeStatusKeys): number { - if (status === "online") { - return 0; - } else { - let min = 1; // One day ago - let max = 360; // One year ago - return Math.floor(Math.random() * (max - min + 1) + min); - } -} - -export const tableData = [ - createData( - "Matchbox", - - NodeStatus.Pending, - 0, - ), - createData( - "Ahorn", - - NodeStatus.Online, - 0, - ), - createData( - "Yellow", - - NodeStatus.Offline, - 16.0, - ), - createData( - "Rauter", - - NodeStatus.Offline, - 6.0, - ), - createData( - "Porree", - - NodeStatus.Offline, - 13, - ), - createData( - "Helsinki", - - NodeStatus.Online, - 0, - ), - createData( - "Kelle", - - NodeStatus.Online, - 0, - ), - createData( - "Shodan", - - NodeStatus.Online, - 0.0, - ), - createData( - "Qubasa", - - NodeStatus.Offline, - 7.0, - ), - createData( - "Green", - - NodeStatus.Offline, - 2, - ), - createData("Gum", NodeStatus.Offline, 0), - createData("Xu", NodeStatus.Online, 0), - createData( - "Zaatar", - - NodeStatus.Online, - 0, - ), -]; - -// A function to execute the createData function with dummy data in a loop 100 times and return an array -export function executeCreateData(): TableData[] { - let result: TableData[] = []; - for (let i = 0; i < 100; i++) { - // Generate dummy data - let name = getRandomName(); - let status = getRandomStatus(); - let last_seen = getRandomLastSeen(status); - - // Call the createData function and push the result to the array - result.push(createData(name, status, last_seen)); - } - return result; -} diff --git a/pkgs/ui/src/data/nodeDataStatic.tsx b/pkgs/ui/src/data/nodeDataStatic.tsx deleted file mode 100644 index 5840689..0000000 --- a/pkgs/ui/src/data/nodeDataStatic.tsx +++ /dev/null @@ -1,602 +0,0 @@ -export const tableData = [ - { - name: "Wendy200", - - status: "Pending", - last_seen: 115, - }, - { - name: "Charlie201", - - status: "Pending", - last_seen: 320, - }, - { - name: "Bob202", - - status: "Offline", - last_seen: 347, - }, - { - name: "Sybil203", - - status: "Online", - last_seen: 0, - }, - { - name: "Trent204", - - status: "Online", - last_seen: 0, - }, - { - name: "Eve205", - - status: "Online", - last_seen: 0, - }, - { - name: "Alice206", - - status: "Offline", - last_seen: 256, - }, - { - name: "Frank207", - - status: "Offline", - last_seen: 248, - }, - { - name: "Eve208", - - status: "Pending", - last_seen: 234, - }, - { - name: "Bob209", - - status: "Pending", - last_seen: 178, - }, - { - name: "Peggy210", - - status: "Offline", - last_seen: 256, - }, - { - name: "Heidi211", - - status: "Online", - last_seen: 0, - }, - { - name: "Charlie212", - - status: "Pending", - last_seen: 15, - }, - { - name: "Victor213", - - status: "Pending", - last_seen: 171, - }, - { - name: "Heidi214", - - status: "Pending", - last_seen: 287, - }, - { - name: "Walter215", - - status: "Online", - last_seen: 0, - }, - { - name: "Alice216", - - status: "Online", - last_seen: 0, - }, - { - name: "Zoe217", - - status: "Online", - last_seen: 0, - }, - { - name: "Judy218", - - status: "Pending", - last_seen: 184, - }, - { - name: "Mallory219", - - status: "Online", - last_seen: 0, - }, - { - name: "Judy220", - - status: "Offline", - last_seen: 63, - }, - { - name: "Wendy221", - - status: "Offline", - last_seen: 181, - }, - { - name: "Bob222", - - status: "Offline", - last_seen: 300, - }, - { - name: "Eve223", - - status: "Pending", - last_seen: 60, - }, - { - name: "Judy224", - - status: "Pending", - last_seen: 218, - }, - { - name: "Peggy225", - - status: "Offline", - last_seen: 140, - }, - { - name: "Frank226", - - status: "Offline", - last_seen: 106, - }, - { - name: "Ivan227", - - status: "Offline", - last_seen: 296, - }, - { - name: "Charlie228", - - status: "Pending", - last_seen: 81, - }, - { - name: "Victor229", - - status: "Pending", - last_seen: 46, - }, - { - name: "Mallory230", - - status: "Online", - last_seen: 0, - }, - { - name: "Wendy231", - - status: "Offline", - last_seen: 205, - }, - { - name: "David232", - - status: "Pending", - last_seen: 11, - }, - { - name: "Eve233", - - status: "Offline", - last_seen: 346, - }, - { - name: "David234", - - status: "Online", - last_seen: 0, - }, - { - name: "Ivan235", - - status: "Pending", - last_seen: 291, - }, - { - name: "Mallory236", - - status: "Offline", - last_seen: 321, - }, - { - name: "David237", - - status: "Online", - last_seen: 0, - }, - { - name: "Victor238", - - status: "Online", - last_seen: 0, - }, - { - name: "Judy239", - - status: "Offline", - last_seen: 3, - }, - { - name: "Trent240", - - status: "Online", - last_seen: 0, - }, - { - name: "Ivan241", - - status: "Pending", - last_seen: 38, - }, - { - name: "Mallory242", - - status: "Online", - last_seen: 0, - }, - { - name: "Charlie243", - - status: "Offline", - last_seen: 288, - }, - { - name: "Ivan244", - - status: "Offline", - last_seen: 225, - }, - { - name: "Zoe245", - - status: "Pending", - last_seen: 56, - }, - { - name: "Trent246", - - status: "Pending", - last_seen: 111, - }, - { - name: "Sybil247", - - status: "Online", - last_seen: 0, - }, - { - name: "Wendy248", - - status: "Offline", - last_seen: 299, - }, - { - name: "David249", - - status: "Pending", - last_seen: 303, - }, - { - name: "Trent250", - - status: "Offline", - last_seen: 69, - }, - { - name: "Eve251", - - status: "Pending", - last_seen: 354, - }, - { - name: "Oscar252", - - status: "Offline", - last_seen: 54, - }, - { - name: "Sybil253", - - status: "Online", - last_seen: 0, - }, - { - name: "Zoe254", - - status: "Offline", - last_seen: 118, - }, - { - name: "Bob255", - - status: "Pending", - last_seen: 112, - }, - { - name: "Alice256", - - status: "Online", - last_seen: 0, - }, - { - name: "Eve257", - - status: "Pending", - last_seen: 97, - }, - { - name: "Peggy258", - - status: "Online", - last_seen: 0, - }, - { - name: "Ivan259", - - status: "Pending", - last_seen: 99, - }, - { - name: "Victor260", - - status: "Offline", - last_seen: 231, - }, - { - name: "Grace261", - - status: "Offline", - last_seen: 199, - }, - { - name: "Heidi262", - - status: "Online", - last_seen: 0, - }, - { - name: "Sybil263", - - status: "Pending", - last_seen: 89, - }, - { - name: "Alice264", - - status: "Pending", - last_seen: 354, - }, - { - name: "Zoe265", - - status: "Pending", - last_seen: 12, - }, - { - name: "Victor266", - - status: "Pending", - last_seen: 24, - }, - { - name: "Ivan267", - - status: "Pending", - last_seen: 238, - }, - { - name: "Peggy268", - - status: "Offline", - last_seen: 113, - }, - { - name: "Oscar269", - - status: "Online", - last_seen: 0, - }, - { - name: "Alice270", - - status: "Online", - last_seen: 0, - }, - { - name: "Eve271", - - status: "Online", - last_seen: 0, - }, - { - name: "Mallory272", - - status: "Pending", - last_seen: 180, - }, - { - name: "David273", - - status: "Online", - last_seen: 0, - }, - { - name: "Eve274", - - status: "Pending", - last_seen: 164, - }, - { - name: "Walter275", - - status: "Online", - last_seen: 0, - }, - { - name: "Eve276", - - status: "Offline", - last_seen: 123, - }, - { - name: "Wendy277", - - status: "Offline", - last_seen: 211, - }, - { - name: "Charlie278", - - status: "Pending", - last_seen: 178, - }, - { - name: "Eve279", - - status: "Online", - last_seen: 0, - }, - { - name: "Zoe280", - - status: "Online", - last_seen: 0, - }, - { - name: "Mallory281", - - status: "Pending", - last_seen: 143, - }, - { - name: "Bob282", - - status: "Online", - last_seen: 0, - }, - { - name: "Judy283", - - status: "Online", - last_seen: 0, - }, - { - name: "Grace284", - - status: "Online", - last_seen: 0, - }, - { - name: "Zoe285", - - status: "Online", - last_seen: 0, - }, - { - name: "Grace286", - - status: "Pending", - last_seen: 92, - }, - { - name: "Walter287", - - status: "Online", - last_seen: 0, - }, - { - name: "Walter288", - - status: "Pending", - last_seen: 248, - }, - { - name: "David289", - - status: "Pending", - last_seen: 301, - }, - { - name: "Peggy290", - - status: "Online", - last_seen: 0, - }, - { - name: "Sybil291", - - status: "Pending", - last_seen: 114, - }, - { - name: "Heidi292", - - status: "Online", - last_seen: 0, - }, - { - name: "David293", - - status: "Offline", - last_seen: 165, - }, - { - name: "Judy294", - - status: "Pending", - last_seen: 52, - }, - { - name: "Trent295", - - status: "Online", - last_seen: 0, - }, - { - name: "Heidi296", - - status: "Pending", - last_seen: 129, - }, - { - name: "Trent297", - - status: "Pending", - last_seen: 108, - }, - { - name: "David298", - - status: "Online", - last_seen: 0, - }, - { - name: "Sybil299", - - status: "Online", - last_seen: 0, - }, -];