Continuing work removing legacy functions and implementing UI changes
Build Library / Build Library (push) Failing after 3m58s
Build Library / Build Library (push) Failing after 3m58s
This commit is contained in:
+114
-68
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user