201 lines
6.8 KiB
Python
201 lines
6.8 KiB
Python
"""
|
|
Add Entry modal window for the hosts TUI application.
|
|
|
|
This module provides a floating modal window for creating new host entries.
|
|
"""
|
|
|
|
from textual.app import ComposeResult
|
|
from textual.containers import Vertical, Horizontal
|
|
from textual.widgets import Static, Button, Input, Checkbox
|
|
from textual.screen import ModalScreen
|
|
from textual.binding import Binding
|
|
|
|
from ..core.models import HostEntry
|
|
from .styles import ADD_ENTRY_MODAL_CSS
|
|
|
|
|
|
class AddEntryModal(ModalScreen):
|
|
"""
|
|
Modal screen for adding new host entries.
|
|
|
|
Provides a floating window with input fields for creating new entries.
|
|
"""
|
|
|
|
CSS = ADD_ENTRY_MODAL_CSS
|
|
|
|
BINDINGS = [
|
|
Binding("escape", "cancel", "Cancel"),
|
|
Binding("ctrl+s", "save", "Save"),
|
|
]
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
|
|
def compose(self) -> ComposeResult:
|
|
"""Create the add entry modal layout."""
|
|
with Vertical(classes="add-entry-container"):
|
|
yield Static("Add New Host Entry", classes="add-entry-title")
|
|
|
|
with Vertical(classes="default-section") as ip_address:
|
|
ip_address.border_title = "IP Address"
|
|
yield Input(
|
|
placeholder="e.g., 192.168.1.1 or 2001:db8::1",
|
|
id="ip-address-input",
|
|
classes="default-input",
|
|
)
|
|
yield Static("", id="ip-error", classes="validation-error")
|
|
|
|
with Vertical(classes="default-section") as hostnames:
|
|
hostnames.border_title = "Hostnames"
|
|
yield Input(
|
|
placeholder="e.g., example.com, www.example.com",
|
|
id="hostnames-input",
|
|
classes="default-input",
|
|
)
|
|
yield Static("", id="hostnames-error", classes="validation-error")
|
|
|
|
with Vertical(classes="default-section") as comment:
|
|
comment.border_title = "Comment (optional)"
|
|
yield Input(
|
|
placeholder="e.g., Development server",
|
|
id="comment-input",
|
|
classes="default-input",
|
|
)
|
|
|
|
with Vertical(classes="default-section") as active:
|
|
active.border_title = "Activate Entry"
|
|
yield Checkbox(
|
|
"Active",
|
|
value=True,
|
|
id="active-checkbox",
|
|
classes="default-checkbox",
|
|
)
|
|
|
|
with Horizontal(classes="button-row"):
|
|
yield Button(
|
|
"Add Entry (CTRL+S)",
|
|
variant="primary",
|
|
id="add-button",
|
|
classes="default-button",
|
|
)
|
|
yield Button(
|
|
"Cancel (ESC)",
|
|
variant="default",
|
|
id="cancel-button",
|
|
classes="default-button",
|
|
)
|
|
|
|
def on_mount(self) -> None:
|
|
"""Focus IP address input when modal opens."""
|
|
ip_input = self.query_one("#ip-address-input", Input)
|
|
ip_input.focus()
|
|
|
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
"""Handle button presses."""
|
|
if event.button.id == "add-button":
|
|
self.action_save()
|
|
elif event.button.id == "cancel-button":
|
|
self.action_cancel()
|
|
|
|
def action_save(self) -> None:
|
|
"""Validate and save new entry."""
|
|
# Clear previous errors
|
|
self._clear_errors()
|
|
|
|
# Get form values
|
|
ip_address = self.query_one("#ip-address-input", Input).value.strip()
|
|
hostnames_str = self.query_one("#hostnames-input", Input).value.strip()
|
|
comment = self.query_one("#comment-input", Input).value.strip()
|
|
is_active = self.query_one("#active-checkbox", Checkbox).value
|
|
|
|
# Validate input
|
|
if not self._validate_input(ip_address, hostnames_str):
|
|
return
|
|
|
|
try:
|
|
# Parse hostnames
|
|
hostnames = [h.strip() for h in hostnames_str.split(",") if h.strip()]
|
|
|
|
# Create new entry
|
|
new_entry = HostEntry(
|
|
ip_address=ip_address,
|
|
hostnames=hostnames,
|
|
comment=comment if comment else None,
|
|
is_active=is_active,
|
|
)
|
|
|
|
# Close modal and return the new entry
|
|
self.dismiss(new_entry)
|
|
|
|
except ValueError as e:
|
|
# Display validation error
|
|
if "IP address" in str(e).lower():
|
|
self._show_error("ip-error", str(e))
|
|
else:
|
|
self._show_error("hostnames-error", str(e))
|
|
|
|
def action_cancel(self) -> None:
|
|
"""Cancel entry creation and close modal."""
|
|
self.dismiss(None)
|
|
|
|
def _validate_input(self, ip_address: str, hostnames_str: str) -> bool:
|
|
"""
|
|
Validate user input.
|
|
|
|
Args:
|
|
ip_address: IP address to validate
|
|
hostnames_str: Comma-separated hostnames to validate
|
|
|
|
Returns:
|
|
True if input is valid, False otherwise
|
|
"""
|
|
valid = True
|
|
|
|
# Validate IP address
|
|
if not ip_address:
|
|
self._show_error("ip-error", "IP address is required")
|
|
valid = False
|
|
|
|
# Validate hostnames
|
|
if not hostnames_str:
|
|
self._show_error("hostnames-error", "At least one hostname is required")
|
|
valid = False
|
|
else:
|
|
hostnames = [h.strip() for h in hostnames_str.split(",") if h.strip()]
|
|
if not hostnames:
|
|
self._show_error("hostnames-error", "At least one hostname is required")
|
|
valid = False
|
|
else:
|
|
# Basic hostname validation
|
|
for hostname in hostnames:
|
|
if (
|
|
" " in hostname
|
|
or not hostname.replace(".", "")
|
|
.replace("-", "")
|
|
.replace("_", "")
|
|
.isalnum()
|
|
):
|
|
self._show_error(
|
|
"hostnames-error", f"Invalid hostname format: {hostname}"
|
|
)
|
|
valid = False
|
|
break
|
|
|
|
return valid
|
|
|
|
def _show_error(self, error_id: str, message: str) -> None:
|
|
"""Show validation error message."""
|
|
try:
|
|
error_widget = self.query_one(f"#{error_id}", Static)
|
|
error_widget.update(message)
|
|
except Exception:
|
|
pass
|
|
|
|
def _clear_errors(self) -> None:
|
|
"""Clear all validation error messages."""
|
|
for error_id in ["ip-error", "hostnames-error"]:
|
|
try:
|
|
error_widget = self.query_one(f"#{error_id}", Static)
|
|
error_widget.update("")
|
|
except Exception:
|
|
pass
|