Befor fixing linting problem
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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])
|
||||
|
||||
66
pkgs/clan-cli/clan_cli/debug.py
Normal file
66
pkgs/clan-cli/clan_cli/debug.py
Normal file
@@ -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)
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
from typing import NewType
|
||||
|
||||
FlakeName = NewType("FlakeName", str)
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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__)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
|
||||
23
pkgs/clan-cli/clan_cli/types.py
Normal file
23
pkgs/clan-cli/clan_cli/types.py
Normal file
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 ]; } ''
|
||||
|
||||
@@ -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" ]
|
||||
|
||||
|
||||
@@ -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
|
||||
'';
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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,
|
||||
]
|
||||
)
|
||||
|
||||
@@ -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__";
|
||||
|
||||
@@ -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__";
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
let
|
||||
clan = clan-core.lib.buildClan {
|
||||
directory = self;
|
||||
clanName = "core_dynamic_machine_clan";
|
||||
machines =
|
||||
let
|
||||
machineModules = builtins.readDir (self + "/machines");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 == ""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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/*"]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
@@ -1,5 +0,0 @@
|
||||
import JoinPrequel from "@/views/joinPrequel";
|
||||
|
||||
export default function Page() {
|
||||
return <JoinPrequel />;
|
||||
}
|
||||
@@ -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({
|
||||
<AppContext.Consumer>
|
||||
{(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 && (
|
||||
<MenuIcon />
|
||||
)}
|
||||
{!showSidebar && <MenuIcon />}
|
||||
</IconButton>
|
||||
</div>
|
||||
<div className="col-span-1 block w-full bg-fixed text-center font-semibold dark:invert lg:hidden">
|
||||
@@ -105,21 +100,7 @@ export default function RootLayout({
|
||||
|
||||
<div className="px-1">
|
||||
<div className="relative flex h-full flex-1 flex-col">
|
||||
<main>
|
||||
<Button
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
appState.setAppState((s) => ({
|
||||
...s,
|
||||
isJoined: !s.isJoined,
|
||||
}));
|
||||
}}
|
||||
>
|
||||
Toggle Joined
|
||||
</Button>
|
||||
|
||||
{children}
|
||||
</main>
|
||||
<main>{children}</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { CreateMachineForm } from "@/components/createMachineForm";
|
||||
|
||||
export default function CreateMachine() {
|
||||
return <CreateMachineForm />;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
interface DeviceEditProps {
|
||||
params: { name: string };
|
||||
}
|
||||
|
||||
export default function EditDevice(props: DeviceEditProps) {
|
||||
const {
|
||||
params: { name },
|
||||
} = props;
|
||||
return <div>{name}</div>;
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { MachineContextProvider } from "@/components/hooks/useMachines";
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return <MachineContextProvider>{children}</MachineContextProvider>;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { NodeTable } from "@/components/table";
|
||||
import { StrictMode } from "react";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<StrictMode>
|
||||
<NodeTable />
|
||||
</StrictMode>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
// // <div className={tw`col-span-full row-span-${rowSpan} 2xl:col-span-1 ${sx}`}>
|
||||
// <div className={tw`row-span-2`}>
|
||||
// {children}
|
||||
// </div>
|
||||
// );
|
||||
// };
|
||||
|
||||
// interface DashboardPanelProps {
|
||||
// children?: React.ReactNode;
|
||||
// }
|
||||
// const DashboardPanel = (props: DashboardPanelProps) => {
|
||||
// const { children } = props;
|
||||
// return (
|
||||
// <div className="col-span-full row-span-1 2xl:col-span-2">{children}</div>
|
||||
// );
|
||||
// };
|
||||
|
||||
export default function Dashboard() {
|
||||
const { data, isLoading } = useAppState();
|
||||
const { isLoading } = useAppState();
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="grid h-full place-items-center">
|
||||
@@ -48,26 +17,13 @@ export default function Dashboard() {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (!data.isJoined) {
|
||||
return <JoinPrequel />;
|
||||
}
|
||||
if (data.isJoined) {
|
||||
} else {
|
||||
return (
|
||||
<div className="flex w-full">
|
||||
<div className="grid w-full grid-flow-row grid-cols-3 gap-4">
|
||||
<div className="row-span-2">
|
||||
<NetworkOverview />
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<AppOverview />
|
||||
</div>
|
||||
<div className="row-span-2">
|
||||
<RecentActivity />
|
||||
</div>
|
||||
<QuickActions />
|
||||
<Notifications />
|
||||
<TaskQueue />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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 | HTMLElement>(null);
|
||||
const open = Boolean(anchorEl);
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col items-center justify-center">
|
||||
<div className="w-full">
|
||||
<Button
|
||||
color="secondary"
|
||||
LinkComponent={"a"}
|
||||
href="/templates"
|
||||
startIcon={<ChevronLeft />}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
</div>
|
||||
<div className="h-full w-full border border-solid border-neutral-90 bg-neutral-98 shadow-sm shadow-neutral-60 dark:bg-paper-dark">
|
||||
<div className="flex w-full flex-col items-center justify-center xl:p-2">
|
||||
<Avatar className="m-1 h-20 w-20 bg-purple-40">
|
||||
<Typography variant="h5">N</Typography>
|
||||
</Avatar>
|
||||
<Typography variant="h6" className="text-purple-40">
|
||||
{details.short}
|
||||
</Typography>
|
||||
<div className="w-full">
|
||||
<List
|
||||
className="xl:px-4"
|
||||
sx={{
|
||||
".MuiListSubheader-root": {
|
||||
px: 0,
|
||||
},
|
||||
".MuiListItem-root": {
|
||||
px: 0,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ListSubheader>
|
||||
<Typography variant="caption">Details</Typography>
|
||||
</ListSubheader>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<SettingsEthernet />
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="network" secondary="10.9.20.2" />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Key />
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="secrets" secondary={"< ...hidden >"} />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Group />
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="clans" secondary={"Boss clan.lol"} />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Attachment />
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary="Image"
|
||||
secondary={"/nix/store/12789-image-clan-lol"}
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<IconButton onClick={handleClick}>
|
||||
<Settings />
|
||||
</IconButton>
|
||||
<Menu
|
||||
MenuListProps={{
|
||||
className: "m-2",
|
||||
}}
|
||||
id="image-menu"
|
||||
aria-labelledby="image-menu"
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
anchorOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "left",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "left",
|
||||
}}
|
||||
>
|
||||
<MenuItem>View</MenuItem>
|
||||
<MenuItem>Rebuild</MenuItem>
|
||||
<MenuItem>Delete</MenuItem>
|
||||
</Menu>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Group />
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary="nodes"
|
||||
secondary={"Dad's PC; Mum; Olaf; ... 3 more"}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 flex w-full justify-evenly">
|
||||
<Button
|
||||
variant="text"
|
||||
className="w-full text-black dark:text-white"
|
||||
startIcon={<Edit />}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<Button className="w-full text-red" startIcon={<Delete />}>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<Typography variant="h4">Templates</Typography>
|
||||
<List className="w-full gap-y-4">
|
||||
{templates.map(({ id, name, date }, idx, all) => (
|
||||
<>
|
||||
<ListItem key={id}>
|
||||
<ListItemButton LinkComponent={"a"} href={`/templates/${id}`}>
|
||||
<ListItemAvatar>
|
||||
<Avatar className="bg-purple-40">{name.slice(0, 1)}</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={name} secondary={date} />
|
||||
<ListItemSecondaryAction>
|
||||
<ChevronRight />
|
||||
</ListItemSecondaryAction>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
{idx < all.length - 1 && <Divider flexItem className="mx-10" />}
|
||||
</>
|
||||
))}
|
||||
</List>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
import { useAppState } from "./hooks/useAppContext";
|
||||
|
||||
export default function Background() {
|
||||
const { data, isLoading } = useAppState();
|
||||
const { isLoading } = useAppState();
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -14,7 +14,7 @@ export default function Background() {
|
||||
"fixed -z-10 h-[100vh] w-[100vw] overflow-hidden opacity-10 blur-md dark:opacity-40"
|
||||
}
|
||||
>
|
||||
{(isLoading || !data.isJoined) && (
|
||||
{isLoading && (
|
||||
<>
|
||||
<Image
|
||||
className="dark:hidden"
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
"use client";
|
||||
import { useGetMachineSchema } from "@/api/default/default";
|
||||
import { Check, Error } from "@mui/icons-material";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
LinearProgress,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Paper,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { IChangeEvent } from "@rjsf/core";
|
||||
import { Form } from "@rjsf/mui";
|
||||
import {
|
||||
ErrorListProps,
|
||||
FormContextType,
|
||||
RJSFSchema,
|
||||
StrictRJSFSchema,
|
||||
TranslatableString,
|
||||
} from "@rjsf/utils";
|
||||
import validator from "@rjsf/validator-ajv8";
|
||||
import { JSONSchema7 } from "json-schema";
|
||||
import { useMemo, useRef } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { FormStepContentProps } from "./interfaces";
|
||||
|
||||
interface PureCustomConfigProps extends FormStepContentProps {
|
||||
schema: JSONSchema7;
|
||||
initialValues: any;
|
||||
}
|
||||
export function CustomConfig(props: FormStepContentProps) {
|
||||
const { formHooks } = props;
|
||||
const { data, isLoading, error } = useGetMachineSchema("mama");
|
||||
// const { data, isLoading, error } = { data: {data:{schema: {
|
||||
// title: 'Test form',
|
||||
// type: 'object',
|
||||
// properties: {
|
||||
// name: {
|
||||
// type: 'string',
|
||||
// },
|
||||
// age: {
|
||||
// type: 'number',
|
||||
// },
|
||||
// },
|
||||
// }}}, isLoading: false, error: undefined }
|
||||
const schema = useMemo(() => {
|
||||
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 ? (
|
||||
<LinearProgress variant="indeterminate" />
|
||||
) : error?.message ? (
|
||||
<div>{error?.message}</div>
|
||||
) : (
|
||||
<PureCustomConfig
|
||||
formHooks={formHooks}
|
||||
initialValues={initialValues}
|
||||
schema={schema}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ErrorList<
|
||||
T = any,
|
||||
S extends StrictRJSFSchema = RJSFSchema,
|
||||
F extends FormContextType = any,
|
||||
>({ errors, registry }: ErrorListProps<T, S, F>) {
|
||||
const { translateString } = registry;
|
||||
return (
|
||||
<Paper elevation={0}>
|
||||
<Box mb={2} p={2}>
|
||||
<Typography variant="h6">
|
||||
{translateString(TranslatableString.ErrorsLabel)}
|
||||
</Typography>
|
||||
<List dense={true}>
|
||||
{errors.map((error, i: number) => {
|
||||
return (
|
||||
<ListItem key={i}>
|
||||
<ListItemIcon>
|
||||
<Error color="error" />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={error.stack} />
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</Box>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
function PureCustomConfig(props: PureCustomConfigProps) {
|
||||
const { schema, formHooks } = props;
|
||||
const { setValue, watch } = formHooks;
|
||||
|
||||
console.log({ schema });
|
||||
|
||||
const configData = watch("config") as IChangeEvent<any>;
|
||||
|
||||
console.log({ configData });
|
||||
|
||||
const setConfig = (data: IChangeEvent<any>) => {
|
||||
console.log({ data });
|
||||
setValue("config", data);
|
||||
};
|
||||
|
||||
const formRef = useRef<any>();
|
||||
|
||||
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 (
|
||||
<Form
|
||||
ref={formRef}
|
||||
onChange={setConfig}
|
||||
formData={configData.formData}
|
||||
acceptcharset="utf-8"
|
||||
schema={schema}
|
||||
validator={validator}
|
||||
liveValidate={true}
|
||||
templates={{
|
||||
// ObjectFieldTemplate:
|
||||
ErrorListTemplate: ErrorList,
|
||||
ButtonTemplates: {
|
||||
SubmitButton: (props) => (
|
||||
<div className="flex w-full items-center justify-center">
|
||||
<Button
|
||||
onClick={validate}
|
||||
startIcon={<Check />}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
>
|
||||
Validate
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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<CreateMachineForm>({
|
||||
defaultValues: {
|
||||
name: "",
|
||||
config: {},
|
||||
},
|
||||
});
|
||||
const { handleSubmit, reset } = formHooks;
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
const [activeStep, setActiveStep] = useState<number>(0);
|
||||
|
||||
const steps: FormStep[] = [
|
||||
{
|
||||
id: "template",
|
||||
label: "Template",
|
||||
content: <div></div>,
|
||||
},
|
||||
{
|
||||
id: "modules",
|
||||
label: "Modules",
|
||||
content: <div></div>,
|
||||
},
|
||||
{
|
||||
id: "config",
|
||||
label: "Customize",
|
||||
content: <CustomConfig formHooks={formHooks} />,
|
||||
},
|
||||
{
|
||||
id: "save",
|
||||
label: "Save",
|
||||
content: <div></div>,
|
||||
},
|
||||
];
|
||||
|
||||
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 = () => (
|
||||
<Button
|
||||
color="secondary"
|
||||
disabled={activeStep === 0}
|
||||
onClick={handleBack}
|
||||
sx={{ mr: 1 }}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
);
|
||||
|
||||
const NextButton = () => (
|
||||
<>
|
||||
{activeStep !== steps.length - 1 && (
|
||||
<Button
|
||||
disabled={!formHooks.formState.isValid}
|
||||
onClick={handleNext}
|
||||
color="secondary"
|
||||
>
|
||||
{activeStep <= steps.length - 1 && "Next"}
|
||||
</Button>
|
||||
)}
|
||||
{activeStep === steps.length - 1 && (
|
||||
<Button color="secondary" onClick={handleReset}>
|
||||
Reset
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Box sx={{ width: "100%" }}>
|
||||
{isMobile && (
|
||||
<MobileStepper
|
||||
activeStep={activeStep}
|
||||
color="secondary"
|
||||
backButton={<BackButton />}
|
||||
nextButton={<NextButton />}
|
||||
steps={steps.length}
|
||||
/>
|
||||
)}
|
||||
{!isMobile && (
|
||||
<Stepper activeStep={activeStep} color="secondary">
|
||||
{steps.map(({ label }, index) => {
|
||||
const stepProps: { completed?: boolean } = {};
|
||||
const labelProps: {
|
||||
optional?: React.ReactNode;
|
||||
} = {};
|
||||
return (
|
||||
<Step
|
||||
sx={{
|
||||
".MuiStepIcon-root.Mui-active": {
|
||||
color: "secondary.main",
|
||||
},
|
||||
".MuiStepIcon-root.Mui-completed": {
|
||||
color: "secondary.main",
|
||||
},
|
||||
}}
|
||||
key={label}
|
||||
{...stepProps}
|
||||
>
|
||||
<StepLabel {...labelProps}>{label}</StepLabel>
|
||||
</Step>
|
||||
);
|
||||
})}
|
||||
</Stepper>
|
||||
)}
|
||||
{/* <CustomConfig formHooks={formHooks} /> */}
|
||||
{/* The step Content */}
|
||||
{currentStep && currentStep.content}
|
||||
|
||||
{/* Desktop step controls */}
|
||||
{!isMobile && (
|
||||
<Box sx={{ display: "flex", flexDirection: "row", pt: 2 }}>
|
||||
<BackButton />
|
||||
<Box sx={{ flex: "1 1 auto" }} />
|
||||
<NextButton />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -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<CreateMachineForm>;
|
||||
|
||||
export type FormStep = {
|
||||
id: StepId;
|
||||
label: string;
|
||||
content: FormStepContent;
|
||||
};
|
||||
|
||||
export interface FormStepContentProps {
|
||||
formHooks: FormHooks;
|
||||
}
|
||||
|
||||
export type FormStepContent = ReactElement<FormStepContentProps>;
|
||||
@@ -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 (
|
||||
<DashboardCard title="Clan Overview">
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemText primary={self.name} secondary={self.status} />
|
||||
<ListItemIcon>
|
||||
<Chip
|
||||
label={status[self.status]}
|
||||
color={statusColorMap[self.status]}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
</ListItem>
|
||||
<Divider flexItem />
|
||||
{!other.length && (
|
||||
<div className="my-3 flex h-full w-full justify-center align-middle">
|
||||
<NoDataOverlay
|
||||
label={
|
||||
<ListItemText
|
||||
primary="No other nodes"
|
||||
secondary={<Link href="/nodes">Add devices</Link>}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{firstOthers.map((o) => (
|
||||
<ListItem key={o.id}>
|
||||
<ListItemText primary={o.name} secondary={o.status} />
|
||||
<ListItemIcon>
|
||||
<Chip label={status[o.status]} color={statusColorMap[o.status]} />
|
||||
</ListItemIcon>
|
||||
</ListItem>
|
||||
))}
|
||||
{other.length > MAX_OTHERS && (
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
secondary={` ${other.length - MAX_OTHERS} more ...`}
|
||||
/>
|
||||
</ListItem>
|
||||
)}
|
||||
</List>
|
||||
</DashboardCard>
|
||||
);
|
||||
};
|
||||
@@ -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 (
|
||||
<div
|
||||
role="button"
|
||||
className="flex h-40 w-40 cursor-pointer items-center justify-center rounded-3xl p-2
|
||||
align-middle shadow-md ring-2 ring-inset ring-purple-50
|
||||
hover:bg-neutral-90 focus:bg-neutral-90 active:bg-neutral-80
|
||||
dark:hover:bg-neutral-10 dark:focus:bg-neutral-10 dark:active:bg-neutral-20"
|
||||
>
|
||||
<div className="flex w-full flex-col justify-center">
|
||||
<div className="my-1 flex h-[22] w-[22] items-center justify-center self-center overflow-visible p-1 dark:invert">
|
||||
<Image
|
||||
src={iconPath}
|
||||
alt={`${name}-app-icon`}
|
||||
width={18 * 3}
|
||||
height={18 * 3}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full justify-center">{name}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<DashboardCard title="Applications">
|
||||
<div className="flex h-full w-full justify-center">
|
||||
<div className="flex h-full w-fit justify-center">
|
||||
<div className="grid w-full auto-cols-min auto-rows-min grid-cols-2 gap-8 py-8 sm:grid-cols-3 md:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 ">
|
||||
{apps.map((app) => (
|
||||
<AppCard key={app.name} name={app.name} icon={app.icon} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DashboardCard>
|
||||
);
|
||||
};
|
||||
@@ -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: <InfoIcon />,
|
||||
color: "info",
|
||||
},
|
||||
success: {
|
||||
icon: <CheckIcon />,
|
||||
color: "success",
|
||||
},
|
||||
warning: {
|
||||
icon: <PriorityHighIcon />,
|
||||
color: "warning",
|
||||
},
|
||||
error: {
|
||||
icon: <CloseIcon />,
|
||||
color: "error",
|
||||
},
|
||||
};
|
||||
|
||||
export const Notifications = () => {
|
||||
return (
|
||||
<DashboardCard title="Notifications">
|
||||
<List>
|
||||
{notificationData.map((n, idx) => (
|
||||
<ListItem key={idx}>
|
||||
<ListItemAvatar>
|
||||
<Avatar
|
||||
sx={{
|
||||
bgcolor: `${n.severity}.main`,
|
||||
}}
|
||||
>
|
||||
{severityMap[n.severity].icon}
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={n.msg}
|
||||
secondary={n.date}
|
||||
sx={{
|
||||
width: "100px",
|
||||
}}
|
||||
/>
|
||||
<ListItemText
|
||||
primary={n.source}
|
||||
sx={{
|
||||
width: "100px",
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</DashboardCard>
|
||||
);
|
||||
};
|
||||
@@ -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<HTMLButtonElement>;
|
||||
};
|
||||
|
||||
export const QuickActions = () => {
|
||||
const actions: Action[] = [
|
||||
{
|
||||
id: "network",
|
||||
icon: <LanIcon sx={{ mr: 1 }} />,
|
||||
label: "Network",
|
||||
eventHandler: (event) => {
|
||||
console.log({ event });
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "apps",
|
||||
icon: <AppsIcon sx={{ mr: 1 }} />,
|
||||
label: "Apps",
|
||||
eventHandler: (event) => {
|
||||
console.log({ event });
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "nodes",
|
||||
icon: <DevicesIcon sx={{ mr: 1 }} />,
|
||||
label: "Devices",
|
||||
eventHandler: (event) => {
|
||||
console.log({ event });
|
||||
},
|
||||
},
|
||||
];
|
||||
return (
|
||||
<DashboardCard title="Quick Actions">
|
||||
<div className="flex h-full w-full items-center justify-start pb-10 align-bottom">
|
||||
<div className="flex w-full flex-col flex-wrap justify-evenly gap-2 sm:flex-row">
|
||||
{actions.map(({ id, icon, label, eventHandler }) => (
|
||||
<Fab
|
||||
className="w-fit self-center shadow-none"
|
||||
color="secondary"
|
||||
key={id}
|
||||
onClick={eventHandler}
|
||||
variant="extended"
|
||||
>
|
||||
{icon}
|
||||
<Typography>{label}</Typography>
|
||||
</Fab>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</DashboardCard>
|
||||
);
|
||||
};
|
||||
@@ -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: <SyncIcon className="animate-bounce" />,
|
||||
done: <DoneIcon />,
|
||||
planned: <ScheduleIcon />,
|
||||
};
|
||||
|
||||
interface TaskEntryProps {
|
||||
status: ReactNode;
|
||||
result: "default" | "error" | "info" | "success" | "warning";
|
||||
task: string;
|
||||
details?: string;
|
||||
}
|
||||
const TaskEntry = (props: TaskEntryProps) => {
|
||||
const { result, task, status } = props;
|
||||
return (
|
||||
<>
|
||||
<div className="col-span-1">{status}</div>
|
||||
<div className="col-span-4">{task}</div>
|
||||
<div className="col-span-1">
|
||||
<Chip color={result} label={result} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const TaskQueue = () => {
|
||||
return (
|
||||
<DashboardCard title="Task Queue">
|
||||
<div className="grid grid-cols-6 gap-2 p-4">
|
||||
<TaskEntry
|
||||
result="success"
|
||||
task="Update DevX"
|
||||
status={statusMap.done}
|
||||
/>
|
||||
<TaskEntry
|
||||
result="default"
|
||||
task="Update XYZ"
|
||||
status={statusMap.running}
|
||||
/>
|
||||
<TaskEntry
|
||||
result="default"
|
||||
task="Update ABC"
|
||||
status={statusMap.planned}
|
||||
/>
|
||||
</div>
|
||||
</DashboardCard>
|
||||
);
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
import { Chip } from "@mui/material";
|
||||
|
||||
interface FlakeBadgeProps {
|
||||
flakeUrl: string;
|
||||
flakeAttr: string;
|
||||
}
|
||||
export const FlakeBadge = (props: FlakeBadgeProps) => (
|
||||
<Chip
|
||||
color="secondary"
|
||||
label={`${props.flakeUrl}#${props.flakeAttr}`}
|
||||
sx={{
|
||||
p: 2,
|
||||
"&.MuiChip-root": {
|
||||
maxWidth: "unset",
|
||||
},
|
||||
"&.MuiChip-label": {
|
||||
overflow: "unset",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -11,36 +11,28 @@ import React, {
|
||||
import { KeyedMutator } from "swr";
|
||||
|
||||
type AppContextType = {
|
||||
// data: AxiosResponse<{}, any> | undefined;
|
||||
data: AppState;
|
||||
|
||||
isLoading: boolean;
|
||||
error: AxiosError<any> | undefined;
|
||||
|
||||
setAppState: Dispatch<SetStateAction<AppState>>;
|
||||
mutate: KeyedMutator<AxiosResponse<MachinesResponse, any>>;
|
||||
swrKey: string | false | Record<any, any>;
|
||||
};
|
||||
|
||||
// const initialState = {
|
||||
// isLoading: true,
|
||||
// } as const;
|
||||
|
||||
export const AppContext = createContext<AppContextType>({} 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<AppState>({ isJoined: false });
|
||||
const isLoading = false;
|
||||
const error = undefined;
|
||||
|
||||
const [data, setAppState] = useState<AppState>({});
|
||||
|
||||
return (
|
||||
<AppContext.Provider
|
||||
@@ -49,8 +41,6 @@ export const WithAppState = (props: AppContextProviderProps) => {
|
||||
setAppState,
|
||||
isLoading,
|
||||
error,
|
||||
swrKey,
|
||||
mutate,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -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<MachinesResponse, any> | undefined;
|
||||
data: Machine[];
|
||||
isLoading: boolean;
|
||||
error: AxiosError<any> | undefined;
|
||||
isValidating: boolean;
|
||||
|
||||
filters: Filters;
|
||||
setFilters: Dispatch<SetStateAction<Filters>>;
|
||||
mutate: KeyedMutator<AxiosResponse<MachinesResponse, any>>;
|
||||
swrKey: string | false | Record<any, any>;
|
||||
}
|
||||
| {
|
||||
isLoading: true;
|
||||
data: readonly [];
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
isLoading: true,
|
||||
data: [],
|
||||
} as const;
|
||||
|
||||
export const MachineContext = createContext<MachineContextType>(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<Filters>([]);
|
||||
|
||||
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 (
|
||||
<MachineContext.Provider
|
||||
value={{
|
||||
rawData,
|
||||
data,
|
||||
|
||||
isLoading,
|
||||
error,
|
||||
isValidating,
|
||||
|
||||
filters,
|
||||
setFilters,
|
||||
|
||||
swrKey,
|
||||
mutate,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</MachineContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useMachines = () => React.useContext(MachineContext);
|
||||
@@ -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<VmConfig>();
|
||||
const [error, setError] = useState<AxiosError<HTTPValidationError>>();
|
||||
|
||||
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<HTTPValidationError>;
|
||||
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,
|
||||
};
|
||||
};
|
||||
@@ -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) => (
|
||||
<div className="col-span-4 flex items-center sm:col-span-1">
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
|
||||
interface VmPropContentProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
const VmPropContent = (props: VmPropContentProps) => (
|
||||
<div className="col-span-4 sm:col-span-3">{props.children}</div>
|
||||
);
|
||||
|
||||
interface VmDetailsProps {
|
||||
formHooks: UseFormReturn<VmConfig, any, undefined>;
|
||||
setVmUuid: Dispatch<SetStateAction<string | null>>;
|
||||
}
|
||||
|
||||
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<VmConfig> = 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 (
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="grid grid-cols-4 gap-y-10"
|
||||
>
|
||||
<div className="col-span-4">
|
||||
<ListSubheader sx={{ bgcolor: "inherit" }}>General</ListSubheader>
|
||||
</div>
|
||||
<VmPropLabel>Flake</VmPropLabel>
|
||||
<VmPropContent>
|
||||
<FlakeBadge
|
||||
flakeAttr={watch("flake_attr")}
|
||||
flakeUrl={watch("flake_url")}
|
||||
/>
|
||||
</VmPropContent>
|
||||
<VmPropLabel>Machine</VmPropLabel>
|
||||
<VmPropContent>
|
||||
{!isLoading && (
|
||||
<Controller
|
||||
name="flake_attr"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
{...field}
|
||||
required
|
||||
variant="standard"
|
||||
fullWidth
|
||||
disabled={isLoading}
|
||||
>
|
||||
{!data?.data.flake_attrs.includes("default") && (
|
||||
<MenuItem value={"default"}>default</MenuItem>
|
||||
)}
|
||||
{data?.data.flake_attrs.map((attr) => (
|
||||
<MenuItem value={attr} key={attr}>
|
||||
{attr}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</VmPropContent>
|
||||
<div className="col-span-4">
|
||||
<ListSubheader sx={{ bgcolor: "inherit" }}>VM</ListSubheader>
|
||||
</div>
|
||||
<VmPropLabel>CPU Cores</VmPropLabel>
|
||||
<VmPropContent>
|
||||
<Controller
|
||||
name="cores"
|
||||
control={control}
|
||||
render={({ field }) => <TextField type="number" {...field} />}
|
||||
/>
|
||||
</VmPropContent>
|
||||
<VmPropLabel>Graphics</VmPropLabel>
|
||||
<VmPropContent>
|
||||
<Controller
|
||||
name="graphics"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Switch {...field} defaultChecked={watch("graphics")} />
|
||||
)}
|
||||
/>
|
||||
</VmPropContent>
|
||||
<VmPropLabel>Memory Size</VmPropLabel>
|
||||
|
||||
<VmPropContent>
|
||||
<Controller
|
||||
name="memory_size"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
type="number"
|
||||
{...field}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">MiB</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</VmPropContent>
|
||||
|
||||
<div className="col-span-4 grid items-center">
|
||||
{isStarting && <LinearProgress />}
|
||||
<Button
|
||||
autoFocus
|
||||
type="submit"
|
||||
disabled={isStarting}
|
||||
variant="contained"
|
||||
>
|
||||
Join Clan
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -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 ? (
|
||||
<ConfirmVM
|
||||
url={flakeUrl}
|
||||
handleBack={handleBack}
|
||||
defaultFlakeAttr={flakeAttr}
|
||||
/>
|
||||
) : (
|
||||
<div className="mb-2 flex w-full max-w-2xl flex-col items-center justify-self-center pb-2 ">
|
||||
{isLoading && (
|
||||
<LoadingOverlay
|
||||
title={"Loading Flake"}
|
||||
subtitle={<FlakeBadge flakeUrl={flakeUrl} flakeAttr={flakeAttr} />}
|
||||
/>
|
||||
)}
|
||||
{data && (
|
||||
<>
|
||||
<Typography variant="subtitle1">
|
||||
To join the clan you must trust the Author
|
||||
</Typography>
|
||||
<GppMaybeIcon sx={{ height: "10rem", width: "10rem", mb: 5 }} />
|
||||
<Button
|
||||
autoFocus
|
||||
size="large"
|
||||
color="warning"
|
||||
variant="contained"
|
||||
onClick={() => setUserConfirmed(true)}
|
||||
sx={{ mb: 10 }}
|
||||
>
|
||||
Trust Flake Author
|
||||
</Button>
|
||||
<Log
|
||||
title="What's about to be built"
|
||||
lines={data.data.content.split("\n")}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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<VmConfig>({
|
||||
defaultValues: {
|
||||
flake_url: url,
|
||||
flake_attr: defaultFlakeAttr,
|
||||
cores: 4,
|
||||
graphics: true,
|
||||
memory_size: 2048,
|
||||
},
|
||||
});
|
||||
const [vmUuid, setVmUuid] = useState<string | null>(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 (
|
||||
<div className="mb-2 flex w-full max-w-2xl flex-col items-center justify-self-center pb-2">
|
||||
{!formState.isSubmitted && (
|
||||
<>
|
||||
<div className="mb-2 w-full max-w-2xl">
|
||||
{isLoading && (
|
||||
<LoadingOverlay title={"Loading VM Configuration"} subtitle="" />
|
||||
)}
|
||||
|
||||
<ConfigureVM formHooks={formHooks} setVmUuid={setVmUuid} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{formState.isSubmitted && vmUuid && <VmBuildLogs vmUuid={vmUuid} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="grid h-[70vh] w-full grid-cols-1 justify-center gap-y-4">
|
||||
<Typography variant="h4" className="w-full text-center">
|
||||
Join{" "}
|
||||
<Typography variant="h4" className="font-bold" component={"span"}>
|
||||
Clan.lol
|
||||
</Typography>
|
||||
</Typography>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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: <DashboardIcon />,
|
||||
label: "Dashoard",
|
||||
icon: <AssignmentIndIcon />,
|
||||
label: "Freelance",
|
||||
to: "/",
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
icon: <DevicesIcon />,
|
||||
label: "Machines",
|
||||
to: "/machines",
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
icon: <AppsIcon />,
|
||||
label: "Applications",
|
||||
to: "/applications",
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
icon: <LanIcon />,
|
||||
label: "Network",
|
||||
to: "/network",
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
icon: <DesignServicesIcon />,
|
||||
label: "Templates",
|
||||
to: "/templates",
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
icon: <BackupIcon />,
|
||||
label: "Backups",
|
||||
to: "/backups",
|
||||
icon: <WysiwygIcon />,
|
||||
label: "Blog",
|
||||
to: "/blog",
|
||||
disabled: true,
|
||||
},
|
||||
];
|
||||
@@ -138,23 +109,6 @@ export function Sidebar(props: SidebarProps) {
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
<Divider
|
||||
flexItem
|
||||
className="mx-8 my-10 hidden bg-neutral-40 lg:block"
|
||||
/>
|
||||
<div className="mx-auto mb-8 hidden w-full max-w-xs rounded-sm px-4 py-6 text-center align-bottom shadow-sm lg:block">
|
||||
<h3 className="mb-2 w-full font-semibold text-white">
|
||||
Clan.lol Admin
|
||||
</h3>
|
||||
<a
|
||||
href=""
|
||||
target="_blank"
|
||||
rel="nofollow"
|
||||
className="inline-block w-full rounded-md p-2 text-center text-white hover:text-purple-60/95"
|
||||
>
|
||||
Donate
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
|
||||
@@ -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<EnhancedTableToolbarProps>,
|
||||
) {
|
||||
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 (
|
||||
<Grid2 container spacing={1}>
|
||||
{/* Pie Chart Grid */}
|
||||
<Grid2
|
||||
key="PieChart"
|
||||
md={6}
|
||||
xs={12}
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
>
|
||||
<Box height={350} width={400}>
|
||||
<NodePieChart data={pieData} showLabels={is_lg} />
|
||||
</Box>
|
||||
</Grid2>
|
||||
|
||||
{/* Card Stack Grid */}
|
||||
<Grid2
|
||||
key="CardStack"
|
||||
lg={6}
|
||||
display="flex"
|
||||
sx={{ display: { lg: "flex", xs: "none", md: "flex" } }}
|
||||
>
|
||||
<PieCards pieData={pieData} />
|
||||
</Grid2>
|
||||
|
||||
{/*Toolbar Grid */}
|
||||
<Grid2
|
||||
key="Toolbar"
|
||||
xs={12}
|
||||
container
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
sx={{ pl: { sm: 2 }, pr: { xs: 1, sm: 1 }, pt: { xs: 1, sm: 3 } }}
|
||||
>
|
||||
{props.children}
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { NodeTable } from "./nodeTable";
|
||||
@@ -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 (
|
||||
<Box height={350}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={data}
|
||||
innerRadius={85}
|
||||
outerRadius={120}
|
||||
fill={theme.palette.primary.main}
|
||||
dataKey="value"
|
||||
nameKey="name"
|
||||
label={showLabels}
|
||||
legendType="square"
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
startAngle={0}
|
||||
endAngle={360}
|
||||
paddingAngle={0}
|
||||
labelLine={true}
|
||||
hide={false}
|
||||
minAngle={0}
|
||||
isAnimationActive={true}
|
||||
animationBegin={0}
|
||||
animationDuration={1000}
|
||||
animationEasing="ease-in"
|
||||
blendStroke={true}
|
||||
>
|
||||
{data.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
<Legend verticalAlign="bottom" />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<Stack direction="row" alignItems="center" gap={1}>
|
||||
<CircleIcon color="success" style={{ fontSize: 15 }} />
|
||||
<Typography component="div" align="left" variant="body1">
|
||||
Online
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
case Status.offline:
|
||||
return (
|
||||
<Stack direction="row" alignItems="center" gap={1}>
|
||||
<CircleIcon color="error" style={{ fontSize: 15 }} />
|
||||
<Typography component="div" align="left" variant="body1">
|
||||
Offline
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
case Status.unknown:
|
||||
return (
|
||||
<Stack direction="row" alignItems="center" gap={1}>
|
||||
<CircleIcon color="warning" style={{ fontSize: 15 }} />
|
||||
<Typography component="div" align="left" variant="body1">
|
||||
Pending
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
}
|
||||
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<unknown>, name: string) => {
|
||||
if (isSelected) {
|
||||
setSelected(undefined);
|
||||
} else {
|
||||
setSelected(name);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{/* Rendered Row */}
|
||||
<TableRow
|
||||
hover
|
||||
role="checkbox"
|
||||
aria-checked={isSelected}
|
||||
tabIndex={-1}
|
||||
key={row.name}
|
||||
selected={isSelected}
|
||||
sx={{ cursor: "pointer" }}
|
||||
>
|
||||
<TableCell padding="none">
|
||||
<IconButton
|
||||
aria-label="expand row"
|
||||
size="small"
|
||||
onClick={() => setOpen(!open)}
|
||||
>
|
||||
{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
component="th"
|
||||
scope="row"
|
||||
onClick={(event) => handleClick(event, row.name)}
|
||||
>
|
||||
<Stack>
|
||||
<Typography component="div" align="left" variant="body1">
|
||||
{row.name}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
align="right"
|
||||
onClick={(event) => handleClick(event, row.name)}
|
||||
>
|
||||
{renderStatus(row.status)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
{/* Row Expansion */}
|
||||
<TableRow>
|
||||
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={6}>
|
||||
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||
<Box sx={{ margin: 1 }}>
|
||||
<Typography variant="h6" gutterBottom component="div">
|
||||
Metadata
|
||||
</Typography>
|
||||
<Grid2 container spacing={2} paddingLeft={0}>
|
||||
<Grid2
|
||||
xs={6}
|
||||
justifyContent="left"
|
||||
display="flex"
|
||||
paddingRight={3}
|
||||
>
|
||||
<Box>Hello1</Box>
|
||||
</Grid2>
|
||||
<Grid2 xs={6} paddingLeft={6}>
|
||||
<Box>Hello2</Box>
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
</Box>
|
||||
</Collapse>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
@@ -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<string | undefined>(undefined);
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(5);
|
||||
const [filteredList, setFilteredList] = useState<readonly Machine[]>([]);
|
||||
|
||||
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<HTMLInputElement>) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10));
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
if (machines.isLoading) {
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
sx={{
|
||||
h: "100vh",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<CircularProgress size={80} color="secondary" />
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ width: "100%" }}>
|
||||
<Paper sx={{ width: "100%", mb: 2 }}>
|
||||
<StickySpeedDial selected={selected} />
|
||||
<EnhancedTableToolbar tableData={tableData}>
|
||||
<Grid2 xs={12}>
|
||||
<SearchBar
|
||||
tableData={tableData}
|
||||
setFilteredList={setFilteredList}
|
||||
/>
|
||||
</Grid2>
|
||||
</EnhancedTableToolbar>
|
||||
|
||||
<NodeTableContainer
|
||||
tableData={filteredList}
|
||||
page={page}
|
||||
rowsPerPage={rowsPerPage}
|
||||
dense={false}
|
||||
selected={selected}
|
||||
setSelected={setSelected}
|
||||
/>
|
||||
|
||||
{/* TODO: This creates the error Warning: Prop `id` did not match. Server: ":RspmmcqH1:" Client: ":R3j6qpj9H1:" */}
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[5, 10, 25]}
|
||||
labelRowsPerPage={is_xs ? "Rows" : "Rows per page:"}
|
||||
component="div"
|
||||
count={filteredList.length}
|
||||
rowsPerPage={rowsPerPage}
|
||||
page={page}
|
||||
onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||
/>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -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<T>(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<Key extends keyof any>(
|
||||
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<T>(
|
||||
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<unknown>,
|
||||
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<unknown>) => {
|
||||
onRequestSort(event, property);
|
||||
};
|
||||
|
||||
return (
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell id="dropdown" colSpan={1} />
|
||||
{headCells.map((headCell) => (
|
||||
<TableCell
|
||||
key={headCell.id}
|
||||
align={headCell.alignRight ? "right" : "left"}
|
||||
padding={headCell.disablePadding ? "none" : "normal"}
|
||||
sortDirection={orderBy === headCell.id ? order : false}
|
||||
>
|
||||
<TableSortLabel
|
||||
active={orderBy === headCell.id}
|
||||
direction={orderBy === headCell.id ? order : "asc"}
|
||||
onClick={createSortHandler(headCell.id)}
|
||||
>
|
||||
{headCell.label}
|
||||
{orderBy === headCell.id ? (
|
||||
<Box component="span" sx={visuallyHidden}>
|
||||
{order === "desc" ? "sorted descending" : "sorted ascending"}
|
||||
</Box>
|
||||
) : null}
|
||||
</TableSortLabel>
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
);
|
||||
}
|
||||
|
||||
interface NodeTableContainerProps {
|
||||
tableData: readonly Machine[];
|
||||
page: number;
|
||||
rowsPerPage: number;
|
||||
dense: boolean;
|
||||
selected: string | undefined;
|
||||
setSelected: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
}
|
||||
|
||||
export function NodeTableContainer(props: NodeTableContainerProps) {
|
||||
const { tableData, page, rowsPerPage, dense, selected, setSelected } = props;
|
||||
const [order, setOrder] = React.useState<NodeOrder>("asc");
|
||||
const [orderBy, setOrderBy] = React.useState<keyof Machine>("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<unknown>,
|
||||
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 (
|
||||
<TableContainer>
|
||||
<Table aria-labelledby="tableTitle" size={dense ? "small" : "medium"}>
|
||||
<EnhancedTableHead
|
||||
order={order}
|
||||
orderBy={orderBy}
|
||||
onRequestSort={handleRequestSort}
|
||||
rowCount={tableData.length}
|
||||
/>
|
||||
<TableBody>
|
||||
{visibleRows.map((row, index) => {
|
||||
return (
|
||||
<NodeRow
|
||||
key={index}
|
||||
row={row}
|
||||
selected={selected}
|
||||
setSelected={setSelected}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{emptyRows > 0 && (
|
||||
<TableRow
|
||||
style={{
|
||||
height: (dense ? 33 : 53) * emptyRows,
|
||||
}}
|
||||
>
|
||||
<TableCell colSpan={6} />
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<Stack
|
||||
sx={{ paddingTop: 6 }}
|
||||
height={350}
|
||||
id="cardBox"
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
justifyContent="flex-start"
|
||||
flexWrap="wrap"
|
||||
>
|
||||
{cardData.map((pieItem) => (
|
||||
<Card
|
||||
key={pieItem.name}
|
||||
sx={{
|
||||
marginBottom: 2,
|
||||
marginRight: 2,
|
||||
width: 110,
|
||||
height: 110,
|
||||
backgroundColor: hexRgb(pieItem.color, {
|
||||
format: "css",
|
||||
alpha: 0.25,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<CardContent>
|
||||
<Typography
|
||||
variant="h4"
|
||||
component="div"
|
||||
gutterBottom={true}
|
||||
textAlign="center"
|
||||
>
|
||||
{pieItem.value}
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{ mb: 1.5 }}
|
||||
color="text.secondary"
|
||||
textAlign="center"
|
||||
>
|
||||
{pieItem.name}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -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<SetStateAction<readonly Machine[]>>;
|
||||
}
|
||||
|
||||
export function SearchBar(props: SearchBarProps) {
|
||||
let { tableData, setFilteredList } = props;
|
||||
const [search, setSearch] = useState<string>("");
|
||||
const debouncedSearch = useDebounce(search, 250);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
// Define a function to handle the Esc key press
|
||||
function handleEsc(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||
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 (
|
||||
<Autocomplete
|
||||
freeSolo
|
||||
autoComplete
|
||||
options={suggestions}
|
||||
renderOption={(props: any, option: any) => {
|
||||
return (
|
||||
<li {...props} key={option}>
|
||||
{option}
|
||||
</li>
|
||||
);
|
||||
}}
|
||||
onKeyDown={handleEsc}
|
||||
onInputChange={handleInputChange}
|
||||
value={search}
|
||||
open={open}
|
||||
onOpen={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
onClose={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
fullWidth
|
||||
label="Search"
|
||||
variant="outlined"
|
||||
autoComplete="nickname"
|
||||
InputProps={{
|
||||
...params.InputProps,
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<IconButton>
|
||||
<SearchIcon />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
></TextField>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<Link href={`/machines/edit/${selected}`} style={{ marginTop: 7.5 }}>
|
||||
<EditIcon color="action" />
|
||||
</Link>
|
||||
);
|
||||
} else {
|
||||
return <EditIcon color="disabled" />;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
transform: "translateZ(0px)",
|
||||
flexGrow: 1,
|
||||
position: "fixed",
|
||||
right: 20,
|
||||
top: 15,
|
||||
margin: 0,
|
||||
zIndex: 9000,
|
||||
}}
|
||||
>
|
||||
<SpeedDial
|
||||
color="secondary"
|
||||
ariaLabel="SpeedDial basic example"
|
||||
icon={<SpeedDialIcon />}
|
||||
direction="down"
|
||||
onClose={handleClose}
|
||||
onOpen={handleOpen}
|
||||
open={open}
|
||||
>
|
||||
<SpeedDialAction
|
||||
key="Add"
|
||||
icon={
|
||||
<Link href="/machines/add" style={{ marginTop: 7.5 }}>
|
||||
<AddIcon color="action" />
|
||||
</Link>
|
||||
}
|
||||
tooltipTitle="Add"
|
||||
/>
|
||||
|
||||
<SpeedDialAction
|
||||
key="Delete"
|
||||
icon={
|
||||
<DeleteIcon color={isSomethingSelected ? "action" : "disabled"} />
|
||||
}
|
||||
tooltipTitle="Delete"
|
||||
/>
|
||||
<SpeedDialAction key="Edit" icon={editDial()} tooltipTitle="Edit" />
|
||||
</SpeedDial>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -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",
|
||||
};
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -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",
|
||||
},
|
||||
];
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
Reference in New Issue
Block a user