Added OTP Activity Review WIP
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import logging
|
||||
from typing import List
|
||||
from typing import List, Optional
|
||||
|
||||
from textual.containers import Horizontal, Vertical
|
||||
from textual.css.query import NoMatches
|
||||
@@ -31,7 +31,11 @@ class OTPGenerator(Widget):
|
||||
|
||||
class OTPInfo(Message):
|
||||
def __init__(
|
||||
self, devices: List[Agent], requestor: str, reasoning: str, duration: int
|
||||
self,
|
||||
devices: Optional[List[Agent]],
|
||||
requestor: str,
|
||||
reasoning: str,
|
||||
duration: int,
|
||||
):
|
||||
super().__init__()
|
||||
self.devices = devices
|
||||
@@ -243,7 +247,7 @@ class OTPGenerator(Widget):
|
||||
self.otp_generated = True
|
||||
|
||||
# Access API from the app - this is the key change!
|
||||
api = self.app.api
|
||||
api = self.app.api # type: ignore
|
||||
|
||||
output_lines = [
|
||||
"Requested OTP Codes:",
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
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;
|
||||
}
|
||||
"""
|
||||
@@ -1,6 +1,6 @@
|
||||
import difflib
|
||||
import re
|
||||
from typing import List
|
||||
from typing import List, Optional
|
||||
|
||||
from textual.containers import Horizontal, Vertical
|
||||
from textual.css.query import NoMatches
|
||||
@@ -25,7 +25,7 @@ class MultiAgentSelector(Widget):
|
||||
super().__init__()
|
||||
self.selected_agents = selected_agents
|
||||
|
||||
def __init__(self, all_agents: List[Agent]):
|
||||
def __init__(self, all_agents: Optional[List[Agent]]):
|
||||
super().__init__()
|
||||
self.all_agents = all_agents
|
||||
self._match_type = "exact"
|
||||
|
||||
+55
-12
@@ -4,7 +4,7 @@ import logging
|
||||
from rich.text import Text
|
||||
from textual.containers import Horizontal, Vertical
|
||||
from textual.widget import Widget
|
||||
from textual.widgets import Input, OptionList, Static, Tree
|
||||
from textual.widgets import Input, OptionList, Static, Switch, Tree
|
||||
from textual.widgets.option_list import Option
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -19,29 +19,49 @@ class PolicyTreeWidget(Widget):
|
||||
self.devices = devices
|
||||
self.last_highlighted_node = None
|
||||
self.leaf_counts = defaultdict(int)
|
||||
self.match_type = "Count" # Default to sorting by count
|
||||
|
||||
def compose(self):
|
||||
# Create the switch and its label
|
||||
switch = Switch(value=False, id="match_switch")
|
||||
switch.styles.margin = (0, 0, 0, 0) # top, right, bottom, left
|
||||
switch.styles.padding = (0, 0, 0, 0)
|
||||
|
||||
switch_label = Static("Sort: Count", id="match_switch_label")
|
||||
switch_label.styles.margin = (1, 0, 0, 0)
|
||||
switch_label.styles.padding = (0, 0, 0, 0)
|
||||
|
||||
# Create the tree
|
||||
policy_tree = Tree("", id="policy_tree") # Label set in on_mount
|
||||
policy_tree.styles.width = "2fr"
|
||||
policy_tree.styles.height = "100%"
|
||||
|
||||
# Create the search box and details pane
|
||||
label = Static("Device Search:")
|
||||
search_box = Input(
|
||||
placeholder="Search policies or devices...", id="tree_search"
|
||||
)
|
||||
details_pane = Static("", id="details_pane")
|
||||
|
||||
# Layout the UI
|
||||
with Horizontal():
|
||||
yield policy_tree
|
||||
with Vertical() as right_pane:
|
||||
right_pane.styles.width = "3fr"
|
||||
# Use a Horizontal container for the switch and label
|
||||
with Horizontal() as switch_container:
|
||||
switch_container.styles.height = 3
|
||||
switch_container.styles.margin = (0, 0, 0, 1)
|
||||
switch_container.styles.padding = (0, 0, 0, 0)
|
||||
yield switch
|
||||
yield switch_label
|
||||
# Add the search box and details pane
|
||||
yield label
|
||||
yield search_box
|
||||
yield details_pane
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self._precompute_leaf_counts()
|
||||
|
||||
# Update root label with total leaf count
|
||||
total_leaves = sum(
|
||||
self.leaf_counts.get(policy.groupid, 0)
|
||||
@@ -50,8 +70,9 @@ class PolicyTreeWidget(Widget):
|
||||
)
|
||||
policy_tree = self.query_one("#policy_tree", Tree)
|
||||
policy_tree.root.set_label(f"Agents in Policies: ({total_leaves})")
|
||||
|
||||
self._build_tree()
|
||||
# Expand the root node
|
||||
policy_tree.root.expand()
|
||||
|
||||
def _precompute_leaf_counts(self):
|
||||
"""Precompute leaf counts for each policy group."""
|
||||
@@ -76,15 +97,21 @@ class PolicyTreeWidget(Widget):
|
||||
|
||||
def _build_tree(self):
|
||||
policy_tree = self.query_one("#policy_tree", Tree)
|
||||
policy_tree.clear() # Clear existing nodes
|
||||
node_map = {}
|
||||
|
||||
# Sort top-level policies
|
||||
top_policies = [
|
||||
p for p in self.policies if p.parent == "global-policy-settings"
|
||||
]
|
||||
top_policies.sort(
|
||||
key=lambda p: self.leaf_counts.get(p.groupid, 0), reverse=True
|
||||
)
|
||||
|
||||
# Sort by count (default) or alphabetically
|
||||
if getattr(self, "match_type", "Count") == "Count":
|
||||
top_policies.sort(
|
||||
key=lambda p: self.leaf_counts.get(p.groupid, 0), reverse=True
|
||||
)
|
||||
else: # Alphabetical
|
||||
top_policies.sort(key=lambda p: p.name.lower())
|
||||
|
||||
for policy in top_policies:
|
||||
label = f"{policy.name} ({self.leaf_counts.get(policy.groupid, 0)})"
|
||||
@@ -98,9 +125,13 @@ class PolicyTreeWidget(Widget):
|
||||
children_by_parent[policy.parent].append(policy)
|
||||
|
||||
for parent_id, children in children_by_parent.items():
|
||||
children.sort(
|
||||
key=lambda p: self.leaf_counts.get(p.groupid, 0), reverse=True
|
||||
)
|
||||
if getattr(self, "match_type", "Count") == "Count":
|
||||
children.sort(
|
||||
key=lambda p: self.leaf_counts.get(p.groupid, 0), reverse=True
|
||||
)
|
||||
else: # Alphabetical
|
||||
children.sort(key=lambda p: p.name.lower())
|
||||
|
||||
parent_node = node_map.get(parent_id)
|
||||
if parent_node:
|
||||
for policy in children:
|
||||
@@ -108,12 +139,17 @@ class PolicyTreeWidget(Widget):
|
||||
node = parent_node.add(label=label, data=policy)
|
||||
node_map[policy.groupid] = node
|
||||
|
||||
# Add devices (leaf nodes)
|
||||
# Add devices (leaf nodes) - always sort alphabetically
|
||||
devices_by_group = defaultdict(list)
|
||||
for device in self.devices:
|
||||
group_id = device.groupid
|
||||
devices_by_group[device.groupid].append(device)
|
||||
|
||||
for group_id, devices in devices_by_group.items():
|
||||
devices.sort(key=lambda d: d.hostname.lower()) # Always sort alphabetically
|
||||
parent_node = node_map.get(group_id)
|
||||
if parent_node:
|
||||
parent_node.add(label=device.hostname, data=device)
|
||||
for device in devices:
|
||||
parent_node.add(label=device.hostname, data=device)
|
||||
|
||||
def _collect_tree_nodes(self, node, all_nodes):
|
||||
all_nodes.append(node)
|
||||
@@ -157,6 +193,13 @@ class PolicyTreeWidget(Widget):
|
||||
|
||||
message.stop()
|
||||
|
||||
def on_switch_changed(self, event: Switch.Changed):
|
||||
self.match_type = "Alpha" if event.value else "Count"
|
||||
self.query_one("#match_switch_label", Static).update(
|
||||
f"Sort: {self.match_type.capitalize()}"
|
||||
)
|
||||
self._build_tree()
|
||||
|
||||
def on_input_submitted(self, message: Input.Submitted) -> None:
|
||||
self._remove_match_selector()
|
||||
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
from textual.color import Color
|
||||
|
||||
|
||||
def get_retro_terminal_theme():
|
||||
from textual.theme import Theme
|
||||
|
||||
return Theme(
|
||||
name="retro-terminal",
|
||||
background=Color.parse("#000000"),
|
||||
primary=Color.parse("#00ff00"),
|
||||
secondary=Color.parse("#00aa00"),
|
||||
success=Color.parse("#00ff00"),
|
||||
warning=Color.parse("#ffff00"),
|
||||
error=Color.parse("#ff0000"),
|
||||
surface=Color.parse("#071802"),
|
||||
)
|
||||
|
||||
|
||||
RETRO_TERMINAL_CSS = """
|
||||
/* Retro terminal CRT effect */
|
||||
Screen {
|
||||
align: center middle;
|
||||
background: $background;
|
||||
color: $text;
|
||||
}
|
||||
|
||||
/* Blocky, pixelated widgets */
|
||||
.widget {
|
||||
border: tall $primary;
|
||||
background: $surface;
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
/* Monospaced font */
|
||||
* {
|
||||
font-family: "Courier New", monospace;
|
||||
}
|
||||
"""
|
||||
Reference in New Issue
Block a user