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:
@@ -29,6 +29,7 @@ from textual.widget import Widget
|
||||
from textual.widgets import Button, DataTable, Footer, Header, Static, TextArea
|
||||
|
||||
from models.agent import Agent
|
||||
from TUI.Screens.executionhistoryscreen import ExecutionHistoryScreen
|
||||
from TUI.Screens.otpworkflowscreen import OTPWorkflowScreen
|
||||
from TUI.Screens.policyselectorscreen import PolicySelectorScreen
|
||||
from TUI.Widgets.OTP_generate import OTPGenerator
|
||||
@@ -140,6 +141,7 @@ class AgentMoveOperations(Widget):
|
||||
toggle_enforcement_btn = self.query_one("#toggle_enforcement_btn", Button)
|
||||
other_policy_btn = self.query_one("#other_policy_btn", Button)
|
||||
otp_gen_btn = self.query_one("#otp_gen_btn", Button)
|
||||
exec_history_btn = self.query_one("#exec_history_btn", Button)
|
||||
|
||||
# If operation in progress, disable all
|
||||
if self.operation_in_progress:
|
||||
@@ -148,6 +150,7 @@ class AgentMoveOperations(Widget):
|
||||
local_approval_btn.disabled = True
|
||||
toggle_enforcement_btn.disabled = True
|
||||
other_policy_btn.disabled = True
|
||||
exec_history_btn.disabled = True
|
||||
else:
|
||||
# If an operation was selected, disable
|
||||
if self.selected_operation:
|
||||
@@ -162,6 +165,9 @@ class AgentMoveOperations(Widget):
|
||||
other_policy_btn.disabled = (
|
||||
self.selected_operation == "other_policy"
|
||||
)
|
||||
exec_history_btn.disabled = (
|
||||
self.selected_operation == "exec_history"
|
||||
)
|
||||
else:
|
||||
# Enable all buttons
|
||||
otp_gen_btn = False
|
||||
@@ -169,6 +175,7 @@ class AgentMoveOperations(Widget):
|
||||
local_approval_btn.disabled = False
|
||||
toggle_enforcement_btn.disabled = False
|
||||
other_policy_btn.disabled = False
|
||||
exec_history_btn.disabled = False
|
||||
|
||||
except NoMatches:
|
||||
pass
|
||||
@@ -199,21 +206,21 @@ class AgentMoveOperations(Widget):
|
||||
f"Operation: {operation_name}",
|
||||
f"{'=' * 50}",
|
||||
"",
|
||||
f"✅ Successful ({len(successful)}):",
|
||||
f"✅ Successful ({len(successful)}):",
|
||||
]
|
||||
|
||||
if successful:
|
||||
for agent, result in successful:
|
||||
results_lines.append(f" ✅ {agent.hostname}")
|
||||
results_lines.append(f" ✅ {agent.hostname}")
|
||||
else:
|
||||
results_lines.append(" (none)")
|
||||
|
||||
results_lines.append("")
|
||||
results_lines.append(f"❌ Failed ({len(unsuccessful)}):")
|
||||
results_lines.append(f"⌠Failed ({len(unsuccessful)}):")
|
||||
|
||||
if unsuccessful:
|
||||
for agent, error in unsuccessful:
|
||||
results_lines.append(f" ❌ {agent.hostname}: {error}")
|
||||
results_lines.append(f" ⌠{agent.hostname}: {error}")
|
||||
else:
|
||||
results_lines.append(" (none)")
|
||||
|
||||
@@ -248,9 +255,9 @@ class AgentMoveOperations(Widget):
|
||||
- Operations panel: 1/3 width
|
||||
- Results area: Initially hidden, shown after operation completion
|
||||
"""
|
||||
yield Header(show_clock=True, icon="⚙️")
|
||||
yield Header(show_clock=True, icon="âš™ï¸")
|
||||
title_text = Static(
|
||||
f"🖥️ Agent Operations - {len(self.agents)} device(s) selected",
|
||||
f"ðŸ–¥ï¸ Agent Operations - {len(self.agents)} device(s) selected",
|
||||
id="move_ops_title",
|
||||
)
|
||||
title_text.styles.margin = (0, 0, 1, 0)
|
||||
@@ -285,37 +292,44 @@ class AgentMoveOperations(Widget):
|
||||
yield operations_label
|
||||
|
||||
# Operation buttons
|
||||
export_csv_btn = Button("📄 Export CSV", id="export_csv_btn")
|
||||
export_csv_btn = Button("📄 Export CSV", id="export_csv_btn")
|
||||
export_csv_btn.styles.width = "100%"
|
||||
export_csv_btn.styles.margin = (0, 0, 1, 0)
|
||||
yield export_csv_btn
|
||||
|
||||
local_approval_btn = Button(
|
||||
"✔️ Local Approval Mode", id="local_approval_btn"
|
||||
"âœ”ï¸ Local Approval Mode", id="local_approval_btn"
|
||||
)
|
||||
local_approval_btn.styles.width = "100%"
|
||||
local_approval_btn.styles.margin = (0, 0, 1, 0)
|
||||
yield local_approval_btn
|
||||
|
||||
otp_gen_btn = Button("🎫 Generate One Time Passes", id="otp_gen_btn")
|
||||
otp_gen_btn = Button("🎫 Generate One Time Passes", id="otp_gen_btn")
|
||||
otp_gen_btn.styles.width = "100%"
|
||||
otp_gen_btn.styles.margin = (0, 0, 1, 0)
|
||||
yield otp_gen_btn
|
||||
|
||||
toggle_enforcement_btn = Button(
|
||||
"🔄 Toggle Audit/Enforcement", id="toggle_enforcement_btn"
|
||||
"🔄 Toggle Audit/Enforcement", id="toggle_enforcement_btn"
|
||||
)
|
||||
toggle_enforcement_btn.styles.width = "100%"
|
||||
toggle_enforcement_btn.styles.margin = (0, 0, 1, 0)
|
||||
yield toggle_enforcement_btn
|
||||
|
||||
other_policy_btn = Button(
|
||||
"🔀 Move to Other Policy", id="other_policy_btn"
|
||||
"🔀 Move to Other Policy", id="other_policy_btn"
|
||||
)
|
||||
other_policy_btn.styles.width = "100%"
|
||||
other_policy_btn.styles.margin = (0, 0, 1, 0)
|
||||
yield other_policy_btn
|
||||
|
||||
exec_history_btn = Button(
|
||||
"📊 View Execution History", id="exec_history_btn"
|
||||
)
|
||||
exec_history_btn.styles.width = "100%"
|
||||
exec_history_btn.styles.margin = (0, 0, 1, 0)
|
||||
yield exec_history_btn
|
||||
|
||||
# Status label
|
||||
status_label = Static("", id="status_label")
|
||||
status_label.styles.margin = (2, 0, 0, 0)
|
||||
@@ -377,17 +391,17 @@ class AgentMoveOperations(Widget):
|
||||
|
||||
pyperclip.copy(results_text.text)
|
||||
self.app.notify(
|
||||
"📋✅ Results copied to clipboard!",
|
||||
"📋✅ Results copied to clipboard!",
|
||||
severity="information",
|
||||
timeout=2,
|
||||
)
|
||||
except ImportError:
|
||||
self.app.notify(
|
||||
"❌ pyperclip not installed. Run: pip install pyperclip",
|
||||
"⌠pyperclip not installed. Run: pip install pyperclip",
|
||||
severity="warning",
|
||||
)
|
||||
except Exception as e:
|
||||
self.app.notify(f"❌ Failed to copy: {str(e)}", severity="error")
|
||||
self.app.notify(f"⌠Failed to copy: {str(e)}", severity="error")
|
||||
event.stop()
|
||||
elif btn_id == "export_csv_btn":
|
||||
self._start_export_csv_operation()
|
||||
@@ -407,6 +421,9 @@ class AgentMoveOperations(Widget):
|
||||
elif btn_id == "otp_gen_btn":
|
||||
self._start_OTP_gen_operation()
|
||||
event.stop()
|
||||
elif btn_id == "exec_history_btn":
|
||||
self._start_execution_history_operation()
|
||||
event.stop()
|
||||
|
||||
def _start_local_approval_operation(self) -> None:
|
||||
"""
|
||||
@@ -435,7 +452,7 @@ class AgentMoveOperations(Widget):
|
||||
self.operation_in_progress = True
|
||||
|
||||
status_label = self.query_one("#status_label", Static)
|
||||
status_label.update("✔️ Moving agents to local approval...")
|
||||
status_label.update("âœ”ï¸ Moving agents to local approval...")
|
||||
|
||||
# Get API from app
|
||||
api = self.app.api
|
||||
@@ -470,12 +487,12 @@ class AgentMoveOperations(Widget):
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error during local approval operation: {e}")
|
||||
status_label.update(f"❌ Error: {str(e)}")
|
||||
status_label.update(f"⌠Error: {str(e)}")
|
||||
self.operation_in_progress = False
|
||||
return
|
||||
|
||||
self.operation_in_progress = False
|
||||
status_label.update("✅ Operation complete!")
|
||||
status_label.update("✅ Operation complete!")
|
||||
|
||||
# Display results in the widget
|
||||
self._display_results("Local Approval Mode", successful, unsuccessful)
|
||||
@@ -518,9 +535,9 @@ class AgentMoveOperations(Widget):
|
||||
file_path = os.path.join(str(path), filename)
|
||||
df.to_csv(file_path, index=False)
|
||||
successful.append(file_path)
|
||||
status_label.update(f"✅ Exported to {file_path}")
|
||||
status_label.update(f"✅ Exported to {file_path}")
|
||||
except Exception:
|
||||
status_label.update("❌ Failed")
|
||||
status_label.update("⌠Failed")
|
||||
|
||||
self.operation_in_progress = False
|
||||
|
||||
@@ -564,7 +581,7 @@ class AgentMoveOperations(Widget):
|
||||
self.operation_in_progress = True
|
||||
|
||||
status_label = self.query_one("#status_label", Static)
|
||||
status_label.update("🔄 Toggling enforcement mode...")
|
||||
status_label.update("🔄 Toggling enforcement mode...")
|
||||
|
||||
# Get API from app
|
||||
api = self.app.api
|
||||
@@ -600,12 +617,12 @@ class AgentMoveOperations(Widget):
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error during toggle enforcement operation: {e}")
|
||||
status_label.update(f"❌ Error: {str(e)}")
|
||||
status_label.update(f"⌠Error: {str(e)}")
|
||||
self.operation_in_progress = False
|
||||
return
|
||||
|
||||
self.operation_in_progress = False
|
||||
status_label.update("✅ Operation complete!")
|
||||
status_label.update("✅ Operation complete!")
|
||||
|
||||
# Display results in the widget
|
||||
self._display_results("Toggle Audit/Enforcement", successful, unsuccessful)
|
||||
@@ -670,7 +687,7 @@ class AgentMoveOperations(Widget):
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading policies: {e}")
|
||||
status_label.update(f"❌ Error: {str(e)}")
|
||||
status_label.update(f"⌠Error: {str(e)}")
|
||||
self.operation_in_progress = False
|
||||
self.selected_operation = ""
|
||||
self.app.notify(f"Failed to load policies: {str(e)}", severity="error")
|
||||
@@ -681,6 +698,35 @@ class AgentMoveOperations(Widget):
|
||||
|
||||
self.app.push_screen(OTPWorkflowScreen(self.agents))
|
||||
|
||||
def _start_execution_history_operation(self) -> None:
|
||||
"""
|
||||
Launch the execution history viewer for selected agents.
|
||||
|
||||
This operation opens a new screen that allows the user to:
|
||||
1. Select a date range for execution history
|
||||
2. Fetch execution logs for all selected agents
|
||||
3. View the results in a table
|
||||
4. Export the results to CSV
|
||||
|
||||
The screen is pushed onto the screen stack, allowing the user to return
|
||||
to this screen when done.
|
||||
"""
|
||||
status_label = self.query_one("#status_label", Static)
|
||||
status_label.update("Opening execution history viewer...")
|
||||
|
||||
try:
|
||||
# Push the execution history screen
|
||||
self.app.push_screen(ExecutionHistoryScreen(self.agents))
|
||||
logger.info(
|
||||
f"Opened execution history viewer for {len(self.agents)} agents"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to open execution history viewer: {e}")
|
||||
status_label.update(f"❌ Error: {str(e)}")
|
||||
self.app.notify(
|
||||
f"Failed to open execution history: {str(e)}", severity="error"
|
||||
)
|
||||
|
||||
def _execute_move_to_policy(self, target_policy) -> None:
|
||||
"""
|
||||
Execute the actual move of agents to the selected policy.
|
||||
|
||||
Reference in New Issue
Block a user