policyprepworkflow: enhancements and fixes
- Added row copy functionality (Ctrl+C) - Improved row selection visual contrast - Fixed data state issues when navigating between stages Server Log tab - Added new Server Log tab to the main application - Implemented live filtering with wildcard support - Enabled auto-refresh capability Multiagent selector - Added file loading support for device lists
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import difflib
|
||||
from pathlib import Path
|
||||
import re
|
||||
from typing import List, Optional
|
||||
|
||||
@@ -57,7 +58,7 @@ class MultiAgentSelector(Widget):
|
||||
|
||||
def compose(self):
|
||||
yield Header(show_clock=True, icon="⚙")
|
||||
title_text = Static("🖥️ Agent Selector", id="selector_title")
|
||||
title_text = Static("🖥️ Agent Selector", id="selector_title")
|
||||
title_text.styles.margin = (0, 0, 0, 1)
|
||||
yield title_text
|
||||
|
||||
@@ -77,7 +78,7 @@ class MultiAgentSelector(Widget):
|
||||
text_area.styles.overflow_y = "auto"
|
||||
yield text_area
|
||||
|
||||
with Horizontal(id="switch_search_container"):
|
||||
with Horizontal(id="switch_container"):
|
||||
switch = Switch(value=False, id="match_switch")
|
||||
switch.styles.width = "auto"
|
||||
switch.styles.margin = (1, 0, 0, 0)
|
||||
@@ -89,8 +90,13 @@ class MultiAgentSelector(Widget):
|
||||
switch_label.styles.margin = (2, 1, 0, 0)
|
||||
yield switch_label
|
||||
|
||||
with Horizontal(id="action_buttons_container"):
|
||||
load_file = Button("📂 Load File", id="load_file_button")
|
||||
load_file.styles.margin = (1, 1, 0, 1)
|
||||
yield load_file
|
||||
|
||||
search = Button("🔍 Search", id="search_button")
|
||||
search.styles.margin = (1, 0, 0, 0)
|
||||
search.styles.margin = (1, 0, 0, 1)
|
||||
yield search
|
||||
|
||||
with Horizontal() as select_buttons:
|
||||
@@ -152,6 +158,9 @@ class MultiAgentSelector(Widget):
|
||||
]
|
||||
self.post_message(self.AgentsSelected(selected_agents))
|
||||
event.stop()
|
||||
elif btn_id == "load_file_button":
|
||||
self._load_from_file()
|
||||
event.stop()
|
||||
elif btn_id == "search_button":
|
||||
self.update_matches()
|
||||
event.stop()
|
||||
@@ -166,7 +175,7 @@ class MultiAgentSelector(Widget):
|
||||
match_list.add_option((name, name))
|
||||
unmatched_label = self.query_one("#unmatched_label", Static)
|
||||
if unmatched:
|
||||
unmatched_label.update(f"⚠️ No matches for: {', '.join(unmatched)}")
|
||||
unmatched_label.update(f"âš ï¸ No matches for: {', '.join(unmatched)}")
|
||||
else:
|
||||
unmatched_label.update("")
|
||||
|
||||
@@ -214,3 +223,118 @@ class MultiAgentSelector(Widget):
|
||||
else:
|
||||
unmatched.append(name)
|
||||
return sorted(matched), unmatched
|
||||
|
||||
def _load_from_file(self):
|
||||
"""Safely load device names from a text file."""
|
||||
try:
|
||||
# Import here to avoid issues if tkinter isn't available
|
||||
import tkinter as tk
|
||||
from tkinter import filedialog
|
||||
|
||||
# Create file dialog
|
||||
root = tk.Tk()
|
||||
root.withdraw()
|
||||
|
||||
file_path = filedialog.askopenfilename(
|
||||
title="Select device list file",
|
||||
filetypes=[
|
||||
("Text files", "*.txt"),
|
||||
("CSV files", "*.csv"),
|
||||
("All files", "*.*"),
|
||||
],
|
||||
)
|
||||
|
||||
if not file_path:
|
||||
# User cancelled
|
||||
return
|
||||
|
||||
# Validate file path
|
||||
path_obj = Path(file_path)
|
||||
if not path_obj.exists():
|
||||
self.app.notify("File does not exist", severity="error", timeout=3)
|
||||
return
|
||||
|
||||
if not path_obj.is_file():
|
||||
self.app.notify(
|
||||
"Selected path is not a file", severity="error", timeout=3
|
||||
)
|
||||
return
|
||||
|
||||
# Check file size (limit to 1 MB for safety)
|
||||
file_size = path_obj.stat().st_size
|
||||
if file_size > 1_000_000: # 1 MB
|
||||
self.app.notify(
|
||||
f"File too large ({file_size:,} bytes). Maximum 1 MB.",
|
||||
severity="error",
|
||||
timeout=5,
|
||||
)
|
||||
return
|
||||
|
||||
# Read file with proper encoding to preserve emojis
|
||||
try:
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
except UnicodeDecodeError:
|
||||
# Try with different encoding if UTF-8 fails
|
||||
try:
|
||||
with open(file_path, "r", encoding="latin-1") as f:
|
||||
content = f.read()
|
||||
self.app.notify(
|
||||
"File loaded with Latin-1 encoding (UTF-8 failed)",
|
||||
severity="warning",
|
||||
timeout=3,
|
||||
)
|
||||
except Exception as e:
|
||||
self.app.notify(
|
||||
f"Error reading file: {str(e)}", severity="error", timeout=5
|
||||
)
|
||||
return
|
||||
|
||||
# Validate and sanitize content
|
||||
lines = content.split("\n")
|
||||
valid_lines = []
|
||||
invalid_count = 0
|
||||
|
||||
# Pattern for valid hostnames/device names
|
||||
# Allows: letters, numbers, hyphens, underscores, periods, and Unicode chars
|
||||
hostname_pattern = re.compile(r"^[\w\-\.\u0080-\uFFFF]+$", re.UNICODE)
|
||||
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue # Skip empty lines
|
||||
|
||||
# Check if line looks like a valid hostname/device name
|
||||
if hostname_pattern.match(line):
|
||||
valid_lines.append(line)
|
||||
else:
|
||||
invalid_count += 1
|
||||
# Log but don't add invalid entries
|
||||
|
||||
if not valid_lines:
|
||||
self.app.notify(
|
||||
"No valid device names found in file", severity="warning", timeout=3
|
||||
)
|
||||
return
|
||||
|
||||
# Update text area with validated content
|
||||
text_area = self.query_one("#device_input", TextArea)
|
||||
text_area.text = "\n".join(valid_lines)
|
||||
|
||||
# Show notification
|
||||
msg = f"✅ Loaded {len(valid_lines)} devices from file"
|
||||
if invalid_count > 0:
|
||||
msg += f" ({invalid_count} invalid entries skipped)"
|
||||
|
||||
self.app.notify(msg, severity="information", timeout=5)
|
||||
|
||||
except ImportError:
|
||||
self.app.notify(
|
||||
"tkinter not available - cannot open file dialog",
|
||||
severity="error",
|
||||
timeout=3,
|
||||
)
|
||||
except Exception as e:
|
||||
self.app.notify(
|
||||
f"Error loading file: {str(e)}", severity="error", timeout=5
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user