Continuing work removing legacy functions and implementing UI changes
Build Library / Build Library (push) Failing after 3m58s

This commit is contained in:
2025-11-10 15:17:07 -05:00
parent a498a94199
commit d9cbce6175
10 changed files with 188 additions and 159 deletions
+114 -68
View File
@@ -18,9 +18,13 @@ Dependencies:
- flows.localApproval: Local approval workflow handling
"""
from dataclasses import asdict
from datetime import datetime
import logging
import os
from typing import List
import pandas as pd
from textual.containers import Horizontal, Vertical
from textual.css.query import NoMatches
from textual.message import Message
@@ -29,7 +33,9 @@ from textual.widget import Widget
from textual.widgets import Button, DataTable, Header, Static, TextArea
from models.agent import Agent
from screens.otpworkflowscreen import OTPWorkflowScreen
from screens.policyselectorscreen import PolicySelectorScreen
from widgets.OTP_generate import OTPGenerator
logger = logging.getLogger(__name__)
@@ -133,18 +139,24 @@ class AgentMoveOperations(Widget):
Handles NoMatches exceptions gracefully in case buttons are not yet rendered.
"""
try:
export_csv_btn = self.query_one("#export_csv_btn", Button)
local_approval_btn = self.query_one("#local_approval_btn", Button)
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)
# If operation in progress, disable all
if self.operation_in_progress:
otp_gen_btn = True
export_csv_btn.disabled = True
local_approval_btn.disabled = True
toggle_enforcement_btn.disabled = True
other_policy_btn.disabled = True
else:
# If an operation was selected, keep it disabled, enable others
# If an operation was selected, disable
if self.selected_operation:
otp_gen_btn.disabled = self.selected_operation == "otp_gen"
export_csv_btn.disabled = self.selected_operation == "export_csv"
local_approval_btn.disabled = (
self.selected_operation == "local_approval"
)
@@ -156,6 +168,8 @@ class AgentMoveOperations(Widget):
)
else:
# Enable all buttons
otp_gen_btn = False
export_csv_btn = False
local_approval_btn.disabled = False
toggle_enforcement_btn.disabled = False
other_policy_btn.disabled = False
@@ -189,21 +203,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)")
@@ -231,7 +245,7 @@ class AgentMoveOperations(Widget):
It builds a two-column layout with:
- Left side: Agent table showing selected agents and their current policies
- Right side: Operation buttons and results display area
- Bottom: Navigation buttons (Back, Reset)
- Bottom: Navigation buttons (Back)
The layout is responsive with:
- Agent table: 2/3 width
@@ -240,7 +254,7 @@ class AgentMoveOperations(Widget):
"""
yield Header(show_clock=True, icon="")
title_text = Static(
f"↔️ Move 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)
@@ -251,7 +265,7 @@ class AgentMoveOperations(Widget):
# Left side - Agent list
with Vertical() as left_side:
left_side.styles.width = "2fr"
left_side.styles.width = "3fr"
left_side.styles.height = "auto"
agents_label = Static("Selected Agents:")
@@ -266,7 +280,8 @@ class AgentMoveOperations(Widget):
# Right side - Operation buttons
with Vertical() as right_side:
right_side.styles.width = "1fr"
right_side.styles.width = "2fr"
right_side.styles.margin = (0, 1, 0, 1)
right_side.styles.height = "auto"
operations_label = Static("Operations:")
@@ -274,13 +289,23 @@ class AgentMoveOperations(Widget):
yield operations_label
# Operation buttons
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.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"
)
@@ -300,39 +325,10 @@ class AgentMoveOperations(Widget):
status_label.styles.margin = (2, 0, 0, 0)
yield status_label
# Results display area (initially hidden)
with Vertical(id="results_container") as results_container:
results_container.styles.height = "auto"
results_container.styles.margin = (1, 0, 0, 0)
results_container.styles.display = "none"
results_label = Static("📊 Results:", id="results_label")
results_label.styles.margin = (0, 0, 0, 0)
yield results_label
results_text = TextArea(id="results_text", read_only=True)
results_text.styles.height = 15
results_text.styles.margin = (0, 0, 1, 0)
yield results_text
copy_results_btn = Button(
"📋 Copy Results to Clipboard", id="copy_results_btn"
)
copy_results_btn.styles.width = "100%"
yield copy_results_btn
# Bottom buttons
with Horizontal() as button_row:
button_row.styles.height = "auto"
button_row.styles.margin = (1, 0, 0, 0)
back_button = Button("← Back", id="back_button")
back_button.styles.width = "1fr"
yield back_button
reset_button = Button("🔄 Reset Selection", id="reset_button")
reset_button.styles.width = "1fr"
yield reset_button
back_button = Button("← Back", id="back_button")
back_button.styles.width = "50%"
back_button.styles.margin = (0, 1, 1, 0)
yield back_button
def on_mount(self) -> None:
"""
@@ -359,13 +355,15 @@ class AgentMoveOperations(Widget):
self._update_button_states()
def on_otp_generator_otp_info(self, message: OTPGenerator.OTPInfo) -> None:
"""Handle OTP generation request - call the actual OTP generation function."""
def on_button_pressed(self, event: Button.Pressed):
"""
Handle button press events from the widget.
This Textual event handler routes button presses to appropriate actions:
- back_button: Pop this screen (return to parent)
- reset_button: Clear operation state and hide results
- copy_results_btn: Copy results text to clipboard (requires pyperclip)
- local_approval_btn: Start local approval operation
- toggle_enforcement_btn: Start toggle audit/enforcement operation
@@ -380,21 +378,8 @@ class AgentMoveOperations(Widget):
btn_id = event.button.id
if btn_id == "back_button":
self.app.pop_screen()
event.stop()
elif btn_id == "reset_button":
# Reset operation selection
self.selected_operation = ""
self.operation_in_progress = False
status_label = self.query_one("#status_label", Static)
status_label.update("")
# Hide results
try:
results_container = self.query_one("#results_container", Vertical)
results_container.styles.display = "none"
except NoMatches:
pass
while len(self.app.screen_stack) > 2:
self.app.pop_screen()
event.stop()
elif btn_id == "copy_results_btn":
@@ -404,18 +389,21 @@ 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")
event.stop()
elif btn_id == "export_csv_btn":
self._start_export_csv_operation()
event.stop()
elif btn_id == "local_approval_btn":
self._start_local_approval_operation()
@@ -428,6 +416,9 @@ class AgentMoveOperations(Widget):
elif btn_id == "other_policy_btn":
self._start_other_policy_operation()
event.stop()
elif btn_id == "otp_gen_btn":
self._start_OTP_gen_operation()
event.stop()
def _start_local_approval_operation(self) -> None:
"""
@@ -456,7 +447,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
@@ -491,12 +482,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)
@@ -508,14 +499,63 @@ class AgentMoveOperations(Widget):
)
)
def _start_export_csv_operation(self) -> None:
self.selected_operation = "export_csv"
self.operation_in_progress = True
successful = []
unsuccessful = []
status_label = self.query_one("#status_label", Static)
status_label.update("Exporting CSV...")
agents = self.agents
policies = self.app.policies
path = self.app.working_dir
try:
# Enrich each agent with policies and status text
for agent in agents:
agent.enrich_with_policies(policies)
# Convert each Agent to a dictionary, including all fields
data = []
for agent in agents:
row = asdict(agent)
# Remove the class-level status_map from the row
row.pop("status_map", None)
data.append(row)
# Create DataFrame
df = pd.DataFrame(data)
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
filename = f"agentsearch_{timestamp}.csv"
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}")
except Exception:
status_label.update("❌ Failed")
self.operation_in_progress = False
"""
# Display results in the widget
self._display_results("CSV Export", successful, unsuccessful)
# Also post message for potential parent handling
self.post_message(
self.OperationComplete(
"CSV Export", self.agents, successful, unsuccessful
)
)
"""
def _start_toggle_enforcement_operation(self) -> None:
"""
Toggle agents between enforcement and audit policy modes.
This operation intelligently switches each agent between enforcement and
audit modes based on its current state:
- If agent.groupid is in POLICY_MAP_ENF_AUD: currently enforcing â†' move to audit
- Otherwise: currently in audit â†' move to enforcement
- If agent.groupid is in POLICY_MAP_ENF_AUD: currently enforcing , move to audit
- Otherwise: currently in audit, move to enforcement
The operation:
- Retrieves the enforcement/audit policy relationship map from protected config
@@ -570,7 +610,7 @@ 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
@@ -640,11 +680,17 @@ 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")
def _start_OTP_gen_operation(self) -> None:
status_label = self.query_one("#status_label", Static)
status_label.update("Generating OTP.")
self.app.push_screen(OTPWorkflowScreen(self.agents))
def _execute_move_to_policy(self, target_policy) -> None:
"""
Execute the actual move of agents to the selected policy.