Befor fixing linting problem
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
source_up
|
source_up
|
||||||
|
|
||||||
|
|
||||||
if type nix_direnv_watch_file &>/dev/null; then
|
if type nix_direnv_watch_file &>/dev/null; then
|
||||||
nix_direnv_watch_file flake-module.nix
|
nix_direnv_watch_file flake-module.nix
|
||||||
nix_direnv_watch_file default.nix
|
nix_direnv_watch_file default.nix
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import argparse
|
import argparse
|
||||||
|
import logging
|
||||||
import sys
|
import sys
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from . import config, flakes, join, machines, secrets, vms, webui
|
from . import config, flakes, join, machines, secrets, vms, webui
|
||||||
|
from .custom_logger import register
|
||||||
from .ssh import cli as ssh_cli
|
from .ssh import cli as ssh_cli
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
argcomplete: Optional[ModuleType] = None
|
argcomplete: Optional[ModuleType] = None
|
||||||
try:
|
try:
|
||||||
import argcomplete # type: ignore[no-redef]
|
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")
|
parser_vms = subparsers.add_parser("vms", help="manage virtual machines")
|
||||||
vms.register_parser(parser_vms)
|
vms.register_parser(parser_vms)
|
||||||
|
|
||||||
|
# if args.debug:
|
||||||
|
register(logging.DEBUG)
|
||||||
|
log.debug("Debug log activated")
|
||||||
|
|
||||||
if argcomplete:
|
if argcomplete:
|
||||||
argcomplete.autocomplete(parser)
|
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.dirs import machine_settings_file, specific_flake_dir
|
||||||
from clan_cli.errors import ClanError
|
from clan_cli.errors import ClanError
|
||||||
from clan_cli.flakes.types import FlakeName
|
|
||||||
from clan_cli.git import commit_file
|
from clan_cli.git import commit_file
|
||||||
from clan_cli.nix import nix_eval
|
from clan_cli.nix import nix_eval
|
||||||
|
from clan_cli.types import FlakeName
|
||||||
|
|
||||||
script_dir = Path(__file__).parent
|
script_dir = Path(__file__).parent
|
||||||
|
|
||||||
@@ -161,7 +161,11 @@ def read_machine_option_value(
|
|||||||
|
|
||||||
def get_or_set_option(args: argparse.Namespace) -> None:
|
def get_or_set_option(args: argparse.Namespace) -> None:
|
||||||
if args.value == []:
|
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:
|
else:
|
||||||
# load options
|
# load options
|
||||||
if args.options_file is None:
|
if args.options_file is None:
|
||||||
@@ -308,11 +312,6 @@ def register_parser(
|
|||||||
|
|
||||||
# inject callback function to process the input later
|
# inject callback function to process the input later
|
||||||
parser.set_defaults(func=get_or_set_option)
|
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(
|
parser.add_argument(
|
||||||
"--machine",
|
"--machine",
|
||||||
"-m",
|
"-m",
|
||||||
@@ -356,6 +355,11 @@ def register_parser(
|
|||||||
nargs="*",
|
nargs="*",
|
||||||
help="option value to set (if omitted, the current value is printed)",
|
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:
|
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.git import commit_file, find_git_repo_root
|
||||||
from clan_cli.nix import nix_eval
|
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:
|
def config_for_machine(flake_name: FlakeName, machine_name: str) -> dict:
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from pathlib import Path
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
grey = "\x1b[38;20m"
|
grey = "\x1b[38;20m"
|
||||||
yellow = "\x1b[33;20m"
|
yellow = "\x1b[33;20m"
|
||||||
@@ -9,11 +11,20 @@ green = "\u001b[32m"
|
|||||||
blue = "\u001b[34m"
|
blue = "\u001b[34m"
|
||||||
|
|
||||||
|
|
||||||
def get_formatter(color: str) -> logging.Formatter:
|
def get_formatter(color: str) -> Callable[[logging.LogRecord, bool], logging.Formatter]:
|
||||||
reset = "\x1b[0m"
|
def myformatter(
|
||||||
return logging.Formatter(
|
record: logging.LogRecord, with_location: bool
|
||||||
f"{color}%(levelname)s{reset}:(%(filename)s:%(lineno)d): %(message)s"
|
) -> 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 = {
|
FORMATTER = {
|
||||||
@@ -26,12 +37,34 @@ FORMATTER = {
|
|||||||
|
|
||||||
|
|
||||||
class CustomFormatter(logging.Formatter):
|
class CustomFormatter(logging.Formatter):
|
||||||
def format(self, record: Any) -> str:
|
def format(self, record: logging.LogRecord) -> str:
|
||||||
return FORMATTER[record.levelno].format(record)
|
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:
|
def register(level: Any) -> None:
|
||||||
ch = logging.StreamHandler()
|
handler = logging.StreamHandler()
|
||||||
ch.setLevel(level)
|
handler.setLevel(level)
|
||||||
ch.setFormatter(CustomFormatter())
|
handler.setFormatter(CustomFormatter())
|
||||||
logging.basicConfig(level=level, handlers=[ch])
|
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 os
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from .errors import ClanError
|
from .errors import ClanError
|
||||||
from .flakes.types import FlakeName
|
from .types import FlakeName
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def _get_clan_flake_toplevel() -> Path:
|
def _get_clan_flake_toplevel() -> Path:
|
||||||
@@ -51,28 +54,31 @@ def user_data_dir() -> Path:
|
|||||||
def clan_data_dir() -> Path:
|
def clan_data_dir() -> Path:
|
||||||
path = user_data_dir() / "clan"
|
path = user_data_dir() / "clan"
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
path.mkdir()
|
log.debug(f"Creating path with parents {path}")
|
||||||
|
path.mkdir(parents=True)
|
||||||
return path.resolve()
|
return path.resolve()
|
||||||
|
|
||||||
|
|
||||||
def clan_config_dir() -> Path:
|
def clan_config_dir() -> Path:
|
||||||
path = user_config_dir() / "clan"
|
path = user_config_dir() / "clan"
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
path.mkdir()
|
log.debug(f"Creating path with parents {path}")
|
||||||
|
path.mkdir(parents=True)
|
||||||
return path.resolve()
|
return path.resolve()
|
||||||
|
|
||||||
|
|
||||||
def clan_flakes_dir() -> Path:
|
def clan_flakes_dir() -> Path:
|
||||||
path = clan_data_dir() / "flake"
|
path = clan_data_dir() / "flake"
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
path.mkdir()
|
log.debug(f"Creating path with parents {path}")
|
||||||
|
path.mkdir(parents=True)
|
||||||
return path.resolve()
|
return path.resolve()
|
||||||
|
|
||||||
|
|
||||||
def specific_flake_dir(flake_name: FlakeName) -> Path:
|
def specific_flake_dir(flake_name: FlakeName) -> Path:
|
||||||
flake_dir = clan_flakes_dir() / flake_name
|
flake_dir = clan_flakes_dir() / flake_name
|
||||||
if not flake_dir.exists():
|
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
|
return flake_dir
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,16 +8,20 @@ from pydantic.tools import parse_obj_as
|
|||||||
|
|
||||||
from ..async_cmd import CmdOut, run, runforcli
|
from ..async_cmd import CmdOut, run, runforcli
|
||||||
from ..dirs import clan_flakes_dir
|
from ..dirs import clan_flakes_dir
|
||||||
|
from ..errors import ClanError
|
||||||
from ..nix import nix_command, nix_shell
|
from ..nix import nix_command, nix_shell
|
||||||
|
|
||||||
DEFAULT_URL: AnyUrl = parse_obj_as(
|
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]:
|
async def create_flake(directory: Path, url: AnyUrl) -> Dict[str, CmdOut]:
|
||||||
if not directory.exists():
|
if not directory.exists():
|
||||||
directory.mkdir()
|
directory.mkdir()
|
||||||
|
else:
|
||||||
|
raise ClanError(f"Flake at '{directory}' already exists")
|
||||||
response = {}
|
response = {}
|
||||||
command = nix_command(
|
command = nix_command(
|
||||||
[
|
[
|
||||||
@@ -27,27 +31,27 @@ async def create_flake(directory: Path, url: AnyUrl) -> Dict[str, CmdOut]:
|
|||||||
url,
|
url,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
out = await run(command, directory)
|
out = await run(command, cwd=directory)
|
||||||
response["flake init"] = out
|
response["flake init"] = out
|
||||||
|
|
||||||
command = nix_shell(["git"], ["git", "init"])
|
command = nix_shell(["git"], ["git", "init"])
|
||||||
out = await run(command, directory)
|
out = await run(command, cwd=directory)
|
||||||
response["git init"] = out
|
response["git init"] = out
|
||||||
|
|
||||||
command = nix_shell(["git"], ["git", "add", "."])
|
command = nix_shell(["git"], ["git", "add", "."])
|
||||||
out = await run(command, directory)
|
out = await run(command, cwd=directory)
|
||||||
response["git add"] = out
|
response["git add"] = out
|
||||||
|
|
||||||
command = nix_shell(["git"], ["git", "config", "user.name", "clan-tool"])
|
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
|
response["git config"] = out
|
||||||
|
|
||||||
command = nix_shell(["git"], ["git", "config", "user.email", "clan@example.com"])
|
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
|
response["git config"] = out
|
||||||
|
|
||||||
command = nix_shell(["git"], ["git", "commit", "-a", "-m", "Initial commit"])
|
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
|
response["git commit"] = out
|
||||||
|
|
||||||
return response
|
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:
|
def create_flake_command(args: argparse.Namespace) -> None:
|
||||||
flake_dir = clan_flakes_dir() / args.name
|
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
|
# takes a (sub)parser and configures it
|
||||||
@@ -65,5 +69,11 @@ def register_create_parser(parser: argparse.ArgumentParser) -> None:
|
|||||||
type=str,
|
type=str,
|
||||||
help="name for the flake",
|
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.add_argument("name", type=str, help="name of the flake")
|
||||||
parser.set_defaults(func=create_flake_command)
|
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 ..async_cmd import CmdOut, run, runforcli
|
||||||
from ..dirs import specific_flake_dir, specific_machine_dir
|
from ..dirs import specific_flake_dir, specific_machine_dir
|
||||||
from ..errors import ClanError
|
from ..errors import ClanError
|
||||||
from ..flakes.types import FlakeName
|
|
||||||
from ..nix import nix_shell
|
from ..nix import nix_shell
|
||||||
|
from ..types import FlakeName
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def create_machine(flake_name: FlakeName, machine_name: str) -> Dict[str, CmdOut]:
|
async def create_machine(flake_name: FlakeName, machine_name: str) -> Dict[str, CmdOut]:
|
||||||
folder = specific_machine_dir(flake_name, machine_name)
|
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)
|
folder.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
# create empty settings.json file inside the folder
|
# create empty settings.json file inside the folder
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from ..dirs import specific_machine_dir
|
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:
|
def machine_has_fact(flake_name: FlakeName, machine: str, fact: str) -> bool:
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import logging
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from ..dirs import machines_dir
|
from ..dirs import machines_dir
|
||||||
from ..flakes.types import FlakeName
|
from ..types import FlakeName
|
||||||
from .types import validate_hostname
|
from .types import validate_hostname
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from typing import Callable
|
|||||||
|
|
||||||
from ..dirs import specific_flake_dir
|
from ..dirs import specific_flake_dir
|
||||||
from ..errors import ClanError
|
from ..errors import ClanError
|
||||||
from ..flakes.types import FlakeName
|
from ..types import FlakeName
|
||||||
|
|
||||||
|
|
||||||
def get_sops_folder(flake_name: FlakeName) -> Path:
|
def get_sops_folder(flake_name: FlakeName) -> Path:
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import os
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from ..errors import ClanError
|
from ..errors import ClanError
|
||||||
from ..flakes.types import FlakeName
|
|
||||||
from ..machines.types import machine_name_type, validate_hostname
|
from ..machines.types import machine_name_type, validate_hostname
|
||||||
|
from ..types import FlakeName
|
||||||
from . import secrets
|
from . import secrets
|
||||||
from .folders import (
|
from .folders import (
|
||||||
sops_groups_folder,
|
sops_groups_folder,
|
||||||
@@ -204,9 +204,17 @@ def register_groups_parser(parser: argparse.ArgumentParser) -> None:
|
|||||||
help="the command to run",
|
help="the command to run",
|
||||||
required=True,
|
required=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# List groups
|
||||||
list_parser = subparser.add_parser("list", help="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)
|
list_parser.set_defaults(func=list_command)
|
||||||
|
|
||||||
|
# Add user
|
||||||
add_machine_parser = subparser.add_parser(
|
add_machine_parser = subparser.add_parser(
|
||||||
"add-machine", help="add a machine to group"
|
"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(
|
add_machine_parser.add_argument(
|
||||||
"machine", help="the name of the machines to add", type=machine_name_type
|
"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)
|
add_machine_parser.set_defaults(func=add_machine_command)
|
||||||
|
|
||||||
|
# Remove machine
|
||||||
remove_machine_parser = subparser.add_parser(
|
remove_machine_parser = subparser.add_parser(
|
||||||
"remove-machine", help="remove a machine from group"
|
"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(
|
remove_machine_parser.add_argument(
|
||||||
"machine", help="the name of the machines to remove", type=machine_name_type
|
"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)
|
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_user_parser = subparser.add_parser("add-user", help="add a user to group")
|
||||||
add_group_argument(add_user_parser)
|
add_group_argument(add_user_parser)
|
||||||
add_user_parser.add_argument(
|
add_user_parser.add_argument(
|
||||||
"user", help="the name of the user to add", type=user_name_type
|
"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)
|
add_user_parser.set_defaults(func=add_user_command)
|
||||||
|
|
||||||
|
# Remove user
|
||||||
remove_user_parser = subparser.add_parser(
|
remove_user_parser = subparser.add_parser(
|
||||||
"remove-user", help="remove a user from group"
|
"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(
|
remove_user_parser.add_argument(
|
||||||
"user", help="the name of the user to remove", type=user_name_type
|
"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)
|
remove_user_parser.set_defaults(func=remove_user_command)
|
||||||
|
|
||||||
|
# Add secret
|
||||||
add_secret_parser = subparser.add_parser(
|
add_secret_parser = subparser.add_parser(
|
||||||
"add-secret", help="allow a user to access a secret"
|
"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(
|
add_secret_parser.add_argument(
|
||||||
"secret", help="the name of the secret", type=secret_name_type
|
"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)
|
add_secret_parser.set_defaults(func=add_secret_command)
|
||||||
|
|
||||||
|
# Remove secret
|
||||||
remove_secret_parser = subparser.add_parser(
|
remove_secret_parser = subparser.add_parser(
|
||||||
"remove-secret", help="remove a group's access to a secret"
|
"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(
|
remove_secret_parser.add_argument(
|
||||||
"secret", help="the name of the secret", type=secret_name_type
|
"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)
|
remove_secret_parser.set_defaults(func=remove_secret_command)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
from ..flakes.types import FlakeName
|
|
||||||
from ..machines.types import machine_name_type, validate_hostname
|
from ..machines.types import machine_name_type, validate_hostname
|
||||||
|
from ..types import FlakeName
|
||||||
from . import secrets
|
from . import secrets
|
||||||
from .folders import list_objects, remove_object, sops_machines_folder
|
from .folders import list_objects, remove_object, sops_machines_folder
|
||||||
from .sops import read_key, write_key
|
from .sops import read_key, write_key
|
||||||
@@ -96,11 +96,6 @@ def register_machines_parser(parser: argparse.ArgumentParser) -> None:
|
|||||||
action="store_true",
|
action="store_true",
|
||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
add_parser.add_argument(
|
|
||||||
"flake",
|
|
||||||
type=str,
|
|
||||||
help="name of the flake to create machine for",
|
|
||||||
)
|
|
||||||
add_parser.add_argument(
|
add_parser.add_argument(
|
||||||
"machine", help="the name of the machine", type=machine_name_type
|
"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",
|
help="public key or private key of the user",
|
||||||
type=public_or_private_age_key_type,
|
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)
|
add_parser.set_defaults(func=add_command)
|
||||||
|
|
||||||
# Parser
|
# Parser
|
||||||
@@ -125,46 +125,46 @@ def register_machines_parser(parser: argparse.ArgumentParser) -> None:
|
|||||||
|
|
||||||
# Parser
|
# Parser
|
||||||
remove_parser = subparser.add_parser("remove", help="remove a machine")
|
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(
|
remove_parser.add_argument(
|
||||||
"flake",
|
"flake",
|
||||||
type=str,
|
type=str,
|
||||||
help="name of the flake to create machine for",
|
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)
|
remove_parser.set_defaults(func=remove_command)
|
||||||
|
|
||||||
# Parser
|
# Parser
|
||||||
add_secret_parser = subparser.add_parser(
|
add_secret_parser = subparser.add_parser(
|
||||||
"add-secret", help="allow a machine to access a secret"
|
"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(
|
add_secret_parser.add_argument(
|
||||||
"machine", help="the name of the machine", type=machine_name_type
|
"machine", help="the name of the machine", type=machine_name_type
|
||||||
)
|
)
|
||||||
add_secret_parser.add_argument(
|
add_secret_parser.add_argument(
|
||||||
"secret", help="the name of the secret", type=secret_name_type
|
"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)
|
add_secret_parser.set_defaults(func=add_secret_command)
|
||||||
|
|
||||||
# Parser
|
# Parser
|
||||||
remove_secret_parser = subparser.add_parser(
|
remove_secret_parser = subparser.add_parser(
|
||||||
"remove-secret", help="remove a group's access to a secret"
|
"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(
|
remove_secret_parser.add_argument(
|
||||||
"machine", help="the name of the group", type=machine_name_type
|
"machine", help="the name of the group", type=machine_name_type
|
||||||
)
|
)
|
||||||
remove_secret_parser.add_argument(
|
remove_secret_parser.add_argument(
|
||||||
"secret", help="the name of the secret", type=secret_name_type
|
"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)
|
remove_secret_parser.set_defaults(func=remove_secret_command)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from typing import IO
|
|||||||
|
|
||||||
from .. import tty
|
from .. import tty
|
||||||
from ..errors import ClanError
|
from ..errors import ClanError
|
||||||
from ..flakes.types import FlakeName
|
from ..types import FlakeName
|
||||||
from .folders import (
|
from .folders import (
|
||||||
list_objects,
|
list_objects,
|
||||||
sops_groups_folder,
|
sops_groups_folder,
|
||||||
@@ -253,24 +253,24 @@ def rename_command(args: argparse.Namespace) -> None:
|
|||||||
|
|
||||||
def register_secrets_parser(subparser: argparse._SubParsersAction) -> None:
|
def register_secrets_parser(subparser: argparse._SubParsersAction) -> None:
|
||||||
parser_list = subparser.add_parser("list", help="list secrets")
|
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_list.set_defaults(func=list_command)
|
||||||
|
|
||||||
parser_get = subparser.add_parser("get", help="get a secret")
|
parser_get = subparser.add_parser("get", help="get a secret")
|
||||||
add_secret_argument(parser_get)
|
add_secret_argument(parser_get)
|
||||||
parser_get.set_defaults(func=get_command)
|
|
||||||
parser_get.add_argument(
|
parser_get.add_argument(
|
||||||
"flake",
|
"flake",
|
||||||
type=str,
|
type=str,
|
||||||
help="name of the flake to create machine for",
|
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")
|
parser_set = subparser.add_parser("set", help="set a secret")
|
||||||
add_secret_argument(parser_set)
|
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(
|
parser_set.add_argument(
|
||||||
"--group",
|
"--group",
|
||||||
type=str,
|
type=str,
|
||||||
@@ -299,13 +299,28 @@ def register_secrets_parser(subparser: argparse._SubParsersAction) -> None:
|
|||||||
default=False,
|
default=False,
|
||||||
help="edit the secret with $EDITOR instead of pasting it",
|
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_set.set_defaults(func=set_command)
|
||||||
|
|
||||||
parser_rename = subparser.add_parser("rename", help="rename a secret")
|
parser_rename = subparser.add_parser("rename", help="rename a secret")
|
||||||
add_secret_argument(parser_rename)
|
add_secret_argument(parser_rename)
|
||||||
parser_rename.add_argument("new_name", type=str, help="the new name of the secret")
|
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_rename.set_defaults(func=rename_command)
|
||||||
|
|
||||||
parser_remove = subparser.add_parser("remove", help="remove a secret")
|
parser_remove = subparser.add_parser("remove", help="remove a secret")
|
||||||
add_secret_argument(parser_remove)
|
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)
|
parser_remove.set_defaults(func=remove_command)
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ from typing import IO, Iterator
|
|||||||
|
|
||||||
from ..dirs import user_config_dir
|
from ..dirs import user_config_dir
|
||||||
from ..errors import ClanError
|
from ..errors import ClanError
|
||||||
from ..flakes.types import FlakeName
|
|
||||||
from ..nix import nix_shell
|
from ..nix import nix_shell
|
||||||
|
from ..types import FlakeName
|
||||||
from .folders import sops_machines_folder, sops_users_folder
|
from .folders import sops_machines_folder, sops_users_folder
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,17 +6,19 @@ import sys
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
import logging
|
||||||
|
|
||||||
from clan_cli.nix import nix_shell
|
from clan_cli.nix import nix_shell
|
||||||
|
|
||||||
from ..dirs import specific_flake_dir
|
from ..dirs import specific_flake_dir
|
||||||
from ..errors import ClanError
|
from ..errors import ClanError
|
||||||
from ..flakes.types import FlakeName
|
from ..types import FlakeName
|
||||||
from .folders import sops_secrets_folder
|
from .folders import sops_secrets_folder
|
||||||
from .machines import add_machine, has_machine
|
from .machines import add_machine, has_machine
|
||||||
from .secrets import decrypt_secret, encrypt_secret, has_secret
|
from .secrets import decrypt_secret, encrypt_secret, has_secret
|
||||||
from .sops import generate_private_key
|
from .sops import generate_private_key
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
def generate_host_key(flake_name: FlakeName, machine_name: str) -> None:
|
def generate_host_key(flake_name: FlakeName, machine_name: str) -> None:
|
||||||
if has_machine(flake_name, machine_name):
|
if has_machine(flake_name, machine_name):
|
||||||
@@ -95,6 +97,7 @@ def generate_secrets_from_nix(
|
|||||||
) -> None:
|
) -> None:
|
||||||
generate_host_key(flake_name, machine_name)
|
generate_host_key(flake_name, machine_name)
|
||||||
errors = {}
|
errors = {}
|
||||||
|
log.debug("Generating secrets for machine %s and flake %s", machine_name, flake_name)
|
||||||
with TemporaryDirectory() as d:
|
with TemporaryDirectory() as d:
|
||||||
# if any of the secrets are missing, we regenerate all connected facts/secrets
|
# if any of the secrets are missing, we regenerate all connected facts/secrets
|
||||||
for secret_group, secret_options in secret_submodules.items():
|
for secret_group, secret_options in secret_submodules.items():
|
||||||
@@ -116,6 +119,7 @@ def upload_age_key_from_nix(
|
|||||||
flake_name: FlakeName,
|
flake_name: FlakeName,
|
||||||
machine_name: str,
|
machine_name: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
log.debug("Uploading secrets for machine %s and flake %s", machine_name, flake_name)
|
||||||
secret_name = f"{machine_name}-age.key"
|
secret_name = f"{machine_name}-age.key"
|
||||||
if not has_secret(
|
if not has_secret(
|
||||||
flake_name, secret_name
|
flake_name, secret_name
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
from ..flakes.types import FlakeName
|
from ..types import FlakeName
|
||||||
from . import secrets
|
from . import secrets
|
||||||
from .folders import list_objects, remove_object, sops_users_folder
|
from .folders import list_objects, remove_object, sops_users_folder
|
||||||
from .sops import read_key, write_key
|
from .sops import read_key, write_key
|
||||||
@@ -131,6 +131,11 @@ def register_users_parser(parser: argparse.ArgumentParser) -> None:
|
|||||||
add_secret_parser.add_argument(
|
add_secret_parser.add_argument(
|
||||||
"secret", help="the name of the secret", type=secret_name_type
|
"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)
|
add_secret_parser.set_defaults(func=add_secret_command)
|
||||||
|
|
||||||
remove_secret_parser = subparser.add_parser(
|
remove_secret_parser = subparser.add_parser(
|
||||||
@@ -142,4 +147,9 @@ def register_users_parser(parser: argparse.ArgumentParser) -> None:
|
|||||||
remove_secret_parser.add_argument(
|
remove_secret_parser.add_argument(
|
||||||
"secret", help="the name of the secret", type=secret_name_type
|
"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)
|
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 typing import Any, Iterator, Optional, Type, TypeVar
|
||||||
from uuid import UUID, uuid4
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
|
from .custom_logger import ThreadFormatter, get_caller
|
||||||
from .errors import ClanError
|
from .errors import ClanError
|
||||||
|
|
||||||
|
|
||||||
@@ -38,7 +39,8 @@ class Command:
|
|||||||
cwd: Optional[Path] = None,
|
cwd: Optional[Path] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.running = True
|
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
|
cwd_res = None
|
||||||
if cwd is not None:
|
if cwd is not None:
|
||||||
@@ -68,10 +70,10 @@ class Command:
|
|||||||
try:
|
try:
|
||||||
for line in fd:
|
for line in fd:
|
||||||
if fd == self.p.stderr:
|
if fd == self.p.stderr:
|
||||||
print(f"[{cmd[0]}] stderr: {line}")
|
self.log.debug(f"[{cmd[0]}] stderr: {line}")
|
||||||
self.stderr.append(line)
|
self.stderr.append(line)
|
||||||
else:
|
else:
|
||||||
print(f"[{cmd[0]}] stdout: {line}")
|
self.log.debug(f"[{cmd[0]}] stdout: {line}")
|
||||||
self.stdout.append(line)
|
self.stdout.append(line)
|
||||||
self._output.put(line)
|
self._output.put(line)
|
||||||
except BlockingIOError:
|
except BlockingIOError:
|
||||||
@@ -80,8 +82,6 @@ class Command:
|
|||||||
if self.p.returncode != 0:
|
if self.p.returncode != 0:
|
||||||
raise ClanError(f"Failed to run command: {shlex.join(cmd)}")
|
raise ClanError(f"Failed to run command: {shlex.join(cmd)}")
|
||||||
|
|
||||||
self.log.debug("Successfully ran command")
|
|
||||||
|
|
||||||
|
|
||||||
class TaskStatus(str, Enum):
|
class TaskStatus(str, Enum):
|
||||||
NOTSTARTED = "NOTSTARTED"
|
NOTSTARTED = "NOTSTARTED"
|
||||||
@@ -94,7 +94,13 @@ class BaseTask:
|
|||||||
def __init__(self, uuid: UUID, num_cmds: int) -> None:
|
def __init__(self, uuid: UUID, num_cmds: int) -> None:
|
||||||
# constructor
|
# constructor
|
||||||
self.uuid: UUID = uuid
|
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.procs: list[Command] = []
|
||||||
self.status = TaskStatus.NOTSTARTED
|
self.status = TaskStatus.NOTSTARTED
|
||||||
self.logs_lock = threading.Lock()
|
self.logs_lock = threading.Lock()
|
||||||
@@ -108,6 +114,10 @@ class BaseTask:
|
|||||||
self.status = TaskStatus.RUNNING
|
self.status = TaskStatus.RUNNING
|
||||||
try:
|
try:
|
||||||
self.run()
|
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:
|
except Exception as e:
|
||||||
# FIXME: fix exception handling here
|
# FIXME: fix exception handling here
|
||||||
traceback.print_exception(*sys.exc_info())
|
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 os
|
||||||
import shlex
|
import shlex
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Iterator
|
from typing import Iterator, Dict
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from ..dirs import specific_flake_dir
|
from ..dirs import clan_flakes_dir, specific_flake_dir
|
||||||
from ..nix import nix_build, nix_config, nix_shell
|
from ..nix import nix_build, nix_config, nix_eval, nix_shell
|
||||||
from ..task_manager import BaseTask, Command, create_task
|
from ..task_manager import BaseTask, Command, create_task
|
||||||
|
from ..types import validate_path
|
||||||
from .inspect import VmConfig, inspect_vm
|
from .inspect import VmConfig, inspect_vm
|
||||||
|
from ..errors import ClanError
|
||||||
|
from ..debug import repro_env_break
|
||||||
|
|
||||||
|
|
||||||
class BuildVmTask(BaseTask):
|
class BuildVmTask(BaseTask):
|
||||||
def __init__(self, uuid: UUID, vm: VmConfig) -> None:
|
def __init__(self, uuid: UUID, vm: VmConfig) -> None:
|
||||||
super().__init__(uuid, num_cmds=6)
|
super().__init__(uuid, num_cmds=7)
|
||||||
self.vm = vm
|
self.vm = vm
|
||||||
|
|
||||||
def get_vm_create_info(self, cmds: Iterator[Command]) -> dict:
|
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}")
|
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)
|
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:
|
def run(self) -> None:
|
||||||
cmds = self.commands()
|
cmds = self.commands()
|
||||||
|
|
||||||
@@ -47,99 +56,106 @@ class BuildVmTask(BaseTask):
|
|||||||
|
|
||||||
# TODO: We should get this from the vm argument
|
# TODO: We should get this from the vm argument
|
||||||
vm_config = self.get_vm_create_info(cmds)
|
vm_config = self.get_vm_create_info(cmds)
|
||||||
|
clan_name = self.get_clan_name(cmds)
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as tmpdir_:
|
self.log.debug(f"Building VM for clan name: {clan_name}")
|
||||||
tmpdir = Path(tmpdir_)
|
|
||||||
xchg_dir = tmpdir / "xchg"
|
|
||||||
xchg_dir.mkdir()
|
|
||||||
secrets_dir = tmpdir / "secrets"
|
|
||||||
secrets_dir.mkdir()
|
|
||||||
disk_img = f"{tmpdir_}/disk.img"
|
|
||||||
|
|
||||||
env = os.environ.copy()
|
flake_dir = clan_flakes_dir() / clan_name
|
||||||
env["CLAN_DIR"] = str(self.vm.flake_url)
|
validate_path(clan_flakes_dir(), flake_dir)
|
||||||
env["PYTHONPATH"] = str(
|
flake_dir.mkdir(exist_ok=True)
|
||||||
":".join(sys.path)
|
|
||||||
) # TODO do this in the clanCore module
|
|
||||||
env["SECRETS_DIR"] = str(secrets_dir)
|
|
||||||
|
|
||||||
cmd = next(cmds)
|
xchg_dir = flake_dir / "xchg"
|
||||||
if Path(self.vm.flake_url).is_dir():
|
xchg_dir.mkdir()
|
||||||
cmd.run(
|
secrets_dir = flake_dir / "secrets"
|
||||||
[vm_config["generateSecrets"]],
|
secrets_dir.mkdir()
|
||||||
env=env,
|
disk_img = f"{flake_dir}/disk.img"
|
||||||
)
|
|
||||||
else:
|
|
||||||
cmd.run(["echo", "won't generate secrets for non local clan"])
|
|
||||||
|
|
||||||
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(
|
cmd.run(
|
||||||
[vm_config["uploadSecrets"]],
|
[vm_config["generateSecrets"], clan_name],
|
||||||
env=env,
|
env=env,
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
self.log.warning("won't generate secrets for non local clan")
|
||||||
|
|
||||||
cmd = next(cmds)
|
cmd = next(cmds)
|
||||||
cmd.run(
|
cmd.run(
|
||||||
nix_shell(
|
[vm_config["uploadSecrets"]],
|
||||||
["qemu"],
|
env=env,
|
||||||
[
|
)
|
||||||
"qemu-img",
|
|
||||||
"create",
|
cmd = next(cmds)
|
||||||
"-f",
|
cmd.run(
|
||||||
"raw",
|
nix_shell(
|
||||||
disk_img,
|
["qemu"],
|
||||||
"1024M",
|
[
|
||||||
],
|
"qemu-img",
|
||||||
)
|
"create",
|
||||||
|
"-f",
|
||||||
|
"raw",
|
||||||
|
disk_img,
|
||||||
|
"1024M",
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
cmd = next(cmds)
|
cmd = next(cmds)
|
||||||
cmd.run(
|
cmd.run(
|
||||||
nix_shell(
|
nix_shell(
|
||||||
["e2fsprogs"],
|
["e2fsprogs"],
|
||||||
[
|
[
|
||||||
"mkfs.ext4",
|
"mkfs.ext4",
|
||||||
"-L",
|
"-L",
|
||||||
"nixos",
|
"nixos",
|
||||||
disk_img,
|
disk_img,
|
||||||
],
|
],
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
cmd = next(cmds)
|
cmd = next(cmds)
|
||||||
cmdline = [
|
cmdline = [
|
||||||
(Path(vm_config["toplevel"]) / "kernel-params").read_text(),
|
(Path(vm_config["toplevel"]) / "kernel-params").read_text(),
|
||||||
f'init={vm_config["toplevel"]}/init',
|
f'init={vm_config["toplevel"]}/init',
|
||||||
f'regInfo={vm_config["regInfo"]}/registration',
|
f'regInfo={vm_config["regInfo"]}/registration',
|
||||||
"console=ttyS0,115200n8",
|
"console=ttyS0,115200n8",
|
||||||
"console=tty0",
|
"console=tty0",
|
||||||
]
|
]
|
||||||
qemu_command = [
|
qemu_command = [
|
||||||
# fmt: off
|
# fmt: off
|
||||||
"qemu-kvm",
|
"qemu-kvm",
|
||||||
"-name", machine,
|
"-name", machine,
|
||||||
"-m", f'{vm_config["memorySize"]}M',
|
"-m", f'{vm_config["memorySize"]}M',
|
||||||
"-smp", str(vm_config["cores"]),
|
"-smp", str(vm_config["cores"]),
|
||||||
"-device", "virtio-rng-pci",
|
"-device", "virtio-rng-pci",
|
||||||
"-net", "nic,netdev=user.0,model=virtio", "-netdev", "user,id=user.0",
|
"-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", "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=shared",
|
||||||
"-virtfs", f"local,path={xchg_dir},security_model=none,mount_tag=xchg",
|
"-virtfs", f"local,path={xchg_dir},security_model=none,mount_tag=xchg",
|
||||||
"-virtfs", f"local,path={secrets_dir},security_model=none,mount_tag=secrets",
|
"-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',
|
"-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-blk-pci,bootindex=1,drive=drive1,serial=root",
|
||||||
"-device", "virtio-keyboard",
|
"-device", "virtio-keyboard",
|
||||||
"-usb",
|
"-usb",
|
||||||
"-device", "usb-tablet,bus=usb-bus.0",
|
"-device", "usb-tablet,bus=usb-bus.0",
|
||||||
"-kernel", f'{vm_config["toplevel"]}/kernel',
|
"-kernel", f'{vm_config["toplevel"]}/kernel',
|
||||||
"-initrd", vm_config["initrd"],
|
"-initrd", vm_config["initrd"],
|
||||||
"-append", " ".join(cmdline),
|
"-append", " ".join(cmdline),
|
||||||
# fmt: on
|
# fmt: on
|
||||||
]
|
]
|
||||||
if not self.vm.graphics:
|
if not self.vm.graphics:
|
||||||
qemu_command.append("-nographic")
|
qemu_command.append("-nographic")
|
||||||
print("$ " + shlex.join(qemu_command))
|
print("$ " + shlex.join(qemu_command))
|
||||||
cmd.run(nix_shell(["qemu"], qemu_command))
|
cmd.run(nix_shell(["qemu"], qemu_command))
|
||||||
|
|
||||||
|
|
||||||
def create_vm(vm: VmConfig) -> BuildVmTask:
|
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 ..dirs import clan_data_dir, clan_flakes_dir
|
||||||
from ..flakes.create import DEFAULT_URL
|
from ..flakes.create import DEFAULT_URL
|
||||||
|
from ..types import validate_path
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
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):
|
class ClanDataPath(BaseModel):
|
||||||
dest: Path
|
dest: Path
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ from ...config.machine import (
|
|||||||
schema_for_machine,
|
schema_for_machine,
|
||||||
set_config_for_machine,
|
set_config_for_machine,
|
||||||
)
|
)
|
||||||
from ...flakes.types import FlakeName
|
|
||||||
from ...machines.create import create_machine as _create_machine
|
from ...machines.create import create_machine as _create_machine
|
||||||
from ...machines.list import list_machines as _list_machines
|
from ...machines.list import list_machines as _list_machines
|
||||||
|
from ...types import FlakeName
|
||||||
from ..api_outputs import (
|
from ..api_outputs import (
|
||||||
ConfigResponse,
|
ConfigResponse,
|
||||||
Machine,
|
Machine,
|
||||||
|
|||||||
@@ -8,9 +8,11 @@
|
|||||||
, openssh
|
, openssh
|
||||||
, pytest
|
, pytest
|
||||||
, pytest-cov
|
, pytest-cov
|
||||||
|
, pytest-xdist
|
||||||
, pytest-subprocess
|
, pytest-subprocess
|
||||||
, pytest-parallel
|
|
||||||
, pytest-timeout
|
, pytest-timeout
|
||||||
|
, remote-pdb
|
||||||
|
, ipdb
|
||||||
, python3
|
, python3
|
||||||
, runCommand
|
, runCommand
|
||||||
, setuptools
|
, setuptools
|
||||||
@@ -45,8 +47,10 @@ let
|
|||||||
pytest
|
pytest
|
||||||
pytest-cov
|
pytest-cov
|
||||||
pytest-subprocess
|
pytest-subprocess
|
||||||
pytest-parallel
|
pytest-xdist
|
||||||
pytest-timeout
|
pytest-timeout
|
||||||
|
remote-pdb
|
||||||
|
ipdb
|
||||||
openssh
|
openssh
|
||||||
git
|
git
|
||||||
gnupg
|
gnupg
|
||||||
@@ -80,9 +84,7 @@ let
|
|||||||
source = runCommand "clan-cli-source" { } ''
|
source = runCommand "clan-cli-source" { } ''
|
||||||
cp -r ${./.} $out
|
cp -r ${./.} $out
|
||||||
chmod -R +w $out
|
chmod -R +w $out
|
||||||
rm $out/clan_cli/config/jsonschema
|
|
||||||
ln -s ${nixpkgs'} $out/clan_cli/nixpkgs
|
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
|
ln -s ${ui-assets} $out/clan_cli/webui/assets
|
||||||
'';
|
'';
|
||||||
nixpkgs' = runCommand "nixpkgs" { nativeBuildInputs = [ nix ]; } ''
|
nixpkgs' = runCommand "nixpkgs" { nativeBuildInputs = [ nix ]; } ''
|
||||||
|
|||||||
@@ -14,9 +14,14 @@ exclude = ["clan_cli.nixpkgs*"]
|
|||||||
[tool.setuptools.package-data]
|
[tool.setuptools.package-data]
|
||||||
clan_cli = [ "config/jsonschema/*", "webui/assets/**/*"]
|
clan_cli = [ "config/jsonschema/*", "webui/assets/**/*"]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = "tests"
|
||||||
faulthandler_timeout = 60
|
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"
|
norecursedirs = "tests/helpers"
|
||||||
markers = [ "impure" ]
|
markers = [ "impure" ]
|
||||||
|
|
||||||
|
|||||||
@@ -22,38 +22,41 @@ mkShell {
|
|||||||
];
|
];
|
||||||
|
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
tmp_path=$(realpath ./.direnv)
|
tmp_path=$(realpath ./.direnv)
|
||||||
|
|
||||||
repo_root=$(realpath .)
|
repo_root=$(realpath .)
|
||||||
mkdir -p "$tmp_path/python/${pythonWithDeps.sitePackages}"
|
mkdir -p "$tmp_path/python/${pythonWithDeps.sitePackages}"
|
||||||
|
|
||||||
# Install the package in editable mode
|
# Install the package in editable mode
|
||||||
# This allows executing `clan` from within the dev-shell using the current
|
# This allows executing `clan` from within the dev-shell using the current
|
||||||
# version of the code and its dependencies.
|
# version of the code and its dependencies.
|
||||||
${pythonWithDeps.interpreter} -m pip install \
|
${pythonWithDeps.interpreter} -m pip install \
|
||||||
--quiet \
|
--quiet \
|
||||||
--disable-pip-version-check \
|
--disable-pip-version-check \
|
||||||
--no-index \
|
--no-index \
|
||||||
--no-build-isolation \
|
--no-build-isolation \
|
||||||
--prefix "$tmp_path/python" \
|
--prefix "$tmp_path/python" \
|
||||||
--editable $repo_root
|
--editable $repo_root
|
||||||
|
|
||||||
rm -f clan_cli/nixpkgs clan_cli/webui/assets
|
rm -f clan_cli/nixpkgs clan_cli/webui/assets
|
||||||
ln -sf ${clan-cli.nixpkgs} clan_cli/nixpkgs
|
ln -sf ${clan-cli.nixpkgs} clan_cli/nixpkgs
|
||||||
ln -sf ${ui-assets} clan_cli/webui/assets
|
ln -sf ${ui-assets} clan_cli/webui/assets
|
||||||
|
|
||||||
export PATH="$tmp_path/python/bin:${checkScript}/bin:$PATH"
|
export PATH="$tmp_path/python/bin:${checkScript}/bin:$PATH"
|
||||||
export PYTHONPATH="$repo_root:$tmp_path/python/${pythonWithDeps.sitePackages}:"
|
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 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}"
|
export fish_complete_path="$tmp_path/share/fish/vendor_completions.d''${fish_complete_path:+:$fish_complete_path}"
|
||||||
mkdir -p \
|
mkdir -p \
|
||||||
$tmp_path/share/fish/vendor_completions.d \
|
$tmp_path/share/fish/vendor_completions.d \
|
||||||
$tmp_path/share/bash-completion/completions \
|
$tmp_path/share/bash-completion/completions \
|
||||||
$tmp_path/share/zsh/site-functions
|
$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 fish clan > $tmp_path/share/fish/vendor_completions.d/clan.fish
|
||||||
register-python-argcomplete --shell bash clan > $tmp_path/share/bash-completion/completions/clan
|
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 fileinput
|
||||||
|
import logging
|
||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -8,7 +9,9 @@ import pytest
|
|||||||
from root import CLAN_CORE
|
from root import CLAN_CORE
|
||||||
|
|
||||||
from clan_cli.dirs import nixpkgs_source
|
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.
|
# substitutes string sin a file.
|
||||||
@@ -28,73 +31,85 @@ def substitute(
|
|||||||
print(line, end="")
|
print(line, end="")
|
||||||
|
|
||||||
|
|
||||||
class TestFlake(NamedTuple):
|
class FlakeForTest(NamedTuple):
|
||||||
name: FlakeName
|
name: FlakeName
|
||||||
path: Path
|
path: Path
|
||||||
|
|
||||||
|
|
||||||
def create_flake(
|
def create_flake(
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
temporary_dir: Path,
|
||||||
flake_name: FlakeName,
|
flake_name: FlakeName,
|
||||||
clan_core_flake: Path | None = None,
|
clan_core_flake: Path | None = None,
|
||||||
machines: list[str] = [],
|
machines: list[str] = [],
|
||||||
remote: bool = False,
|
remote: bool = False,
|
||||||
) -> Iterator[TestFlake]:
|
) -> Iterator[FlakeForTest]:
|
||||||
"""
|
"""
|
||||||
Creates a flake with the given name and machines.
|
Creates a flake with the given name and machines.
|
||||||
The machine names map to the machines in ./test_machines
|
The machine names map to the machines in ./test_machines
|
||||||
"""
|
"""
|
||||||
template = Path(__file__).parent / flake_name
|
template = Path(__file__).parent / flake_name
|
||||||
|
|
||||||
# copy the template to a new temporary location
|
# copy the template to a new temporary location
|
||||||
with tempfile.TemporaryDirectory() as tmpdir_:
|
home = Path(temporary_dir)
|
||||||
home = Path(tmpdir_)
|
flake = home / ".local/state/clan/flake" / flake_name
|
||||||
flake = home / flake_name
|
shutil.copytree(template, flake)
|
||||||
shutil.copytree(template, flake)
|
|
||||||
# lookup the requested machines in ./test_machines and include them
|
# lookup the requested machines in ./test_machines and include them
|
||||||
if machines:
|
if machines:
|
||||||
(flake / "machines").mkdir(parents=True, exist_ok=True)
|
(flake / "machines").mkdir(parents=True, exist_ok=True)
|
||||||
for machine_name in machines:
|
for machine_name in machines:
|
||||||
machine_path = Path(__file__).parent / "machines" / machine_name
|
machine_path = Path(__file__).parent / "machines" / machine_name
|
||||||
shutil.copytree(machine_path, flake / "machines" / machine_name)
|
shutil.copytree(machine_path, flake / "machines" / machine_name)
|
||||||
substitute(flake / "machines" / machine_name / "default.nix", flake)
|
substitute(flake / "machines" / machine_name / "default.nix", flake)
|
||||||
# in the flake.nix file replace the string __CLAN_URL__ with the the clan flake
|
# in the flake.nix file replace the string __CLAN_URL__ with the the clan flake
|
||||||
# provided by get_test_flake_toplevel
|
# provided by get_test_flake_toplevel
|
||||||
flake_nix = flake / "flake.nix"
|
flake_nix = flake / "flake.nix"
|
||||||
# this is where we would install the sops key to, when updating
|
# this is where we would install the sops key to, when updating
|
||||||
substitute(flake_nix, clan_core_flake, flake)
|
substitute(flake_nix, clan_core_flake, flake)
|
||||||
if remote:
|
if remote:
|
||||||
with tempfile.TemporaryDirectory() as workdir:
|
with tempfile.TemporaryDirectory() as workdir:
|
||||||
monkeypatch.chdir(workdir)
|
monkeypatch.chdir(workdir)
|
||||||
monkeypatch.setenv("HOME", str(home))
|
|
||||||
yield TestFlake(flake_name, flake)
|
|
||||||
else:
|
|
||||||
monkeypatch.chdir(flake)
|
|
||||||
monkeypatch.setenv("HOME", str(home))
|
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
|
@pytest.fixture
|
||||||
def test_flake(monkeypatch: pytest.MonkeyPatch) -> Iterator[TestFlake]:
|
def test_flake(
|
||||||
yield from create_flake(monkeypatch, FlakeName("test_flake"))
|
monkeypatch: pytest.MonkeyPatch, temporary_home: Path
|
||||||
|
) -> Iterator[FlakeForTest]:
|
||||||
|
yield from create_flake(monkeypatch, temporary_home, FlakeName("test_flake"))
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def test_flake_with_core(monkeypatch: pytest.MonkeyPatch) -> Iterator[TestFlake]:
|
def test_flake_with_core(
|
||||||
if not (CLAN_CORE / "flake.nix").exists():
|
monkeypatch: pytest.MonkeyPatch, temporary_dir: Path
|
||||||
raise Exception(
|
) -> Iterator[FlakeForTest]:
|
||||||
"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]:
|
|
||||||
if not (CLAN_CORE / "flake.nix").exists():
|
if not (CLAN_CORE / "flake.nix").exists():
|
||||||
raise Exception(
|
raise Exception(
|
||||||
"clan-core flake not found. This test requires the clan-core flake to be present"
|
"clan-core flake not found. This test requires the clan-core flake to be present"
|
||||||
)
|
)
|
||||||
yield from create_flake(
|
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 argparse
|
||||||
|
import logging
|
||||||
|
import shlex
|
||||||
|
|
||||||
from clan_cli import create_parser
|
from clan_cli import create_parser
|
||||||
|
from clan_cli.custom_logger import get_caller
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Cli:
|
class Cli:
|
||||||
@@ -8,6 +13,9 @@ class Cli:
|
|||||||
self.parser = create_parser(prog="clan")
|
self.parser = create_parser(prog="clan")
|
||||||
|
|
||||||
def run(self, args: list[str]) -> argparse.Namespace:
|
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)
|
parsed = self.parser.parse_args(args)
|
||||||
if hasattr(parsed, "func"):
|
if hasattr(parsed, "func"):
|
||||||
parsed.func(parsed)
|
parsed.func(parsed)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -5,14 +6,20 @@ from typing import Iterator
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def temporary_dir() -> Iterator[Path]:
|
def temporary_home(monkeypatch: pytest.MonkeyPatch) -> Iterator[Path]:
|
||||||
if os.getenv("TEST_KEEP_TEMPORARY_DIR"):
|
env_dir = os.getenv("TEST_TEMPORARY_DIR")
|
||||||
temp_dir = tempfile.mkdtemp(prefix="pytest-")
|
if env_dir is not None:
|
||||||
path = Path(temp_dir)
|
path = Path(env_dir).resolve()
|
||||||
|
log.debug("Temp HOME directory: %s", str(path))
|
||||||
|
monkeypatch.setenv("HOME", str(path))
|
||||||
yield path
|
yield path
|
||||||
print("=========> Keeping temporary directory: ", path)
|
|
||||||
else:
|
else:
|
||||||
|
log.debug("TEST_TEMPORARY_DIR not set, using TemporaryDirectory")
|
||||||
with tempfile.TemporaryDirectory(prefix="pytest-") as dirpath:
|
with tempfile.TemporaryDirectory(prefix="pytest-") as dirpath:
|
||||||
|
monkeypatch.setenv("HOME", str(dirpath))
|
||||||
|
log.debug("Temp HOME directory: %s", str(dirpath))
|
||||||
yield Path(dirpath)
|
yield Path(dirpath)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from cli import Cli
|
|||||||
from clan_cli import config
|
from clan_cli import config
|
||||||
from clan_cli.config import parsing
|
from clan_cli.config import parsing
|
||||||
from clan_cli.errors import ClanError
|
from clan_cli.errors import ClanError
|
||||||
|
from fixtures_flakes import FlakeForTest
|
||||||
|
|
||||||
example_options = f"{Path(config.__file__).parent}/jsonschema/options.json"
|
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(
|
def test_set_some_option(
|
||||||
args: list[str],
|
args: list[str],
|
||||||
expected: dict[str, Any],
|
expected: dict[str, Any],
|
||||||
test_flake: Path,
|
test_flake: FlakeForTest,
|
||||||
) -> None:
|
) -> None:
|
||||||
# create temporary file for out_file
|
# create temporary file for out_file
|
||||||
with tempfile.NamedTemporaryFile() as out_file:
|
with tempfile.NamedTemporaryFile() as out_file:
|
||||||
@@ -46,24 +47,24 @@ def test_set_some_option(
|
|||||||
out_file.name,
|
out_file.name,
|
||||||
]
|
]
|
||||||
+ args
|
+ args
|
||||||
|
+ [test_flake.name]
|
||||||
)
|
)
|
||||||
json_out = json.loads(open(out_file.name).read())
|
json_out = json.loads(open(out_file.name).read())
|
||||||
assert json_out == expected
|
assert json_out == expected
|
||||||
|
|
||||||
|
|
||||||
def test_configure_machine(
|
def test_configure_machine(
|
||||||
test_flake: Path,
|
test_flake: FlakeForTest,
|
||||||
temporary_dir: Path,
|
temporary_home: Path,
|
||||||
capsys: pytest.CaptureFixture,
|
capsys: pytest.CaptureFixture,
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
) -> None:
|
) -> None:
|
||||||
monkeypatch.setenv("HOME", str(temporary_dir))
|
|
||||||
cli = Cli()
|
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
|
# clear the output buffer
|
||||||
capsys.readouterr()
|
capsys.readouterr()
|
||||||
# read a option value
|
# 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
|
# read the output
|
||||||
assert capsys.readouterr().out == "true\n"
|
assert capsys.readouterr().out == "true\n"
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import pytest
|
|||||||
from api import TestClient
|
from api import TestClient
|
||||||
from cli import Cli
|
from cli import Cli
|
||||||
|
|
||||||
|
from clan_cli.dirs import clan_flakes_dir
|
||||||
|
from clan_cli.flakes.create import DEFAULT_URL
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def cli() -> Cli:
|
def cli() -> Cli:
|
||||||
@@ -14,15 +17,16 @@ def cli() -> Cli:
|
|||||||
|
|
||||||
@pytest.mark.impure
|
@pytest.mark.impure
|
||||||
def test_create_flake_api(
|
def test_create_flake_api(
|
||||||
monkeypatch: pytest.MonkeyPatch, api: TestClient, temporary_dir: Path
|
monkeypatch: pytest.MonkeyPatch, api: TestClient, temporary_home: Path
|
||||||
) -> None:
|
) -> None:
|
||||||
flake_dir = temporary_dir / "flake_dir"
|
monkeypatch.chdir(clan_flakes_dir())
|
||||||
flake_dir_str = str(flake_dir.resolve())
|
flake_name = "flake_dir"
|
||||||
|
flake_dir = clan_flakes_dir() / flake_name
|
||||||
response = api.post(
|
response = api.post(
|
||||||
"/api/flake/create",
|
"/api/flake/create",
|
||||||
json=dict(
|
json=dict(
|
||||||
dest=flake_dir_str,
|
dest=str(flake_dir),
|
||||||
url="git+https://git.clan.lol/clan/clan-core#new-clan",
|
url=str(DEFAULT_URL),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -34,19 +38,21 @@ def test_create_flake_api(
|
|||||||
@pytest.mark.impure
|
@pytest.mark.impure
|
||||||
def test_create_flake(
|
def test_create_flake(
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
temporary_dir: Path,
|
|
||||||
capsys: pytest.CaptureFixture,
|
capsys: pytest.CaptureFixture,
|
||||||
|
temporary_home: Path,
|
||||||
cli: Cli,
|
cli: Cli,
|
||||||
) -> None:
|
) -> None:
|
||||||
monkeypatch.chdir(temporary_dir)
|
monkeypatch.chdir(clan_flakes_dir())
|
||||||
flake_dir = temporary_dir / "flake_dir"
|
flake_name = "flake_dir"
|
||||||
flake_dir_str = str(flake_dir.resolve())
|
flake_dir = clan_flakes_dir() / flake_name
|
||||||
cli.run(["flake", "create", flake_dir_str])
|
|
||||||
|
cli.run(["flakes", "create", flake_name])
|
||||||
assert (flake_dir / ".clan-flake").exists()
|
assert (flake_dir / ".clan-flake").exists()
|
||||||
monkeypatch.chdir(flake_dir)
|
monkeypatch.chdir(flake_dir)
|
||||||
cli.run(["machines", "create", "machine1"])
|
cli.run(["machines", "create", "machine1", flake_name])
|
||||||
capsys.readouterr() # flush cache
|
capsys.readouterr() # flush cache
|
||||||
cli.run(["machines", "list"])
|
|
||||||
|
cli.run(["machines", "list", flake_name])
|
||||||
assert "machine1" in capsys.readouterr().out
|
assert "machine1" in capsys.readouterr().out
|
||||||
flake_show = subprocess.run(
|
flake_show = subprocess.run(
|
||||||
["nix", "flake", "show", "--json"],
|
["nix", "flake", "show", "--json"],
|
||||||
@@ -61,6 +67,17 @@ def test_create_flake(
|
|||||||
pytest.fail("nixosConfigurations.machine1 not found in flake outputs")
|
pytest.fail("nixosConfigurations.machine1 not found in flake outputs")
|
||||||
# configure machine1
|
# configure machine1
|
||||||
capsys.readouterr()
|
capsys.readouterr()
|
||||||
cli.run(["config", "--machine", "machine1", "services.openssh.enable"])
|
cli.run(
|
||||||
|
["config", "--machine", "machine1", "services.openssh.enable", "", flake_name]
|
||||||
|
)
|
||||||
capsys.readouterr()
|
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
|
let
|
||||||
clan = clan-core.lib.buildClan {
|
clan = clan-core.lib.buildClan {
|
||||||
directory = self;
|
directory = self;
|
||||||
|
clanName = "test_with_core_clan";
|
||||||
machines = {
|
machines = {
|
||||||
vm1 = { lib, ... }: {
|
vm1 = { lib, ... }: {
|
||||||
clan.networking.deploymentAddress = "__CLAN_DEPLOYMENT_ADDRESS__";
|
clan.networking.deploymentAddress = "__CLAN_DEPLOYMENT_ADDRESS__";
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
let
|
let
|
||||||
clan = clan-core.lib.buildClan {
|
clan = clan-core.lib.buildClan {
|
||||||
directory = self;
|
directory = self;
|
||||||
|
clanName = "test_with_core_and_pass_clan";
|
||||||
machines = {
|
machines = {
|
||||||
vm1 = { lib, ... }: {
|
vm1 = { lib, ... }: {
|
||||||
clan.networking.deploymentAddress = "__CLAN_DEPLOYMENT_ADDRESS__";
|
clan.networking.deploymentAddress = "__CLAN_DEPLOYMENT_ADDRESS__";
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
let
|
let
|
||||||
clan = clan-core.lib.buildClan {
|
clan = clan-core.lib.buildClan {
|
||||||
directory = self;
|
directory = self;
|
||||||
|
clanName = "core_dynamic_machine_clan";
|
||||||
machines =
|
machines =
|
||||||
let
|
let
|
||||||
machineModules = builtins.readDir (self + "/machines");
|
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
|
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")
|
schema = machine.schema_for_machine(test_flake.name, "machine1")
|
||||||
assert "properties" in schema
|
assert "properties" in schema
|
||||||
|
|||||||
@@ -1,30 +1,33 @@
|
|||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from pathlib import Path
|
|
||||||
from typing import TYPE_CHECKING, Iterator
|
from typing import TYPE_CHECKING, Iterator
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from cli import Cli
|
from cli import Cli
|
||||||
|
from fixtures_flakes import FlakeForTest
|
||||||
|
|
||||||
from clan_cli.errors import ClanError
|
from clan_cli.errors import ClanError
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from age_keys import KeyPair
|
from age_keys import KeyPair
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def _test_identities(
|
def _test_identities(
|
||||||
what: str,
|
what: str,
|
||||||
test_flake: Path,
|
test_flake: FlakeForTest,
|
||||||
capsys: pytest.CaptureFixture,
|
capsys: pytest.CaptureFixture,
|
||||||
age_keys: list["KeyPair"],
|
age_keys: list["KeyPair"],
|
||||||
) -> None:
|
) -> None:
|
||||||
cli = Cli()
|
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()
|
assert (sops_folder / what / "foo" / "key.json").exists()
|
||||||
with pytest.raises(ClanError):
|
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(
|
cli.run(
|
||||||
[
|
[
|
||||||
@@ -34,73 +37,80 @@ def _test_identities(
|
|||||||
"-f",
|
"-f",
|
||||||
"foo",
|
"foo",
|
||||||
age_keys[0].privkey,
|
age_keys[0].privkey,
|
||||||
|
test_flake.name,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
capsys.readouterr() # empty the buffer
|
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
|
out = capsys.readouterr() # empty the buffer
|
||||||
assert age_keys[0].pubkey in out.out
|
assert age_keys[0].pubkey in out.out
|
||||||
|
|
||||||
capsys.readouterr() # empty the buffer
|
capsys.readouterr() # empty the buffer
|
||||||
cli.run(["secrets", what, "list"])
|
cli.run(["secrets", what, "list", test_flake.name])
|
||||||
out = capsys.readouterr() # empty the buffer
|
out = capsys.readouterr() # empty the buffer
|
||||||
assert "foo" in out.out
|
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()
|
assert not (sops_folder / what / "foo" / "key.json").exists()
|
||||||
|
|
||||||
with pytest.raises(ClanError): # already removed
|
with pytest.raises(ClanError): # already removed
|
||||||
cli.run(["secrets", what, "remove", "foo"])
|
cli.run(["secrets", what, "remove", "foo", test_flake.name])
|
||||||
|
|
||||||
capsys.readouterr()
|
capsys.readouterr()
|
||||||
cli.run(["secrets", what, "list"])
|
cli.run(["secrets", what, "list", test_flake.name])
|
||||||
out = capsys.readouterr()
|
out = capsys.readouterr()
|
||||||
assert "foo" not in out.out
|
assert "foo" not in out.out
|
||||||
|
|
||||||
|
|
||||||
def test_users(
|
def test_users(
|
||||||
test_flake: Path, capsys: pytest.CaptureFixture, age_keys: list["KeyPair"]
|
test_flake: FlakeForTest, capsys: pytest.CaptureFixture, age_keys: list["KeyPair"]
|
||||||
) -> None:
|
) -> None:
|
||||||
_test_identities("users", test_flake, capsys, age_keys)
|
_test_identities("users", test_flake, capsys, age_keys)
|
||||||
|
|
||||||
|
|
||||||
def test_machines(
|
def test_machines(
|
||||||
test_flake: Path, capsys: pytest.CaptureFixture, age_keys: list["KeyPair"]
|
test_flake: FlakeForTest, capsys: pytest.CaptureFixture, age_keys: list["KeyPair"]
|
||||||
) -> None:
|
) -> None:
|
||||||
_test_identities("machines", test_flake, capsys, age_keys)
|
_test_identities("machines", test_flake, capsys, age_keys)
|
||||||
|
|
||||||
|
|
||||||
def test_groups(
|
def test_groups(
|
||||||
test_flake: Path, capsys: pytest.CaptureFixture, age_keys: list["KeyPair"]
|
test_flake: FlakeForTest, capsys: pytest.CaptureFixture, age_keys: list["KeyPair"]
|
||||||
) -> None:
|
) -> None:
|
||||||
cli = Cli()
|
cli = Cli()
|
||||||
capsys.readouterr() # empty the buffer
|
capsys.readouterr() # empty the buffer
|
||||||
cli.run(["secrets", "groups", "list"])
|
cli.run(["secrets", "groups", "list", test_flake.name])
|
||||||
assert capsys.readouterr().out == ""
|
assert capsys.readouterr().out == ""
|
||||||
|
|
||||||
with pytest.raises(ClanError): # machine does not exist yet
|
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
|
with pytest.raises(ClanError): # user does not exist yet
|
||||||
cli.run(["secrets", "groups", "add-user", "groupb1", "user1"])
|
cli.run(["secrets", "groups", "add-user", "groupb1", "user1", test_flake.name])
|
||||||
cli.run(["secrets", "machines", "add", "machine1", age_keys[0].pubkey])
|
cli.run(
|
||||||
cli.run(["secrets", "groups", "add-machine", "group1", "machine1"])
|
["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?
|
# 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", "users", "add", "user1", age_keys[0].pubkey, test_flake.name])
|
||||||
cli.run(["secrets", "groups", "add-user", "group1", "user1"])
|
cli.run(["secrets", "groups", "add-user", "group1", "user1", test_flake.name])
|
||||||
|
|
||||||
capsys.readouterr() # empty the buffer
|
capsys.readouterr() # empty the buffer
|
||||||
cli.run(["secrets", "groups", "list"])
|
cli.run(["secrets", "groups", "list", test_flake.name])
|
||||||
out = capsys.readouterr().out
|
out = capsys.readouterr().out
|
||||||
assert "user1" in out
|
assert "user1" in out
|
||||||
assert "machine1" in out
|
assert "machine1" in out
|
||||||
|
|
||||||
cli.run(["secrets", "groups", "remove-user", "group1", "user1"])
|
cli.run(["secrets", "groups", "remove-user", "group1", "user1", test_flake.name])
|
||||||
cli.run(["secrets", "groups", "remove-machine", "group1", "machine1"])
|
cli.run(
|
||||||
groups = os.listdir(test_flake / "sops" / "groups")
|
["secrets", "groups", "remove-machine", "group1", "machine1", test_flake.name]
|
||||||
|
)
|
||||||
|
groups = os.listdir(test_flake.path / "sops" / "groups")
|
||||||
assert len(groups) == 0
|
assert len(groups) == 0
|
||||||
|
|
||||||
|
|
||||||
@@ -117,104 +127,114 @@ def use_key(key: str, monkeypatch: pytest.MonkeyPatch) -> Iterator[None]:
|
|||||||
|
|
||||||
|
|
||||||
def test_secrets(
|
def test_secrets(
|
||||||
test_flake: Path,
|
test_flake: FlakeForTest,
|
||||||
capsys: pytest.CaptureFixture,
|
capsys: pytest.CaptureFixture,
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
age_keys: list["KeyPair"],
|
age_keys: list["KeyPair"],
|
||||||
) -> None:
|
) -> None:
|
||||||
cli = Cli()
|
cli = Cli()
|
||||||
capsys.readouterr() # empty the buffer
|
capsys.readouterr() # empty the buffer
|
||||||
cli.run(["secrets", "list"])
|
cli.run(["secrets", "list", test_flake.name])
|
||||||
assert capsys.readouterr().out == ""
|
assert capsys.readouterr().out == ""
|
||||||
|
|
||||||
monkeypatch.setenv("SOPS_NIX_SECRET", "foo")
|
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"])
|
cli.run(["secrets", "key", "generate"])
|
||||||
capsys.readouterr() # empty the buffer
|
capsys.readouterr() # empty the buffer
|
||||||
cli.run(["secrets", "key", "show"])
|
cli.run(["secrets", "key", "show"])
|
||||||
key = capsys.readouterr().out
|
key = capsys.readouterr().out
|
||||||
assert key.startswith("age1")
|
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
|
with pytest.raises(ClanError): # does not exist yet
|
||||||
cli.run(["secrets", "get", "nonexisting"])
|
cli.run(["secrets", "get", "nonexisting", test_flake.name])
|
||||||
cli.run(["secrets", "set", "initialkey"])
|
cli.run(["secrets", "set", "initialkey", test_flake.name])
|
||||||
capsys.readouterr()
|
capsys.readouterr()
|
||||||
cli.run(["secrets", "get", "initialkey"])
|
cli.run(["secrets", "get", "initialkey", test_flake.name])
|
||||||
assert capsys.readouterr().out == "foo"
|
assert capsys.readouterr().out == "foo"
|
||||||
capsys.readouterr()
|
capsys.readouterr()
|
||||||
cli.run(["secrets", "users", "list"])
|
cli.run(["secrets", "users", "list", test_flake.name])
|
||||||
users = capsys.readouterr().out.rstrip().split("\n")
|
users = capsys.readouterr().out.rstrip().split("\n")
|
||||||
assert len(users) == 1, f"users: {users}"
|
assert len(users) == 1, f"users: {users}"
|
||||||
owner = users[0]
|
owner = users[0]
|
||||||
|
|
||||||
monkeypatch.setenv("EDITOR", "cat")
|
monkeypatch.setenv("EDITOR", "cat")
|
||||||
cli.run(["secrets", "set", "--edit", "initialkey"])
|
cli.run(["secrets", "set", "--edit", "initialkey", test_flake.name])
|
||||||
monkeypatch.delenv("EDITOR")
|
monkeypatch.delenv("EDITOR")
|
||||||
|
|
||||||
cli.run(["secrets", "rename", "initialkey", "key"])
|
cli.run(["secrets", "rename", "initialkey", "key", test_flake.name])
|
||||||
|
|
||||||
capsys.readouterr() # empty the buffer
|
capsys.readouterr() # empty the buffer
|
||||||
cli.run(["secrets", "list"])
|
cli.run(["secrets", "list", test_flake.name])
|
||||||
assert capsys.readouterr().out == "key\n"
|
assert capsys.readouterr().out == "key\n"
|
||||||
|
|
||||||
cli.run(["secrets", "machines", "add", "machine1", age_keys[0].pubkey])
|
cli.run(
|
||||||
cli.run(["secrets", "machines", "add-secret", "machine1", "key"])
|
["secrets", "machines", "add", "machine1", age_keys[0].pubkey, test_flake.name]
|
||||||
|
)
|
||||||
|
cli.run(["secrets", "machines", "add-secret", "machine1", "key", test_flake.name])
|
||||||
capsys.readouterr()
|
capsys.readouterr()
|
||||||
cli.run(["secrets", "machines", "list"])
|
cli.run(["secrets", "machines", "list", test_flake.name])
|
||||||
assert capsys.readouterr().out == "machine1\n"
|
assert capsys.readouterr().out == "machine1\n"
|
||||||
|
|
||||||
with use_key(age_keys[0].privkey, monkeypatch):
|
with use_key(age_keys[0].privkey, monkeypatch):
|
||||||
capsys.readouterr()
|
capsys.readouterr()
|
||||||
cli.run(["secrets", "get", "key"])
|
cli.run(["secrets", "get", "key", test_flake.name])
|
||||||
|
|
||||||
assert capsys.readouterr().out == "foo"
|
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", "user1", age_keys[1].pubkey, test_flake.name])
|
||||||
cli.run(["secrets", "users", "add-secret", "user1", "key"])
|
cli.run(["secrets", "users", "add-secret", "user1", "key", test_flake.name])
|
||||||
capsys.readouterr()
|
capsys.readouterr()
|
||||||
with use_key(age_keys[1].privkey, monkeypatch):
|
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"
|
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
|
with pytest.raises(ClanError): # does not exist yet
|
||||||
cli.run(["secrets", "groups", "add-secret", "admin-group", "key"])
|
cli.run(
|
||||||
cli.run(["secrets", "groups", "add-user", "admin-group", "user1"])
|
["secrets", "groups", "add-secret", "admin-group", "key", test_flake.name]
|
||||||
cli.run(["secrets", "groups", "add-user", "admin-group", owner])
|
)
|
||||||
cli.run(["secrets", "groups", "add-secret", "admin-group", "key"])
|
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
|
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):
|
with use_key(age_keys[1].privkey, monkeypatch):
|
||||||
capsys.readouterr()
|
capsys.readouterr()
|
||||||
cli.run(["secrets", "get", "key"])
|
cli.run(["secrets", "get", "key", test_flake.name])
|
||||||
assert capsys.readouterr().out == "foo"
|
assert capsys.readouterr().out == "foo"
|
||||||
|
|
||||||
# extend group will update secrets
|
# extend group will update secrets
|
||||||
cli.run(["secrets", "users", "add", "user2", age_keys[2].pubkey])
|
cli.run(["secrets", "users", "add", "user2", age_keys[2].pubkey, test_flake.name])
|
||||||
cli.run(["secrets", "groups", "add-user", "admin-group", "user2"])
|
cli.run(["secrets", "groups", "add-user", "admin-group", "user2", test_flake.name])
|
||||||
|
|
||||||
with use_key(age_keys[2].privkey, monkeypatch): # user2
|
with use_key(age_keys[2].privkey, monkeypatch): # user2
|
||||||
capsys.readouterr()
|
capsys.readouterr()
|
||||||
cli.run(["secrets", "get", "key"])
|
cli.run(["secrets", "get", "key", test_flake.name])
|
||||||
assert capsys.readouterr().out == "foo"
|
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):
|
with pytest.raises(ClanError), use_key(age_keys[2].privkey, monkeypatch):
|
||||||
# user2 is not in the group anymore
|
# user2 is not in the group anymore
|
||||||
capsys.readouterr()
|
capsys.readouterr()
|
||||||
cli.run(["secrets", "get", "key"])
|
cli.run(["secrets", "get", "key", test_flake.name])
|
||||||
print(capsys.readouterr().out)
|
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", "key", test_flake.name])
|
||||||
cli.run(["secrets", "remove", "key2"])
|
cli.run(["secrets", "remove", "key2", test_flake.name])
|
||||||
|
|
||||||
capsys.readouterr() # empty the buffer
|
capsys.readouterr() # empty the buffer
|
||||||
cli.run(["secrets", "list"])
|
cli.run(["secrets", "list", test_flake.name])
|
||||||
assert capsys.readouterr().out == ""
|
assert capsys.readouterr().out == ""
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from typing import TYPE_CHECKING
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from cli import Cli
|
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.machines.facts import machine_get_fact
|
||||||
from clan_cli.secrets.folders import sops_secrets_folder
|
from clan_cli.secrets.folders import sops_secrets_folder
|
||||||
@@ -15,7 +15,7 @@ if TYPE_CHECKING:
|
|||||||
@pytest.mark.impure
|
@pytest.mark.impure
|
||||||
def test_generate_secret(
|
def test_generate_secret(
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
test_flake_with_core: TestFlake,
|
test_flake_with_core: FlakeForTest,
|
||||||
age_keys: list["KeyPair"],
|
age_keys: list["KeyPair"],
|
||||||
) -> None:
|
) -> None:
|
||||||
monkeypatch.chdir(test_flake_with_core.path)
|
monkeypatch.chdir(test_flake_with_core.path)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from cli import Cli
|
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.machines.facts import machine_get_fact
|
||||||
from clan_cli.nix import nix_shell
|
from clan_cli.nix import nix_shell
|
||||||
@@ -13,7 +13,7 @@ from clan_cli.ssh import HostGroup
|
|||||||
@pytest.mark.impure
|
@pytest.mark.impure
|
||||||
def test_upload_secret(
|
def test_upload_secret(
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
test_flake_with_core_and_pass: TestFlake,
|
test_flake_with_core_and_pass: FlakeForTest,
|
||||||
temporary_dir: Path,
|
temporary_dir: Path,
|
||||||
host_group: HostGroup,
|
host_group: HostGroup,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
@@ -5,20 +5,23 @@ from typing import TYPE_CHECKING, Iterator
|
|||||||
import pytest
|
import pytest
|
||||||
from api import TestClient
|
from api import TestClient
|
||||||
from cli import Cli
|
from cli import Cli
|
||||||
from fixtures_flakes import TestFlake, create_flake
|
from fixtures_flakes import FlakeForTest, create_flake
|
||||||
from httpx import SyncByteStream
|
from httpx import SyncByteStream
|
||||||
from root import CLAN_CORE
|
from root import CLAN_CORE
|
||||||
|
|
||||||
from clan_cli.flakes.types import FlakeName
|
from clan_cli.types import FlakeName
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from age_keys import KeyPair
|
from age_keys import KeyPair
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@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(
|
yield from create_flake(
|
||||||
monkeypatch,
|
monkeypatch,
|
||||||
|
temporary_home,
|
||||||
FlakeName("test_flake_with_core_dynamic_machines"),
|
FlakeName("test_flake_with_core_dynamic_machines"),
|
||||||
CLAN_CORE,
|
CLAN_CORE,
|
||||||
machines=["vm_with_secrets"],
|
machines=["vm_with_secrets"],
|
||||||
@@ -27,10 +30,11 @@ def flake_with_vm_with_secrets(monkeypatch: pytest.MonkeyPatch) -> Iterator[Test
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def remote_flake_with_vm_without_secrets(
|
def remote_flake_with_vm_without_secrets(
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch, temporary_home: Path
|
||||||
) -> Iterator[TestFlake]:
|
) -> Iterator[FlakeForTest]:
|
||||||
yield from create_flake(
|
yield from create_flake(
|
||||||
monkeypatch,
|
monkeypatch,
|
||||||
|
temporary_home,
|
||||||
FlakeName("test_flake_with_core_dynamic_machines"),
|
FlakeName("test_flake_with_core_dynamic_machines"),
|
||||||
CLAN_CORE,
|
CLAN_CORE,
|
||||||
machines=["vm_without_secrets"],
|
machines=["vm_without_secrets"],
|
||||||
@@ -41,11 +45,12 @@ def remote_flake_with_vm_without_secrets(
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def create_user_with_age_key(
|
def create_user_with_age_key(
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
test_flake: FlakeForTest,
|
||||||
age_keys: list["KeyPair"],
|
age_keys: list["KeyPair"],
|
||||||
) -> None:
|
) -> None:
|
||||||
monkeypatch.setenv("SOPS_AGE_KEY", age_keys[0].privkey)
|
monkeypatch.setenv("SOPS_AGE_KEY", age_keys[0].privkey)
|
||||||
cli = Cli()
|
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:
|
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(
|
def test_create_local(
|
||||||
api: TestClient,
|
api: TestClient,
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
flake_with_vm_with_secrets: Path,
|
flake_with_vm_with_secrets: FlakeForTest,
|
||||||
create_user_with_age_key: None,
|
create_user_with_age_key: None,
|
||||||
) -> 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")
|
@pytest.mark.skipif(not os.path.exists("/dev/kvm"), reason="Requires KVM")
|
||||||
@@ -102,8 +107,8 @@ def test_create_local(
|
|||||||
def test_create_remote(
|
def test_create_remote(
|
||||||
api: TestClient,
|
api: TestClient,
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
remote_flake_with_vm_without_secrets: Path,
|
remote_flake_with_vm_without_secrets: FlakeForTest,
|
||||||
) -> None:
|
) -> None:
|
||||||
generic_create_vm_test(
|
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,
|
"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/*"]
|
"ignorePatterns": ["**/src/api/*"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ set -xeuo pipefail
|
|||||||
# GITEA_TOKEN
|
# GITEA_TOKEN
|
||||||
if [[ -z "${GITEA_TOKEN:-}" ]]; then
|
if [[ -z "${GITEA_TOKEN:-}" ]]; then
|
||||||
echo "GITEA_TOKEN is not set"
|
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
|
exit 1
|
||||||
fi
|
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 .
|
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"))
|
NAR_HASH=$(nix-prefetch-url --unpack file://<(cat "$tmpdir/assets.tar.gz"))
|
||||||
|
|
||||||
|
owner=Luis
|
||||||
url="https://git.clan.lol/api/packages/clan/generic/ui/$NAR_HASH/assets.tar.gz"
|
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
|
set +x
|
||||||
curl --upload-file "$tmpdir/assets.tar.gz" -X PUT "$url?token=$GITEA_TOKEN"
|
curl --upload-file "$tmpdir/assets.tar.gz" -X PUT "$url?token=$GITEA_TOKEN"
|
||||||
set -x
|
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 { tw } from "@/utils/tailwind";
|
||||||
import MenuIcon from "@mui/icons-material/Menu";
|
import MenuIcon from "@mui/icons-material/Menu";
|
||||||
import {
|
import {
|
||||||
Button,
|
|
||||||
CssBaseline,
|
CssBaseline,
|
||||||
IconButton,
|
IconButton,
|
||||||
ThemeProvider,
|
ThemeProvider,
|
||||||
useMediaQuery,
|
useMediaQuery
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { StyledEngineProvider } from "@mui/material/styles";
|
import { StyledEngineProvider } from "@mui/material/styles";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
@@ -62,9 +61,7 @@ export default function RootLayout({
|
|||||||
<AppContext.Consumer>
|
<AppContext.Consumer>
|
||||||
{(appState) => {
|
{(appState) => {
|
||||||
const showSidebarDerived = Boolean(
|
const showSidebarDerived = Boolean(
|
||||||
showSidebar &&
|
showSidebar && !appState.isLoading,
|
||||||
!appState.isLoading &&
|
|
||||||
appState.data.isJoined,
|
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -86,9 +83,7 @@ export default function RootLayout({
|
|||||||
hidden={true}
|
hidden={true}
|
||||||
onClick={() => setShowSidebar((c) => !c)}
|
onClick={() => setShowSidebar((c) => !c)}
|
||||||
>
|
>
|
||||||
{!showSidebar && appState.data.isJoined && (
|
{!showSidebar && <MenuIcon />}
|
||||||
<MenuIcon />
|
|
||||||
)}
|
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-1 block w-full bg-fixed text-center font-semibold dark:invert lg:hidden">
|
<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="px-1">
|
||||||
<div className="relative flex h-full flex-1 flex-col">
|
<div className="relative flex h-full flex-1 flex-col">
|
||||||
<main>
|
<main>{children}</main>
|
||||||
<Button
|
|
||||||
fullWidth
|
|
||||||
onClick={() => {
|
|
||||||
appState.setAppState((s) => ({
|
|
||||||
...s,
|
|
||||||
isJoined: !s.isJoined,
|
|
||||||
}));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Toggle Joined
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{children}
|
|
||||||
</main>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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";
|
"use client";
|
||||||
import { NetworkOverview } from "@/components/dashboard/NetworkOverview";
|
|
||||||
import { RecentActivity } from "@/components/dashboard/activity";
|
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 { useAppState } from "@/components/hooks/useAppContext";
|
||||||
import { LoadingOverlay } from "@/components/join/loadingOverlay";
|
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() {
|
export default function Dashboard() {
|
||||||
const { data, isLoading } = useAppState();
|
const { isLoading } = useAppState();
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="grid h-full place-items-center">
|
<div className="grid h-full place-items-center">
|
||||||
@@ -48,26 +17,13 @@ export default function Dashboard() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
} else {
|
||||||
if (!data.isJoined) {
|
|
||||||
return <JoinPrequel />;
|
|
||||||
}
|
|
||||||
if (data.isJoined) {
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full">
|
<div className="flex w-full">
|
||||||
<div className="grid w-full grid-flow-row grid-cols-3 gap-4">
|
<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">
|
<div className="row-span-2">
|
||||||
<RecentActivity />
|
<RecentActivity />
|
||||||
</div>
|
</div>
|
||||||
<QuickActions />
|
|
||||||
<Notifications />
|
|
||||||
<TaskQueue />
|
|
||||||
</div>
|
</div>
|
||||||
</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";
|
import { useAppState } from "./hooks/useAppContext";
|
||||||
|
|
||||||
export default function Background() {
|
export default function Background() {
|
||||||
const { data, isLoading } = useAppState();
|
const { isLoading } = useAppState();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<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"
|
"fixed -z-10 h-[100vh] w-[100vw] overflow-hidden opacity-10 blur-md dark:opacity-40"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{(isLoading || !data.isJoined) && (
|
{isLoading && (
|
||||||
<>
|
<>
|
||||||
<Image
|
<Image
|
||||||
className="dark:hidden"
|
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";
|
import { KeyedMutator } from "swr";
|
||||||
|
|
||||||
type AppContextType = {
|
type AppContextType = {
|
||||||
// data: AxiosResponse<{}, any> | undefined;
|
|
||||||
data: AppState;
|
data: AppState;
|
||||||
|
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
error: AxiosError<any> | undefined;
|
error: AxiosError<any> | undefined;
|
||||||
|
|
||||||
setAppState: Dispatch<SetStateAction<AppState>>;
|
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);
|
export const AppContext = createContext<AppContextType>({} as AppContextType);
|
||||||
|
|
||||||
type AppState = {
|
type AppState = {};
|
||||||
isJoined?: boolean;
|
|
||||||
clanName?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface AppContextProviderProps {
|
interface AppContextProviderProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
export const WithAppState = (props: AppContextProviderProps) => {
|
export const WithAppState = (props: AppContextProviderProps) => {
|
||||||
const { children } = props;
|
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 (
|
return (
|
||||||
<AppContext.Provider
|
<AppContext.Provider
|
||||||
@@ -49,8 +41,6 @@ export const WithAppState = (props: AppContextProviderProps) => {
|
|||||||
setAppState,
|
setAppState,
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
swrKey,
|
|
||||||
mutate,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{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 { ReactNode } from "react";
|
||||||
|
|
||||||
import { tw } from "@/utils/tailwind";
|
import { tw } from "@/utils/tailwind";
|
||||||
import AppsIcon from "@mui/icons-material/Apps";
|
import AssignmentIndIcon from "@mui/icons-material/AssignmentInd";
|
||||||
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 Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import WysiwygIcon from "@mui/icons-material/Wysiwyg";
|
||||||
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
|
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
|
||||||
|
|
||||||
type MenuEntry = {
|
type MenuEntry = {
|
||||||
@@ -32,39 +27,15 @@ type MenuEntry = {
|
|||||||
|
|
||||||
const menuEntries: MenuEntry[] = [
|
const menuEntries: MenuEntry[] = [
|
||||||
{
|
{
|
||||||
icon: <DashboardIcon />,
|
icon: <AssignmentIndIcon />,
|
||||||
label: "Dashoard",
|
label: "Freelance",
|
||||||
to: "/",
|
to: "/",
|
||||||
disabled: false,
|
disabled: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <DevicesIcon />,
|
icon: <WysiwygIcon />,
|
||||||
label: "Machines",
|
label: "Blog",
|
||||||
to: "/machines",
|
to: "/blog",
|
||||||
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",
|
|
||||||
disabled: true,
|
disabled: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -138,23 +109,6 @@ export function Sidebar(props: SidebarProps) {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</List>
|
</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>
|
</div>
|
||||||
</aside>
|
</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