mirror of
https://github.com/Cian-H/I-Form_Server_Node_Deployer.git
synced 2025-12-23 06:32:08 +00:00
Added more user feedback for better experience
This commit is contained in:
@@ -41,7 +41,6 @@ mkdocs-material = "^9.4.8"
|
|||||||
flet = "^0.11.0"
|
flet = "^0.11.0"
|
||||||
psutil = "^5.9.6"
|
psutil = "^5.9.6"
|
||||||
types-psutil = "^5.9.5.17"
|
types-psutil = "^5.9.5.17"
|
||||||
bidict = "^0.22.1"
|
|
||||||
|
|
||||||
[tool.poetry.scripts]
|
[tool.poetry.scripts]
|
||||||
node_deployer = "node_deployer.__main__:main"
|
node_deployer = "node_deployer.__main__:main"
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
from functools import wraps
|
from functools import wraps
|
||||||
from typing import Callable, Mapping
|
from typing import Callable, Optional, Tuple
|
||||||
|
|
||||||
from bidict import frozenbidict
|
|
||||||
import flet as ft
|
import flet as ft
|
||||||
from flet_core.form_field_control import FormFieldControl
|
|
||||||
from node_deployer.create_disk import create_ignition_disk
|
from node_deployer.create_disk import create_ignition_disk
|
||||||
from node_deployer.ip_interface import IPAddress
|
from node_deployer.ip_interface import IPAddress
|
||||||
|
|
||||||
from .disk_dropdown import disk_dropdown
|
from .disk_dropdown import disk_dropdown
|
||||||
|
from .types import CreateDiskArgs
|
||||||
|
|
||||||
|
|
||||||
def main(page: ft.Page) -> None:
|
def main(page: ft.Page) -> None:
|
||||||
page.title = "I-Form Server Node Deployer"
|
page.title = "I-Form Server Node Deployer"
|
||||||
page.vertical_alignment = ft.MainAxisAlignment.CENTER
|
page.vertical_alignment = ft.MainAxisAlignment.CENTER
|
||||||
|
|
||||||
|
# TODO: Add hotkeys
|
||||||
|
# TODO: Add a logo
|
||||||
# TODO: Add a progress bar
|
# TODO: Add a progress bar
|
||||||
# TODO: Finalise arrangement of fields
|
# TODO: Finalise arrangement of fields
|
||||||
|
|
||||||
@@ -27,16 +28,24 @@ def main(page: ft.Page) -> None:
|
|||||||
switch_port = ft.TextField(label="Switch Port", value="4789", text_align=ft.TextAlign.LEFT)
|
switch_port = ft.TextField(label="Switch Port", value="4789", text_align=ft.TextAlign.LEFT)
|
||||||
swarm_token = ft.TextField(label="Swarm Token", text_align=ft.TextAlign.LEFT)
|
swarm_token = ft.TextField(label="Swarm Token", text_align=ft.TextAlign.LEFT)
|
||||||
|
|
||||||
|
# Add varnames, as they will be useful for unpacking later
|
||||||
|
disk.__varname__ = "disk"
|
||||||
|
hostname.__varname__ = "hostname"
|
||||||
|
password.__varname__ = "password"
|
||||||
|
switch_ip.__varname__ = "switch_ip"
|
||||||
|
switch_port.__varname__ = "switch_port"
|
||||||
|
swarm_token.__varname__ = "swarm_token"
|
||||||
|
|
||||||
# This wrapper validates the value of the field before passing it to the function
|
# This wrapper validates the value of the field before passing it to the function
|
||||||
def validate_value(func: Callable) -> Callable:
|
def validate_value[F](func: Callable[[], F]) -> Callable[[], Optional[F]]: # mypy PEP 695 support can't come quickly enough # noqa
|
||||||
|
#! It is important that bool(F) evaluates to False if the value is invalid
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
def wrapped():
|
def wrapped() -> Optional[F]:
|
||||||
out = func()
|
out: F = func()
|
||||||
if out:
|
if out:
|
||||||
return out
|
return out
|
||||||
else:
|
else:
|
||||||
# TODO: Highlight the invalid field
|
return None
|
||||||
raise NotImplementedError("Invalid field value path not implemented")
|
|
||||||
|
|
||||||
return wrapped
|
return wrapped
|
||||||
|
|
||||||
@@ -55,7 +64,12 @@ def main(page: ft.Page) -> None:
|
|||||||
|
|
||||||
@validate_value
|
@validate_value
|
||||||
def get_switch_ip() -> IPAddress:
|
def get_switch_ip() -> IPAddress:
|
||||||
return IPAddress(switch_ip.value if switch_ip.value is not None else "0.0.0.0")
|
ip = IPAddress("0.0.0.0")
|
||||||
|
try:
|
||||||
|
ip = IPAddress(switch_ip.value)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return ip
|
||||||
|
|
||||||
@validate_value
|
@validate_value
|
||||||
def get_switch_port() -> int:
|
def get_switch_port() -> int:
|
||||||
@@ -67,26 +81,60 @@ def main(page: ft.Page) -> None:
|
|||||||
|
|
||||||
# A bidirectional dictionary gives us a stateless bidirectional map between
|
# A bidirectional dictionary gives us a stateless bidirectional map between
|
||||||
# fields and their fetch functions
|
# fields and their fetch functions
|
||||||
field_fetch_map: Mapping[FormFieldControl, Callable] = frozenbidict(
|
type FieldFetch = Tuple[ft.TextField | ft.Dropdown, Callable]
|
||||||
{
|
field_fetch_map: Tuple[
|
||||||
disk: get_disk,
|
FieldFetch, FieldFetch, FieldFetch, FieldFetch, FieldFetch, FieldFetch
|
||||||
hostname: get_hostname,
|
] = (
|
||||||
password: get_password,
|
(disk, get_disk),
|
||||||
switch_ip: get_switch_ip,
|
(hostname, get_hostname),
|
||||||
switch_port: get_switch_port,
|
(password, get_password),
|
||||||
swarm_token: get_swarm_token,
|
(switch_ip, get_switch_ip),
|
||||||
}
|
(switch_port, get_switch_port),
|
||||||
|
(swarm_token, get_swarm_token),
|
||||||
)
|
)
|
||||||
|
|
||||||
# This button triggers the confirmation popup before calling the disk creation function
|
# This button triggers the confirmation popup before calling the disk creation function
|
||||||
def confirm_disk_creation(*_):
|
def confirm_disk_creation(*_) -> None:
|
||||||
# Fetch the values of the fields
|
# Fetch the values of the fields
|
||||||
disk_val: str = field_fetch_map[disk]()
|
vals: CreateDiskArgs = {
|
||||||
hostname_val: str = field_fetch_map[hostname]()
|
"disk": "",
|
||||||
password_val: str = field_fetch_map[password]()
|
"hostname": "",
|
||||||
switch_ip_val: IPAddress = field_fetch_map[switch_ip]()
|
"password": "",
|
||||||
switch_port_val: int = field_fetch_map[switch_port]()
|
"switch_ip": IPAddress("0.0.0.0"),
|
||||||
swarm_token_val: str = field_fetch_map[swarm_token]()
|
"switch_port": 0,
|
||||||
|
"swarm_token": "",
|
||||||
|
}
|
||||||
|
invalid_values = False
|
||||||
|
for field, fetch_func in field_fetch_map:
|
||||||
|
value = fetch_func()
|
||||||
|
varname: str = str(field.__varname__)
|
||||||
|
if varname in CreateDiskArgs.__annotations__.keys():
|
||||||
|
target_type = CreateDiskArgs.__annotations__[varname]
|
||||||
|
else:
|
||||||
|
raise KeyError(f"Field {varname} is not in CreateDiskArgs")
|
||||||
|
if value is None:
|
||||||
|
# If invalid values, highlight the field and set the error text
|
||||||
|
field.error_text = "This field is required"
|
||||||
|
field.border_color = "RED"
|
||||||
|
field.update()
|
||||||
|
invalid_values = True
|
||||||
|
else:
|
||||||
|
# If valid values, ensure the field is not highlighted and clear the error text
|
||||||
|
field.error_text = None
|
||||||
|
field.border_color = None
|
||||||
|
field.update()
|
||||||
|
typed_val = target_type(value)
|
||||||
|
vals[varname] = typed_val # type: ignore #! This is a false positive, an invalid literal would have been caught by the if statement above
|
||||||
|
|
||||||
|
if invalid_values:
|
||||||
|
return
|
||||||
|
|
||||||
|
disk_val: str = vals["disk"]
|
||||||
|
hostname_val: str = vals["hostname"]
|
||||||
|
password_val: str = vals["password"]
|
||||||
|
switch_ip_val: IPAddress = vals["switch_ip"]
|
||||||
|
switch_port_val: int = vals["switch_port"]
|
||||||
|
swarm_token_val: str = vals["swarm_token"]
|
||||||
|
|
||||||
# The following closures build the confirmation popup
|
# The following closures build the confirmation popup
|
||||||
# Also: nested closures, eww. It feels dirty, but maintains the functional style
|
# Also: nested closures, eww. It feels dirty, but maintains the functional style
|
||||||
@@ -116,7 +164,11 @@ def main(page: ft.Page) -> None:
|
|||||||
switch_port=switch_port_val,
|
switch_port=switch_port_val,
|
||||||
swarm_token=swarm_token_val,
|
swarm_token=swarm_token_val,
|
||||||
)
|
)
|
||||||
close_dlg(None)
|
dlg.content = ft.Text("Ignition disk created!")
|
||||||
|
dlg.actions = [
|
||||||
|
ft.TextButton("OK", on_click=close_dlg),
|
||||||
|
]
|
||||||
|
page.update()
|
||||||
|
|
||||||
dlg = ft.AlertDialog(
|
dlg = ft.AlertDialog(
|
||||||
modal=True,
|
modal=True,
|
||||||
|
|||||||
12
src/node_deployer_gui/types.py
Normal file
12
src/node_deployer_gui/types.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from typing import TypedDict
|
||||||
|
|
||||||
|
from node_deployer.ip_interface import IPAddress
|
||||||
|
|
||||||
|
|
||||||
|
class CreateDiskArgs(TypedDict):
|
||||||
|
disk: str
|
||||||
|
hostname: str
|
||||||
|
password: str
|
||||||
|
switch_ip: IPAddress
|
||||||
|
switch_port: int
|
||||||
|
swarm_token: str
|
||||||
Reference in New Issue
Block a user