2025-03-27 21:53:01 +01:00
|
|
|
"""Admin UI components for EOS Dashboard.
|
|
|
|
|
|
|
|
This module provides functions to generate administrative UI components
|
|
|
|
for the EOS dashboard.
|
|
|
|
"""
|
|
|
|
|
2025-04-05 13:08:12 +02:00
|
|
|
import json
|
|
|
|
from pathlib import Path
|
2025-03-27 21:53:01 +01:00
|
|
|
from typing import Any, Optional, Union
|
|
|
|
|
|
|
|
import requests
|
2025-04-05 13:08:12 +02:00
|
|
|
from fasthtml.common import Select
|
2025-06-10 22:00:28 +02:00
|
|
|
from loguru import logger
|
2025-03-27 21:53:01 +01:00
|
|
|
from monsterui.foundations import stringify
|
2025-04-05 13:08:12 +02:00
|
|
|
from monsterui.franken import ( # Select, TODO: Select from FrankenUI does not work - using Select from FastHTML instead
|
|
|
|
H3,
|
2025-03-27 21:53:01 +01:00
|
|
|
Button,
|
|
|
|
ButtonT,
|
|
|
|
Card,
|
|
|
|
Details,
|
2025-04-05 13:08:12 +02:00
|
|
|
Div,
|
2025-03-27 21:53:01 +01:00
|
|
|
DivHStacked,
|
|
|
|
DividerLine,
|
|
|
|
Grid,
|
2025-04-05 13:08:12 +02:00
|
|
|
Input,
|
|
|
|
Options,
|
2025-03-27 21:53:01 +01:00
|
|
|
P,
|
|
|
|
Summary,
|
|
|
|
UkIcon,
|
|
|
|
)
|
2025-04-05 13:08:12 +02:00
|
|
|
from platformdirs import user_config_dir
|
|
|
|
|
|
|
|
from akkudoktoreos.server.dash.components import Error, Success
|
|
|
|
from akkudoktoreos.server.dash.configuration import get_nested_value
|
|
|
|
from akkudoktoreos.utils.datetimeutil import to_datetime
|
|
|
|
|
|
|
|
# Directory to export files to, or to import files from
|
|
|
|
export_import_directory = Path(user_config_dir("net.akkudoktor.eosdash", "akkudoktor"))
|
2025-03-27 21:53:01 +01:00
|
|
|
|
|
|
|
|
|
|
|
def AdminButton(*c: Any, cls: Optional[Union[str, tuple]] = None, **kwargs: Any) -> Button:
|
|
|
|
"""Creates a styled button for administrative actions.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
*c (Any): Positional arguments representing the button's content.
|
|
|
|
cls (Optional[Union[str, tuple]]): Additional CSS classes for styling. Defaults to None.
|
|
|
|
**kwargs (Any): Additional keyword arguments passed to the `Button`.
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
Button: A styled `Button` component for admin actions.
|
|
|
|
"""
|
|
|
|
new_cls = f"{ButtonT.primary}"
|
|
|
|
if cls:
|
|
|
|
new_cls += f" {stringify(cls)}"
|
|
|
|
kwargs["cls"] = new_cls
|
|
|
|
return Button(*c, submit=False, **kwargs)
|
|
|
|
|
|
|
|
|
2025-04-05 13:08:12 +02:00
|
|
|
def AdminConfig(
|
|
|
|
eos_host: str, eos_port: Union[str, int], data: Optional[dict], config: Optional[dict[str, Any]]
|
|
|
|
) -> tuple[str, Union[Card, list[Card]]]:
|
2025-03-27 21:53:01 +01:00
|
|
|
"""Creates a configuration management card with save-to-file functionality.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
eos_host (str): The hostname of the EOS server.
|
|
|
|
eos_port (Union[str, int]): The port of the EOS server.
|
|
|
|
data (Optional[dict]): Incoming data containing action and category for processing.
|
|
|
|
|
|
|
|
Returns:
|
2025-04-05 13:08:12 +02:00
|
|
|
tuple[str, Union[Card, list[Card]]]: A tuple containing the configuration category label and the `Card` UI component.
|
2025-03-27 21:53:01 +01:00
|
|
|
"""
|
|
|
|
server = f"http://{eos_host}:{eos_port}"
|
2025-04-05 13:08:12 +02:00
|
|
|
eos_hostname = "EOS server"
|
|
|
|
eosdash_hostname = "EOSdash server"
|
2025-03-27 21:53:01 +01:00
|
|
|
|
|
|
|
category = "configuration"
|
2025-04-05 13:08:12 +02:00
|
|
|
# save config file
|
2025-03-27 21:53:01 +01:00
|
|
|
status = (None,)
|
2025-04-05 13:08:12 +02:00
|
|
|
config_file_path = "<unknown>"
|
|
|
|
try:
|
|
|
|
if config:
|
|
|
|
config_file_path = get_nested_value(config, ["general", "config_file_path"])
|
2025-06-03 08:30:37 +02:00
|
|
|
except Exception as e:
|
|
|
|
logger.debug(f"general.config_file_path: {e}")
|
2025-04-05 13:08:12 +02:00
|
|
|
# export config file
|
|
|
|
export_to_file_next_tag = to_datetime(as_string="YYYYMMDDHHmmss")
|
|
|
|
export_to_file_status = (None,)
|
|
|
|
# import config file
|
|
|
|
import_from_file_status = (None,)
|
|
|
|
|
|
|
|
if data and data.get("category", None) == category:
|
2025-03-27 21:53:01 +01:00
|
|
|
# This data is for us
|
|
|
|
if data["action"] == "save_to_file":
|
|
|
|
# Safe current configuration to file
|
|
|
|
try:
|
2025-06-03 08:30:37 +02:00
|
|
|
result = requests.put(f"{server}/v1/config/file", timeout=10)
|
2025-03-27 21:53:01 +01:00
|
|
|
result.raise_for_status()
|
|
|
|
config_file_path = result.json()["general"]["config_file_path"]
|
2025-04-05 13:08:12 +02:00
|
|
|
status = Success(f"Saved to '{config_file_path}' on '{eos_hostname}'")
|
|
|
|
except requests.exceptions.HTTPError as e:
|
|
|
|
detail = result.json()["detail"]
|
|
|
|
status = Error(
|
|
|
|
f"Can not save actual config to file on '{eos_hostname}': {e}, {detail}"
|
|
|
|
)
|
|
|
|
except Exception as e:
|
|
|
|
status = Error(f"Can not save actual config to file on '{eos_hostname}': {e}")
|
|
|
|
elif data["action"] == "export_to_file":
|
|
|
|
# Export current configuration to file
|
|
|
|
export_to_file_tag = data.get("export_to_file_tag", export_to_file_next_tag)
|
|
|
|
export_to_file_path = export_import_directory.joinpath(
|
|
|
|
f"eos_config_{export_to_file_tag}.json"
|
|
|
|
)
|
|
|
|
try:
|
|
|
|
if not config:
|
|
|
|
raise ValueError(f"No config from '{eos_hostname}'")
|
|
|
|
export_to_file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
with export_to_file_path.open("w", encoding="utf-8", newline="\n") as fd:
|
|
|
|
json.dump(config, fd, indent=4, sort_keys=True)
|
|
|
|
export_to_file_status = Success(
|
|
|
|
f"Exported to '{export_to_file_path}' on '{eosdash_hostname}'"
|
2025-03-27 21:53:01 +01:00
|
|
|
)
|
2025-04-05 13:08:12 +02:00
|
|
|
except requests.exceptions.HTTPError as e:
|
2025-03-27 21:53:01 +01:00
|
|
|
detail = result.json()["detail"]
|
2025-04-05 13:08:12 +02:00
|
|
|
export_to_file_status = Error(
|
|
|
|
f"Can not export actual config to '{export_to_file_path}' on '{eosdash_hostname}': {e}, {detail}"
|
|
|
|
)
|
|
|
|
except Exception as e:
|
|
|
|
export_to_file_status = Error(
|
|
|
|
f"Can not export actual config to '{export_to_file_path}' on '{eosdash_hostname}': {e}"
|
|
|
|
)
|
|
|
|
elif data["action"] == "import_from_file":
|
|
|
|
import_file_name = data.get("import_file_name", None)
|
|
|
|
import_from_file_pathes = list(
|
|
|
|
export_import_directory.glob("*.json")
|
|
|
|
) # expand generator object
|
|
|
|
import_file_path = None
|
|
|
|
for f in import_from_file_pathes:
|
|
|
|
if f.name == import_file_name:
|
|
|
|
import_file_path = f
|
|
|
|
if import_file_path:
|
|
|
|
try:
|
|
|
|
with import_file_path.open("r", encoding="utf-8", newline=None) as fd:
|
|
|
|
import_config = json.load(fd)
|
2025-06-03 08:30:37 +02:00
|
|
|
result = requests.put(f"{server}/v1/config", json=import_config, timeout=10)
|
2025-04-05 13:08:12 +02:00
|
|
|
result.raise_for_status()
|
|
|
|
import_from_file_status = Success(
|
|
|
|
f"Config imported from '{import_file_path}' on '{eosdash_hostname}'"
|
|
|
|
)
|
|
|
|
except requests.exceptions.HTTPError as e:
|
|
|
|
detail = result.json()["detail"]
|
|
|
|
import_from_file_status = Error(
|
|
|
|
f"Can not import config from '{import_file_name}' on '{eosdash_hostname}' {e}, {detail}"
|
|
|
|
)
|
|
|
|
except Exception as e:
|
|
|
|
import_from_file_status = Error(
|
|
|
|
f"Can not import config from '{import_file_name}' on '{eosdash_hostname}' {e}"
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
import_from_file_status = Error(
|
|
|
|
f"Can not import config from '{import_file_name}', not found in '{export_import_directory}' on '{eosdash_hostname}'"
|
2025-03-27 21:53:01 +01:00
|
|
|
)
|
2025-04-05 13:08:12 +02:00
|
|
|
|
|
|
|
# Update for display, in case we added a new file before
|
|
|
|
import_from_file_names = [f.name for f in list(export_import_directory.glob("*.json"))]
|
|
|
|
|
2025-03-27 21:53:01 +01:00
|
|
|
return (
|
|
|
|
category,
|
2025-04-05 13:08:12 +02:00
|
|
|
[
|
|
|
|
Card(
|
|
|
|
Details(
|
|
|
|
Summary(
|
|
|
|
Grid(
|
|
|
|
DivHStacked(
|
|
|
|
UkIcon(icon="play"),
|
|
|
|
AdminButton(
|
|
|
|
"Save to file",
|
|
|
|
hx_post="/eosdash/admin",
|
|
|
|
hx_target="#page-content",
|
|
|
|
hx_swap="innerHTML",
|
|
|
|
hx_vals='{"category": "configuration", "action": "save_to_file"}',
|
|
|
|
),
|
|
|
|
P(f"'{config_file_path}' on '{eos_hostname}'"),
|
|
|
|
),
|
|
|
|
status,
|
|
|
|
),
|
|
|
|
cls="list-none",
|
|
|
|
),
|
|
|
|
P(f"Safe actual configuration to '{config_file_path}' on '{eos_hostname}'."),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
Card(
|
|
|
|
Details(
|
|
|
|
Summary(
|
|
|
|
Grid(
|
|
|
|
DivHStacked(
|
|
|
|
UkIcon(icon="play"),
|
|
|
|
AdminButton(
|
|
|
|
"Export to file",
|
|
|
|
hx_post="/eosdash/admin",
|
|
|
|
hx_target="#page-content",
|
|
|
|
hx_swap="innerHTML",
|
|
|
|
hx_vals='js:{"category": "configuration", "action": "export_to_file", "export_to_file_tag": document.querySelector("[name=\'chosen_export_file_tag\']").value }',
|
|
|
|
),
|
|
|
|
P("'eos_config_"),
|
|
|
|
Input(
|
|
|
|
id="export_file_tag",
|
|
|
|
name="chosen_export_file_tag",
|
|
|
|
value=export_to_file_next_tag,
|
|
|
|
),
|
|
|
|
P(".json'"),
|
|
|
|
),
|
|
|
|
export_to_file_status,
|
|
|
|
),
|
|
|
|
cls="list-none",
|
|
|
|
),
|
|
|
|
P(
|
|
|
|
f"Export actual configuration to 'eos_config_{export_to_file_next_tag}.json' on '{eosdash_hostname}'."
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
Card(
|
|
|
|
Details(
|
|
|
|
Summary(
|
|
|
|
Grid(
|
|
|
|
DivHStacked(
|
|
|
|
UkIcon(icon="play"),
|
|
|
|
AdminButton(
|
|
|
|
"Import from file",
|
|
|
|
hx_post="/eosdash/admin",
|
|
|
|
hx_target="#page-content",
|
|
|
|
hx_swap="innerHTML",
|
|
|
|
hx_vals='js:{ "category": "configuration", "action": "import_from_file", "import_file_name": document.querySelector("[name=\'selected_import_file_name\']").value }',
|
|
|
|
),
|
|
|
|
Select(
|
|
|
|
*Options(*import_from_file_names),
|
|
|
|
id="import_file_name",
|
|
|
|
name="selected_import_file_name", # Name of hidden input field with selected value
|
|
|
|
placeholder="Select file",
|
|
|
|
),
|
2025-03-27 21:53:01 +01:00
|
|
|
),
|
2025-04-05 13:08:12 +02:00
|
|
|
import_from_file_status,
|
2025-03-27 21:53:01 +01:00
|
|
|
),
|
2025-04-05 13:08:12 +02:00
|
|
|
cls="list-none",
|
2025-03-27 21:53:01 +01:00
|
|
|
),
|
2025-04-05 13:08:12 +02:00
|
|
|
P(f"Import configuration from config file on '{eosdash_hostname}'."),
|
2025-03-27 21:53:01 +01:00
|
|
|
),
|
|
|
|
),
|
2025-04-05 13:08:12 +02:00
|
|
|
],
|
2025-03-27 21:53:01 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def Admin(eos_host: str, eos_port: Union[str, int], data: Optional[dict] = None) -> Div:
|
|
|
|
"""Generates the administrative dashboard layout.
|
|
|
|
|
|
|
|
This includes configuration management and other administrative tools.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
eos_host (str): The hostname of the EOS server.
|
|
|
|
eos_port (Union[str, int]): The port of the EOS server.
|
|
|
|
data (Optional[dict], optional): Incoming data to trigger admin actions. Defaults to None.
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
Div: A `Div` component containing the assembled admin interface.
|
|
|
|
"""
|
2025-04-05 13:08:12 +02:00
|
|
|
# Get current configuration from server
|
|
|
|
server = f"http://{eos_host}:{eos_port}"
|
|
|
|
try:
|
2025-06-03 08:30:37 +02:00
|
|
|
result = requests.get(f"{server}/v1/config", timeout=10)
|
2025-04-05 13:08:12 +02:00
|
|
|
result.raise_for_status()
|
|
|
|
config = result.json()
|
|
|
|
except requests.exceptions.HTTPError as e:
|
|
|
|
config = {}
|
|
|
|
detail = result.json()["detail"]
|
|
|
|
warning_msg = f"Can not retrieve configuration from {server}: {e}, {detail}"
|
|
|
|
logger.warning(warning_msg)
|
|
|
|
return Error(warning_msg)
|
|
|
|
except Exception as e:
|
|
|
|
warning_msg = f"Can not retrieve configuration from {server}: {e}"
|
|
|
|
logger.warning(warning_msg)
|
|
|
|
return Error(warning_msg)
|
|
|
|
|
2025-03-27 21:53:01 +01:00
|
|
|
rows = []
|
|
|
|
last_category = ""
|
|
|
|
for category, admin in [
|
2025-04-05 13:08:12 +02:00
|
|
|
AdminConfig(eos_host, eos_port, data, config),
|
2025-03-27 21:53:01 +01:00
|
|
|
]:
|
|
|
|
if category != last_category:
|
2025-04-05 13:08:12 +02:00
|
|
|
rows.append(H3(category))
|
2025-03-27 21:53:01 +01:00
|
|
|
rows.append(DividerLine())
|
|
|
|
last_category = category
|
2025-04-05 13:08:12 +02:00
|
|
|
if isinstance(admin, list):
|
|
|
|
for card in admin:
|
|
|
|
rows.append(card)
|
|
|
|
else:
|
|
|
|
rows.append(admin)
|
2025-03-27 21:53:01 +01:00
|
|
|
|
|
|
|
return Div(*rows, cls="space-y-4")
|