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:
@@ -1,41 +1,24 @@
|
|||||||
|
# otp_workflow_screen.py
|
||||||
|
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from textual.app import ComposeResult
|
from textual.app import ComposeResult
|
||||||
from textual.screen import Screen
|
from textual.screen import Screen
|
||||||
|
|
||||||
from models.agent import Agent
|
from models.agent import Agent
|
||||||
from widgets.multiagentselector import MultiAgentSelector
|
|
||||||
from widgets.OTP_generate import OTPGenerator
|
from widgets.OTP_generate import OTPGenerator
|
||||||
|
|
||||||
|
|
||||||
class OTPWorkflowScreen(Screen):
|
class OTPWorkflowScreen(Screen):
|
||||||
"""Screen that handles the OTP generation workflow."""
|
"""Screen that handles the OTP generation workflow without agent selection."""
|
||||||
|
|
||||||
def __init__(self, all_agents: List[Agent]):
|
def __init__(self, selected_agents: List[Agent]):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.all_agents = all_agents
|
self.selected_agents = selected_agents
|
||||||
self.selected_devices = None
|
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
"""Start with the multi-agent selector."""
|
"""Directly show the OTP generator for the selected agents."""
|
||||||
yield MultiAgentSelector(self.all_agents)
|
yield OTPGenerator(self.selected_agents)
|
||||||
|
|
||||||
def on_multi_agent_selector_agents_selected(
|
|
||||||
self, message: MultiAgentSelector.AgentsSelected
|
|
||||||
) -> None:
|
|
||||||
"""Handle selected agents - switch to OTP generator."""
|
|
||||||
self.selected_devices = message.selected_agents
|
|
||||||
|
|
||||||
# Remove the MultiAgentSelector
|
|
||||||
selector = self.query_one(MultiAgentSelector)
|
|
||||||
selector.remove()
|
|
||||||
|
|
||||||
# Mount the OTPGenerator with the selected Agent objects
|
|
||||||
# No need to pass API - it will access self.app.api directly
|
|
||||||
self.mount(OTPGenerator(self.selected_devices))
|
|
||||||
|
|
||||||
def on_otp_generator_otp_info(self, message: OTPGenerator.OTPInfo) -> None:
|
def on_otp_generator_otp_info(self, message: OTPGenerator.OTPInfo) -> None:
|
||||||
"""Handle OTP generation request - call the actual OTP generation function."""
|
"""Handle OTP generation request - pass it up to the app level if needed."""
|
||||||
# This will be handled by the main app, but we can also do it here
|
|
||||||
# For now, just pass it up to the app level
|
|
||||||
pass
|
|
||||||
|
|||||||
+14
-37
@@ -20,18 +20,17 @@ from textual.widgets import (
|
|||||||
|
|
||||||
from flows.otp import otp_activities_by_agent, otp_revoke
|
from flows.otp import otp_activities_by_agent, otp_revoke
|
||||||
from flows.prepPolicy import menu_policy_enforce
|
from flows.prepPolicy import menu_policy_enforce
|
||||||
from flows.quietAgent import findQuietAgents
|
|
||||||
from models.agent import Agent
|
from models.agent import Agent
|
||||||
from models.policy import Policy
|
from models.policy import Policy
|
||||||
from screens.moveagentworkflowscreen import MoveAgentWorkflowScreen
|
from screens.moveagentworkflowscreen import MoveAgentWorkflowScreen
|
||||||
from screens.otpworkflowscreen import OTPWorkflowScreen
|
from screens.otpworkflowscreen import OTPWorkflowScreen
|
||||||
from services.agenthandler import findAgents, moveAgents, toggleEnforcement
|
|
||||||
from services.API import AirlockAPIWrapper
|
from services.API import AirlockAPIWrapper
|
||||||
from services.policyhandler import confirmUpdateAfromE
|
from services.policyhandler import confirmUpdateAfromE
|
||||||
from utils.configmanager import load_env
|
from utils.configmanager import load_env
|
||||||
from utils.setup import get_base_directory, load_user_config
|
from utils.setup import get_base_directory, load_user_config
|
||||||
from utils.utils import open_directory
|
from utils.utils import open_directory
|
||||||
from widgets.agentmoveoperations import AgentMoveOperations
|
from widgets.agentmoveoperations import AgentMoveOperations
|
||||||
|
from widgets.amber_terminal_theme import get_amber_terminal_theme
|
||||||
from widgets.multiagentselector import MultiAgentSelector
|
from widgets.multiagentselector import MultiAgentSelector
|
||||||
from widgets.OTP_generate import OTPGenerator
|
from widgets.OTP_generate import OTPGenerator
|
||||||
from widgets.policytreewidget import PolicyTreeWidget
|
from widgets.policytreewidget import PolicyTreeWidget
|
||||||
@@ -105,24 +104,18 @@ class MainMenuScreen(Screen):
|
|||||||
current_tab = reactive("")
|
current_tab = reactive("")
|
||||||
|
|
||||||
BUTTON_DEFS = {
|
BUTTON_DEFS = {
|
||||||
"find": [
|
"agent_actions": [
|
||||||
("🔍 - Device Search", "find_device_button"),
|
(
|
||||||
|
"🖥️ - Find, Move, or Generate OTP for Agents",
|
||||||
|
"move_agent_workflow_button",
|
||||||
|
),
|
||||||
("🔇 - Find Quiet Hosts", "find_quiet_button"),
|
("🔇 - Find Quiet Hosts", "find_quiet_button"),
|
||||||
],
|
],
|
||||||
"move": [
|
|
||||||
("🔄 - Move Agent Workflow", "move_agent_workflow_button"),
|
|
||||||
("✅ - Move to local approval", "move_local_button"),
|
|
||||||
("🔄 - Move to Audit/Enforcement", "move_audit_button"),
|
|
||||||
("🔀 - Move - Other", "move_other_button"),
|
|
||||||
],
|
|
||||||
"otp": [
|
|
||||||
("🎫 - Generate OTPs", "otp_generate_button"),
|
|
||||||
("📊 - OTP Activities By Agent", "otp_activities_button"),
|
|
||||||
("❌ - Revoke OTPs", "otp_revoke_button"),
|
|
||||||
],
|
|
||||||
"policy": [
|
"policy": [
|
||||||
("🔒 - Prepare Policy For Enforcement", "policy_prep_button"),
|
("🔒 - Prepare Policy For Enforcement", "policy_prep_button"),
|
||||||
("🔄 - Update Audit Policies", "policy_audit_update_button"),
|
("🔄 - Update Audit Policies", "policy_audit_update_button"),
|
||||||
|
("📊 - OTP Activities By Agent", "otp_activities_button"),
|
||||||
|
("❌ - Revoke OTPs", "otp_revoke_button"),
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,23 +141,21 @@ class MainMenuScreen(Screen):
|
|||||||
yield Header(show_clock=True, icon="⚙")
|
yield Header(show_clock=True, icon="⚙")
|
||||||
|
|
||||||
tabs = [
|
tabs = [
|
||||||
Tab("Policy Tree", id="p_tree"),
|
Tab("Tree View", id="p_tree"),
|
||||||
Tab("Device Search", id="find"),
|
Tab("Agents", id="agent_actions"),
|
||||||
Tab("Move Agent", id="move"),
|
|
||||||
Tab("OTP", id="otp"),
|
|
||||||
Tab("Directory", id="dir"),
|
Tab("Directory", id="dir"),
|
||||||
Tab("Settings", id="settings"),
|
Tab("Settings", id="settings"),
|
||||||
]
|
]
|
||||||
|
|
||||||
if self.extras == "POLICYPREP":
|
if self.extras == "POLICYPREP":
|
||||||
tabs.insert(3, Tab("Policy Prep", id="policy"))
|
tabs.insert(2, Tab("Policy Prep", id="policy"))
|
||||||
|
|
||||||
yield Tabs(*tabs, id="tabs")
|
yield Tabs(*tabs, id="tabs")
|
||||||
yield Vertical(id="content")
|
yield Vertical(id="content")
|
||||||
yield Footer()
|
yield Footer()
|
||||||
|
|
||||||
def on_mount(self) -> None:
|
def on_mount(self) -> None:
|
||||||
self.switch_tab("find")
|
self.switch_tab("agent_actions")
|
||||||
|
|
||||||
# focus helpers
|
# focus helpers
|
||||||
def _get_content_buttons(self) -> list[Button]:
|
def _get_content_buttons(self) -> list[Button]:
|
||||||
@@ -225,7 +216,7 @@ class MainMenuScreen(Screen):
|
|||||||
def on_multi_agent_selector_agents_selected(
|
def on_multi_agent_selector_agents_selected(
|
||||||
self, message: MultiAgentSelector.AgentsSelected
|
self, message: MultiAgentSelector.AgentsSelected
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Handle selected agents from MultiAgentSelector."""
|
"""Handle selected agents from AgentSelector."""
|
||||||
global _PENDING_JOB
|
global _PENDING_JOB
|
||||||
selected_agents = message.selected_agents
|
selected_agents = message.selected_agents
|
||||||
logger.info("Selected agents: %s", selected_agents)
|
logger.info("Selected agents: %s", selected_agents)
|
||||||
@@ -324,26 +315,11 @@ class MainMenuScreen(Screen):
|
|||||||
logger.debug("Button pressed: %s", button_id)
|
logger.debug("Button pressed: %s", button_id)
|
||||||
|
|
||||||
match button_id:
|
match button_id:
|
||||||
case "find_device_button":
|
|
||||||
_PENDING_JOB = ("legacy", findAgents, (self.app.api, False), {})
|
|
||||||
case "find_quiet_button":
|
|
||||||
_PENDING_JOB = ("legacy", findQuietAgents, (self.app.api,), {})
|
|
||||||
case "move_agent_workflow_button":
|
case "move_agent_workflow_button":
|
||||||
# Push Move Agent workflow screen
|
# Push Move Agent workflow screen
|
||||||
self.app.push_screen(MoveAgentWorkflowScreen(self.app.devices))
|
self.app.push_screen(MoveAgentWorkflowScreen(self.app.devices))
|
||||||
event.stop()
|
event.stop()
|
||||||
return # Don't exit the app
|
return # Don't exit the app
|
||||||
case "move_local_button":
|
|
||||||
_PENDING_JOB = (
|
|
||||||
"legacy",
|
|
||||||
print,
|
|
||||||
("Move to local approval (placeholder)",),
|
|
||||||
{},
|
|
||||||
)
|
|
||||||
case "move_audit_button":
|
|
||||||
_PENDING_JOB = ("legacy", toggleEnforcement, (self.app.api,), {})
|
|
||||||
case "move_other_button":
|
|
||||||
_PENDING_JOB = ("legacy", moveAgents, (self.app.api,), {})
|
|
||||||
case "otp_generate_button":
|
case "otp_generate_button":
|
||||||
# NEW: Push OTP workflow screen instead of legacy function
|
# NEW: Push OTP workflow screen instead of legacy function
|
||||||
self.app.push_screen(OTPWorkflowScreen(self.app.devices))
|
self.app.push_screen(OTPWorkflowScreen(self.app.devices))
|
||||||
@@ -415,6 +391,7 @@ class Loxide(App):
|
|||||||
|
|
||||||
def on_mount(self, api: AirlockAPIWrapper) -> None:
|
def on_mount(self, api: AirlockAPIWrapper) -> None:
|
||||||
self.register_theme(get_retro_terminal_theme())
|
self.register_theme(get_retro_terminal_theme())
|
||||||
|
self.register_theme(get_amber_terminal_theme())
|
||||||
self.theme = self._textual_theme
|
self.theme = self._textual_theme
|
||||||
self.push_screen(MainMenuScreen(api))
|
self.push_screen(MainMenuScreen(api))
|
||||||
|
|
||||||
|
|||||||
@@ -194,7 +194,10 @@ class OTPGenerator(Widget):
|
|||||||
btn_id = event.button.id
|
btn_id = event.button.id
|
||||||
|
|
||||||
if btn_id == "back_button":
|
if btn_id == "back_button":
|
||||||
|
|
||||||
|
while len(self.app.screen_stack) > 2:
|
||||||
self.app.pop_screen()
|
self.app.pop_screen()
|
||||||
|
|
||||||
event.stop()
|
event.stop()
|
||||||
|
|
||||||
elif btn_id == "copy_clipboard_button":
|
elif btn_id == "copy_clipboard_button":
|
||||||
|
|||||||
+112
-66
@@ -18,9 +18,13 @@ Dependencies:
|
|||||||
- flows.localApproval: Local approval workflow handling
|
- flows.localApproval: Local approval workflow handling
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from dataclasses import asdict
|
||||||
|
from datetime import datetime
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
from textual.containers import Horizontal, Vertical
|
from textual.containers import Horizontal, Vertical
|
||||||
from textual.css.query import NoMatches
|
from textual.css.query import NoMatches
|
||||||
from textual.message import Message
|
from textual.message import Message
|
||||||
@@ -29,7 +33,9 @@ from textual.widget import Widget
|
|||||||
from textual.widgets import Button, DataTable, Header, Static, TextArea
|
from textual.widgets import Button, DataTable, Header, Static, TextArea
|
||||||
|
|
||||||
from models.agent import Agent
|
from models.agent import Agent
|
||||||
|
from screens.otpworkflowscreen import OTPWorkflowScreen
|
||||||
from screens.policyselectorscreen import PolicySelectorScreen
|
from screens.policyselectorscreen import PolicySelectorScreen
|
||||||
|
from widgets.OTP_generate import OTPGenerator
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -133,18 +139,24 @@ class AgentMoveOperations(Widget):
|
|||||||
Handles NoMatches exceptions gracefully in case buttons are not yet rendered.
|
Handles NoMatches exceptions gracefully in case buttons are not yet rendered.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
export_csv_btn = self.query_one("#export_csv_btn", Button)
|
||||||
local_approval_btn = self.query_one("#local_approval_btn", Button)
|
local_approval_btn = self.query_one("#local_approval_btn", Button)
|
||||||
toggle_enforcement_btn = self.query_one("#toggle_enforcement_btn", Button)
|
toggle_enforcement_btn = self.query_one("#toggle_enforcement_btn", Button)
|
||||||
other_policy_btn = self.query_one("#other_policy_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 operation in progress, disable all
|
||||||
if self.operation_in_progress:
|
if self.operation_in_progress:
|
||||||
|
otp_gen_btn = True
|
||||||
|
export_csv_btn.disabled = True
|
||||||
local_approval_btn.disabled = True
|
local_approval_btn.disabled = True
|
||||||
toggle_enforcement_btn.disabled = True
|
toggle_enforcement_btn.disabled = True
|
||||||
other_policy_btn.disabled = True
|
other_policy_btn.disabled = True
|
||||||
else:
|
else:
|
||||||
# If an operation was selected, keep it disabled, enable others
|
# If an operation was selected, disable
|
||||||
if self.selected_operation:
|
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 = (
|
local_approval_btn.disabled = (
|
||||||
self.selected_operation == "local_approval"
|
self.selected_operation == "local_approval"
|
||||||
)
|
)
|
||||||
@@ -156,6 +168,8 @@ class AgentMoveOperations(Widget):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Enable all buttons
|
# Enable all buttons
|
||||||
|
otp_gen_btn = False
|
||||||
|
export_csv_btn = False
|
||||||
local_approval_btn.disabled = False
|
local_approval_btn.disabled = False
|
||||||
toggle_enforcement_btn.disabled = False
|
toggle_enforcement_btn.disabled = False
|
||||||
other_policy_btn.disabled = False
|
other_policy_btn.disabled = False
|
||||||
@@ -189,21 +203,21 @@ class AgentMoveOperations(Widget):
|
|||||||
f"Operation: {operation_name}",
|
f"Operation: {operation_name}",
|
||||||
f"{'=' * 50}",
|
f"{'=' * 50}",
|
||||||
"",
|
"",
|
||||||
f"✅ Successful ({len(successful)}):",
|
f"✅ Successful ({len(successful)}):",
|
||||||
]
|
]
|
||||||
|
|
||||||
if successful:
|
if successful:
|
||||||
for agent, result in successful:
|
for agent, result in successful:
|
||||||
results_lines.append(f" • {agent.hostname}")
|
results_lines.append(f" ✅ {agent.hostname}")
|
||||||
else:
|
else:
|
||||||
results_lines.append(" (none)")
|
results_lines.append(" (none)")
|
||||||
|
|
||||||
results_lines.append("")
|
results_lines.append("")
|
||||||
results_lines.append(f"⌠Failed ({len(unsuccessful)}):")
|
results_lines.append(f"❌ Failed ({len(unsuccessful)}):")
|
||||||
|
|
||||||
if unsuccessful:
|
if unsuccessful:
|
||||||
for agent, error in unsuccessful:
|
for agent, error in unsuccessful:
|
||||||
results_lines.append(f" • {agent.hostname}: {error}")
|
results_lines.append(f" ❌ {agent.hostname}: {error}")
|
||||||
else:
|
else:
|
||||||
results_lines.append(" (none)")
|
results_lines.append(" (none)")
|
||||||
|
|
||||||
@@ -231,7 +245,7 @@ class AgentMoveOperations(Widget):
|
|||||||
It builds a two-column layout with:
|
It builds a two-column layout with:
|
||||||
- Left side: Agent table showing selected agents and their current policies
|
- Left side: Agent table showing selected agents and their current policies
|
||||||
- Right side: Operation buttons and results display area
|
- Right side: Operation buttons and results display area
|
||||||
- Bottom: Navigation buttons (Back, Reset)
|
- Bottom: Navigation buttons (Back)
|
||||||
|
|
||||||
The layout is responsive with:
|
The layout is responsive with:
|
||||||
- Agent table: 2/3 width
|
- Agent table: 2/3 width
|
||||||
@@ -240,7 +254,7 @@ class AgentMoveOperations(Widget):
|
|||||||
"""
|
"""
|
||||||
yield Header(show_clock=True, icon="⚙")
|
yield Header(show_clock=True, icon="⚙")
|
||||||
title_text = Static(
|
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",
|
id="move_ops_title",
|
||||||
)
|
)
|
||||||
title_text.styles.margin = (0, 0, 1, 0)
|
title_text.styles.margin = (0, 0, 1, 0)
|
||||||
@@ -251,7 +265,7 @@ class AgentMoveOperations(Widget):
|
|||||||
|
|
||||||
# Left side - Agent list
|
# Left side - Agent list
|
||||||
with Vertical() as left_side:
|
with Vertical() as left_side:
|
||||||
left_side.styles.width = "2fr"
|
left_side.styles.width = "3fr"
|
||||||
left_side.styles.height = "auto"
|
left_side.styles.height = "auto"
|
||||||
|
|
||||||
agents_label = Static("Selected Agents:")
|
agents_label = Static("Selected Agents:")
|
||||||
@@ -266,7 +280,8 @@ class AgentMoveOperations(Widget):
|
|||||||
|
|
||||||
# Right side - Operation buttons
|
# Right side - Operation buttons
|
||||||
with Vertical() as right_side:
|
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"
|
right_side.styles.height = "auto"
|
||||||
|
|
||||||
operations_label = Static("Operations:")
|
operations_label = Static("Operations:")
|
||||||
@@ -274,13 +289,23 @@ class AgentMoveOperations(Widget):
|
|||||||
yield operations_label
|
yield operations_label
|
||||||
|
|
||||||
# Operation buttons
|
# 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_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.width = "100%"
|
||||||
local_approval_btn.styles.margin = (0, 0, 1, 0)
|
local_approval_btn.styles.margin = (0, 0, 1, 0)
|
||||||
yield local_approval_btn
|
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_enforcement_btn = Button(
|
||||||
"🔄 Toggle Audit/Enforcement", id="toggle_enforcement_btn"
|
"🔄 Toggle Audit/Enforcement", id="toggle_enforcement_btn"
|
||||||
)
|
)
|
||||||
@@ -300,40 +325,11 @@ class AgentMoveOperations(Widget):
|
|||||||
status_label.styles.margin = (2, 0, 0, 0)
|
status_label.styles.margin = (2, 0, 0, 0)
|
||||||
yield status_label
|
yield status_label
|
||||||
|
|
||||||
# Results display area (initially hidden)
|
back_button = Button("← Back", id="back_button")
|
||||||
with Vertical(id="results_container") as results_container:
|
back_button.styles.width = "50%"
|
||||||
results_container.styles.height = "auto"
|
back_button.styles.margin = (0, 1, 1, 0)
|
||||||
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
|
yield back_button
|
||||||
|
|
||||||
reset_button = Button("🔄 Reset Selection", id="reset_button")
|
|
||||||
reset_button.styles.width = "1fr"
|
|
||||||
yield reset_button
|
|
||||||
|
|
||||||
def on_mount(self) -> None:
|
def on_mount(self) -> None:
|
||||||
"""
|
"""
|
||||||
Initialize widget after it has been mounted on the screen.
|
Initialize widget after it has been mounted on the screen.
|
||||||
@@ -359,13 +355,15 @@ class AgentMoveOperations(Widget):
|
|||||||
|
|
||||||
self._update_button_states()
|
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):
|
def on_button_pressed(self, event: Button.Pressed):
|
||||||
"""
|
"""
|
||||||
Handle button press events from the widget.
|
Handle button press events from the widget.
|
||||||
|
|
||||||
This Textual event handler routes button presses to appropriate actions:
|
This Textual event handler routes button presses to appropriate actions:
|
||||||
- back_button: Pop this screen (return to parent)
|
- 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)
|
- copy_results_btn: Copy results text to clipboard (requires pyperclip)
|
||||||
- local_approval_btn: Start local approval operation
|
- local_approval_btn: Start local approval operation
|
||||||
- toggle_enforcement_btn: Start toggle audit/enforcement operation
|
- toggle_enforcement_btn: Start toggle audit/enforcement operation
|
||||||
@@ -380,23 +378,10 @@ class AgentMoveOperations(Widget):
|
|||||||
btn_id = event.button.id
|
btn_id = event.button.id
|
||||||
|
|
||||||
if btn_id == "back_button":
|
if btn_id == "back_button":
|
||||||
|
while len(self.app.screen_stack) > 2:
|
||||||
self.app.pop_screen()
|
self.app.pop_screen()
|
||||||
event.stop()
|
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
|
|
||||||
event.stop()
|
|
||||||
|
|
||||||
elif btn_id == "copy_results_btn":
|
elif btn_id == "copy_results_btn":
|
||||||
try:
|
try:
|
||||||
results_text = self.query_one("#results_text", TextArea)
|
results_text = self.query_one("#results_text", TextArea)
|
||||||
@@ -404,18 +389,21 @@ class AgentMoveOperations(Widget):
|
|||||||
|
|
||||||
pyperclip.copy(results_text.text)
|
pyperclip.copy(results_text.text)
|
||||||
self.app.notify(
|
self.app.notify(
|
||||||
"✅ Results copied to clipboard!",
|
"📋✅ Results copied to clipboard!",
|
||||||
severity="information",
|
severity="information",
|
||||||
timeout=2,
|
timeout=2,
|
||||||
)
|
)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
self.app.notify(
|
self.app.notify(
|
||||||
"âš ï¸ pyperclip not installed. Run: pip install pyperclip",
|
"❌ pyperclip not installed. Run: pip install pyperclip",
|
||||||
severity="warning",
|
severity="warning",
|
||||||
)
|
)
|
||||||
except Exception as e:
|
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()
|
event.stop()
|
||||||
|
elif btn_id == "export_csv_btn":
|
||||||
|
self._start_export_csv_operation()
|
||||||
|
event.stop()
|
||||||
|
|
||||||
elif btn_id == "local_approval_btn":
|
elif btn_id == "local_approval_btn":
|
||||||
self._start_local_approval_operation()
|
self._start_local_approval_operation()
|
||||||
@@ -428,6 +416,9 @@ class AgentMoveOperations(Widget):
|
|||||||
elif btn_id == "other_policy_btn":
|
elif btn_id == "other_policy_btn":
|
||||||
self._start_other_policy_operation()
|
self._start_other_policy_operation()
|
||||||
event.stop()
|
event.stop()
|
||||||
|
elif btn_id == "otp_gen_btn":
|
||||||
|
self._start_OTP_gen_operation()
|
||||||
|
event.stop()
|
||||||
|
|
||||||
def _start_local_approval_operation(self) -> None:
|
def _start_local_approval_operation(self) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -456,7 +447,7 @@ class AgentMoveOperations(Widget):
|
|||||||
self.operation_in_progress = True
|
self.operation_in_progress = True
|
||||||
|
|
||||||
status_label = self.query_one("#status_label", Static)
|
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
|
# Get API from app
|
||||||
api = self.app.api
|
api = self.app.api
|
||||||
@@ -491,12 +482,12 @@ class AgentMoveOperations(Widget):
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error during local approval operation: {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
|
self.operation_in_progress = False
|
||||||
return
|
return
|
||||||
|
|
||||||
self.operation_in_progress = False
|
self.operation_in_progress = False
|
||||||
status_label.update("✅ Operation complete!")
|
status_label.update("✅ Operation complete!")
|
||||||
|
|
||||||
# Display results in the widget
|
# Display results in the widget
|
||||||
self._display_results("Local Approval Mode", successful, unsuccessful)
|
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:
|
def _start_toggle_enforcement_operation(self) -> None:
|
||||||
"""
|
"""
|
||||||
Toggle agents between enforcement and audit policy modes.
|
Toggle agents between enforcement and audit policy modes.
|
||||||
|
|
||||||
This operation intelligently switches each agent between enforcement and
|
This operation intelligently switches each agent between enforcement and
|
||||||
audit modes based on its current state:
|
audit modes based on its current state:
|
||||||
- If agent.groupid is in POLICY_MAP_ENF_AUD: currently enforcing â†' move to audit
|
- If agent.groupid is in POLICY_MAP_ENF_AUD: currently enforcing , move to audit
|
||||||
- Otherwise: currently in audit â†' move to enforcement
|
- Otherwise: currently in audit, move to enforcement
|
||||||
|
|
||||||
The operation:
|
The operation:
|
||||||
- Retrieves the enforcement/audit policy relationship map from protected config
|
- Retrieves the enforcement/audit policy relationship map from protected config
|
||||||
@@ -570,7 +610,7 @@ class AgentMoveOperations(Widget):
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error during toggle enforcement operation: {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
|
self.operation_in_progress = False
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -640,11 +680,17 @@ class AgentMoveOperations(Widget):
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error loading policies: {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.operation_in_progress = False
|
||||||
self.selected_operation = ""
|
self.selected_operation = ""
|
||||||
self.app.notify(f"Failed to load policies: {str(e)}", severity="error")
|
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:
|
def _execute_move_to_policy(self, target_policy) -> None:
|
||||||
"""
|
"""
|
||||||
Execute the actual move of agents to the selected policy.
|
Execute the actual move of agents to the selected policy.
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
from textual.color import Color
|
||||||
|
from textual.theme import Theme
|
||||||
|
|
||||||
|
|
||||||
|
def get_amber_terminal_theme():
|
||||||
|
"""Amber CRT theme with compensated brightness for blending."""
|
||||||
|
return Theme(
|
||||||
|
name="amber-terminal",
|
||||||
|
background=Color.parse("#000000"), # pure black
|
||||||
|
primary=Color.parse("#ffb733"), # bright amber
|
||||||
|
secondary=Color.parse("#e69500"), # strong amber
|
||||||
|
success=Color.parse("#ffb733"),
|
||||||
|
warning=Color.parse("#ffff66"),
|
||||||
|
error=Color.parse("#ff3300"),
|
||||||
|
surface=Color.parse("#3a1f00"), # brighter brown for blending
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
AMBER_TERMINAL_CSS = """
|
||||||
|
Screen {
|
||||||
|
align: center middle;
|
||||||
|
background: #000000; /* force black */
|
||||||
|
color: #ffb733; /* force amber text */
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget {
|
||||||
|
border: tall #ffb733; /* force amber border */
|
||||||
|
background: #3a1f00; /* compensated surface */
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
font-family: "Courier New", monospace;
|
||||||
|
}
|
||||||
|
"""
|
||||||
@@ -40,7 +40,7 @@ class MultiAgentSelector(Widget):
|
|||||||
|
|
||||||
def compose(self):
|
def compose(self):
|
||||||
yield Header(show_clock=True, icon="⚙")
|
yield Header(show_clock=True, icon="⚙")
|
||||||
title_text = Static("🖧 Multi-Agent Selector", id="selector_title")
|
title_text = Static("🖧 Agent Selector", id="selector_title")
|
||||||
title_text.styles.margin = (0, 0, 0, 1)
|
title_text.styles.margin = (0, 0, 0, 1)
|
||||||
yield title_text
|
yield title_text
|
||||||
|
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ class PolicySelector(Widget):
|
|||||||
- Clear Filter button
|
- Clear Filter button
|
||||||
- Confirm Selection button
|
- Confirm Selection button
|
||||||
- Policy table displaying available policies
|
- Policy table displaying available policies
|
||||||
- Back and Continue buttons for navigation
|
- Back buttons for navigation
|
||||||
"""
|
"""
|
||||||
yield Header(show_clock=True, icon="⚙")
|
yield Header(show_clock=True, icon="⚙")
|
||||||
title_text = Static(
|
title_text = Static(
|
||||||
@@ -144,6 +144,11 @@ class PolicySelector(Widget):
|
|||||||
selected_label.styles.margin = (2, 0, 1, 0)
|
selected_label.styles.margin = (2, 0, 1, 0)
|
||||||
yield selected_label
|
yield selected_label
|
||||||
|
|
||||||
|
cancel_button = Button("← Back", id="back_button")
|
||||||
|
cancel_button.styles.width = "100%"
|
||||||
|
cancel_button.styles.margin = (1, 0, 1, 0)
|
||||||
|
yield cancel_button
|
||||||
|
|
||||||
# Right side - Policy table
|
# Right side - Policy table
|
||||||
with Vertical() as right_side:
|
with Vertical() as right_side:
|
||||||
right_side.styles.width = "2fr"
|
right_side.styles.width = "2fr"
|
||||||
@@ -158,23 +163,6 @@ class PolicySelector(Widget):
|
|||||||
policy_table.styles.margin = (1, 0, 1, 0)
|
policy_table.styles.margin = (1, 0, 1, 0)
|
||||||
yield policy_table
|
yield policy_table
|
||||||
|
|
||||||
# Bottom buttons
|
|
||||||
with Horizontal() as button_row:
|
|
||||||
button_row.styles.height = "auto"
|
|
||||||
button_row.styles.margin = (1, 0, 0, 0)
|
|
||||||
|
|
||||||
cancel_button = Button("✕ Cancel", id="back_button", variant="error")
|
|
||||||
cancel_button.styles.width = "1fr"
|
|
||||||
yield cancel_button
|
|
||||||
|
|
||||||
continue_button = Button(
|
|
||||||
"▶ Continue",
|
|
||||||
id="continue_button",
|
|
||||||
variant="primary",
|
|
||||||
)
|
|
||||||
continue_button.styles.width = "1fr"
|
|
||||||
continue_button.styles.margin = (0, 0, 0, 1)
|
|
||||||
yield continue_button
|
|
||||||
yield Footer()
|
yield Footer()
|
||||||
|
|
||||||
def on_mount(self) -> None:
|
def on_mount(self) -> None:
|
||||||
@@ -239,7 +227,6 @@ class PolicySelector(Widget):
|
|||||||
- filter_button (Apply Filter): Filter policies with wildcard support
|
- filter_button (Apply Filter): Filter policies with wildcard support
|
||||||
- clear_filter_button: Clear filter and show all policies
|
- clear_filter_button: Clear filter and show all policies
|
||||||
- confirm_button: Confirm selection and post message
|
- confirm_button: Confirm selection and post message
|
||||||
- continue_button: Continue without posting message
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
event (Button.Pressed): The button press event.
|
event (Button.Pressed): The button press event.
|
||||||
@@ -247,6 +234,7 @@ class PolicySelector(Widget):
|
|||||||
btn_id = event.button.id
|
btn_id = event.button.id
|
||||||
|
|
||||||
if btn_id == "back_button":
|
if btn_id == "back_button":
|
||||||
|
while len(self.app.screen_stack) > 2:
|
||||||
self.app.pop_screen()
|
self.app.pop_screen()
|
||||||
event.stop()
|
event.stop()
|
||||||
|
|
||||||
@@ -262,10 +250,6 @@ class PolicySelector(Widget):
|
|||||||
self._confirm_selection()
|
self._confirm_selection()
|
||||||
event.stop()
|
event.stop()
|
||||||
|
|
||||||
elif btn_id == "continue_button":
|
|
||||||
self.app.pop_screen()
|
|
||||||
event.stop()
|
|
||||||
|
|
||||||
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
|
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
|
||||||
"""
|
"""
|
||||||
Handle row selection in the policy table.
|
Handle row selection in the policy table.
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ def get_retro_terminal_theme():
|
|||||||
success=Color.parse("#00ff00"),
|
success=Color.parse("#00ff00"),
|
||||||
warning=Color.parse("#ffff00"),
|
warning=Color.parse("#ffff00"),
|
||||||
error=Color.parse("#ff0000"),
|
error=Color.parse("#ff0000"),
|
||||||
surface=Color.parse("#111111"),
|
surface=Color.parse("#071802"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ class ThemeSelector(Widget):
|
|||||||
("Flexoki", "flexoki"),
|
("Flexoki", "flexoki"),
|
||||||
("Catppuccin Latte", "catppuccin-latte"),
|
("Catppuccin Latte", "catppuccin-latte"),
|
||||||
("Solarized Light", "solarized-light"),
|
("Solarized Light", "solarized-light"),
|
||||||
("Retro Terminal", "retro-terminal"), # your custom theme
|
("Retro Terminal", "retro-terminal"),
|
||||||
|
("Amber Terminal", "amber-terminal"), # your custom theme
|
||||||
]
|
]
|
||||||
|
|
||||||
def compose(self):
|
def compose(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user