feat: Multiple UI improvements and new server log functionality
- Add Server Log tab with DataTable display of server activity logs - Fix keyboard navigation bug in agents tab - Add execution history viewer for selected agents - Improve policy tree widget functionality by adding single device operations - Integrate logging notifications into TUI - Add TextualNotificationHandler to setup.py - Display ERROR/WARNING/CRITICAL logs as toast notifications - Remove terminal output to prevent interference with TUI - Logs still written to Loxide.log file closes #45
This commit is contained in:
@@ -18,8 +18,9 @@ import logging
|
||||
|
||||
from rich.text import Text
|
||||
from textual.containers import Horizontal, Vertical
|
||||
from textual.message import Message
|
||||
from textual.widget import Widget
|
||||
from textual.widgets import Input, OptionList, Static, Switch, Tree
|
||||
from textual.widgets import Button, Input, OptionList, Static, Switch, Tree
|
||||
from textual.widgets.option_list import Option
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -28,6 +29,27 @@ logger = logging.getLogger(__name__)
|
||||
class PolicyTreeWidget(Widget):
|
||||
"""Widget for displaying and searching a hierarchical policy tree."""
|
||||
|
||||
class ViewExecutionHistory(Message):
|
||||
"""Message sent when user wants to view execution history for a device."""
|
||||
|
||||
def __init__(self, device):
|
||||
super().__init__()
|
||||
self.device = device
|
||||
|
||||
class GenerateOTP(Message):
|
||||
"""Message sent when user wants to generate OTP for a device."""
|
||||
|
||||
def __init__(self, device):
|
||||
super().__init__()
|
||||
self.device = device
|
||||
|
||||
class ToggleEnforcement(Message):
|
||||
"""Message sent when user wants to toggle audit/enforcement for a device."""
|
||||
|
||||
def __init__(self, device):
|
||||
super().__init__()
|
||||
self.device = device
|
||||
|
||||
def __init__(self, policies, devices):
|
||||
super().__init__()
|
||||
self.policies = policies
|
||||
@@ -35,6 +57,7 @@ class PolicyTreeWidget(Widget):
|
||||
self.last_highlighted_node = None
|
||||
self.leaf_counts = defaultdict(int)
|
||||
self.match_type = "Count" # Default to sorting by count
|
||||
self.selected_device = None # Track currently selected device
|
||||
|
||||
def compose(self):
|
||||
# Create the switch and its label
|
||||
@@ -56,6 +79,18 @@ class PolicyTreeWidget(Widget):
|
||||
search_box = Input(
|
||||
placeholder="Search policies or devices...", id="tree_search"
|
||||
)
|
||||
exec_history_button = Button(
|
||||
"📊 Execution History", id="view_exec_history_button", disabled=True
|
||||
)
|
||||
exec_history_button.styles.margin = (0, 1, 0, 0) # Right margin
|
||||
|
||||
otp_button = Button("🎫 Generate OTP", id="generate_otp_button", disabled=True)
|
||||
otp_button.styles.margin = (0, 1, 0, 0) # Right margin
|
||||
|
||||
toggle_enforcement_button = Button(
|
||||
"🔄 Toggle Enforcement/Audit", id="toggle_enforcement_button", disabled=True
|
||||
)
|
||||
# No right margin on last button
|
||||
details_pane = Static("", id="details_pane")
|
||||
|
||||
# Layout the UI
|
||||
@@ -73,6 +108,12 @@ class PolicyTreeWidget(Widget):
|
||||
# Add the search box and details pane
|
||||
yield label
|
||||
yield search_box
|
||||
# Action buttons in a horizontal row
|
||||
with Horizontal() as button_row:
|
||||
button_row.styles.height = "auto"
|
||||
yield exec_history_button
|
||||
yield otp_button
|
||||
yield toggle_enforcement_button
|
||||
yield details_pane
|
||||
|
||||
def on_mount(self) -> None:
|
||||
@@ -89,6 +130,32 @@ class PolicyTreeWidget(Widget):
|
||||
# Expand the root node
|
||||
policy_tree.root.expand()
|
||||
|
||||
def refresh_data(self, policies, devices):
|
||||
"""Refresh the widget with new data and rebuild the tree."""
|
||||
self.policies = policies
|
||||
self.devices = devices
|
||||
self.selected_device = None
|
||||
|
||||
# Disable all buttons since selection is lost
|
||||
try:
|
||||
self.query_one("#view_exec_history_button", Button).disabled = True
|
||||
self.query_one("#generate_otp_button", Button).disabled = True
|
||||
self.query_one("#toggle_enforcement_button", Button).disabled = True
|
||||
except:
|
||||
pass
|
||||
|
||||
# Rebuild tree with new data
|
||||
self._precompute_leaf_counts()
|
||||
total_leaves = sum(
|
||||
self.leaf_counts.get(policy.groupid, 0)
|
||||
for policy in self.policies
|
||||
if policy.parent == "global-policy-settings"
|
||||
)
|
||||
policy_tree = self.query_one("#policy_tree", Tree)
|
||||
policy_tree.root.set_label(f"Agents in Policies: ({total_leaves})")
|
||||
self._build_tree()
|
||||
policy_tree.root.expand()
|
||||
|
||||
def _precompute_leaf_counts(self):
|
||||
"""Precompute leaf counts for each policy group."""
|
||||
device_counts = defaultdict(int)
|
||||
@@ -184,6 +251,9 @@ class PolicyTreeWidget(Widget):
|
||||
node = message.node
|
||||
data = node.data
|
||||
details_pane = self.query_one("#details_pane", Static)
|
||||
exec_history_button = self.query_one("#view_exec_history_button", Button)
|
||||
otp_button = self.query_one("#generate_otp_button", Button)
|
||||
toggle_enforcement_button = self.query_one("#toggle_enforcement_button", Button)
|
||||
|
||||
if self.last_highlighted_node is not None:
|
||||
original_label = str(self.last_highlighted_node.label).strip()
|
||||
@@ -198,6 +268,20 @@ class PolicyTreeWidget(Widget):
|
||||
node.set_label(highlighted_label)
|
||||
self.last_highlighted_node = node
|
||||
|
||||
# Check if selected node is a device (has Agent data)
|
||||
from models.agent import Agent
|
||||
|
||||
if data and isinstance(data, Agent):
|
||||
self.selected_device = data
|
||||
exec_history_button.disabled = False
|
||||
otp_button.disabled = False
|
||||
toggle_enforcement_button.disabled = False
|
||||
else:
|
||||
self.selected_device = None
|
||||
exec_history_button.disabled = True
|
||||
otp_button.disabled = True
|
||||
toggle_enforcement_button.disabled = True
|
||||
|
||||
if data:
|
||||
details = "\n".join(
|
||||
f"{key}: {value}" for key, value in data.__dict__.items()
|
||||
@@ -287,3 +371,18 @@ class PolicyTreeWidget(Widget):
|
||||
option_list.remove()
|
||||
except:
|
||||
pass
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
"""Handle button presses."""
|
||||
if event.button.id == "view_exec_history_button":
|
||||
if self.selected_device:
|
||||
self.post_message(self.ViewExecutionHistory(self.selected_device))
|
||||
event.stop()
|
||||
elif event.button.id == "generate_otp_button":
|
||||
if self.selected_device:
|
||||
self.post_message(self.GenerateOTP(self.selected_device))
|
||||
event.stop()
|
||||
elif event.button.id == "toggle_enforcement_button":
|
||||
if self.selected_device:
|
||||
self.post_message(self.ToggleEnforcement(self.selected_device))
|
||||
event.stop()
|
||||
|
||||
Reference in New Issue
Block a user