tui fixes
This commit is contained in:
parent
214877e9ff
commit
956d7582eb
8 changed files with 253 additions and 130 deletions
|
|
@ -115,6 +115,41 @@ class ContainerManager:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return False, "", f"Command execution failed: {e}"
|
return False, "", f"Command execution failed: {e}"
|
||||||
|
|
||||||
|
async def _run_compose_command_streaming(self, args: List[str], cpu_mode: Optional[bool] = None) -> AsyncIterator[str]:
|
||||||
|
"""Run a compose command and yield output lines in real-time."""
|
||||||
|
if not self.is_available():
|
||||||
|
yield "No container runtime available"
|
||||||
|
return
|
||||||
|
|
||||||
|
if cpu_mode is None:
|
||||||
|
cpu_mode = self.use_cpu_compose
|
||||||
|
compose_file = self.cpu_compose_file if cpu_mode else self.compose_file
|
||||||
|
cmd = self.runtime_info.compose_command + ["-f", str(compose_file)] + args
|
||||||
|
|
||||||
|
try:
|
||||||
|
process = await asyncio.create_subprocess_exec(
|
||||||
|
*cmd,
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
stderr=asyncio.subprocess.STDOUT, # Combine stderr with stdout for unified output
|
||||||
|
cwd=Path.cwd()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Simple approach: read line by line and yield each one
|
||||||
|
while True:
|
||||||
|
line = await process.stdout.readline()
|
||||||
|
if not line:
|
||||||
|
break
|
||||||
|
|
||||||
|
line_text = line.decode().rstrip()
|
||||||
|
if line_text:
|
||||||
|
yield line_text
|
||||||
|
|
||||||
|
# Wait for process to complete
|
||||||
|
await process.wait()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
yield f"Command execution failed: {e}"
|
||||||
|
|
||||||
async def _run_runtime_command(self, args: List[str]) -> tuple[bool, str, str]:
|
async def _run_runtime_command(self, args: List[str]) -> tuple[bool, str, str]:
|
||||||
"""Run a runtime command (docker/podman) and return (success, stdout, stderr)."""
|
"""Run a runtime command (docker/podman) and return (success, stdout, stderr)."""
|
||||||
if not self.is_available():
|
if not self.is_available():
|
||||||
|
|
@ -386,22 +421,31 @@ class ContainerManager:
|
||||||
"""Upgrade services (pull latest images and restart) and yield progress updates."""
|
"""Upgrade services (pull latest images and restart) and yield progress updates."""
|
||||||
yield False, "Pulling latest images..."
|
yield False, "Pulling latest images..."
|
||||||
|
|
||||||
# Pull latest images
|
# Pull latest images with streaming output
|
||||||
success, stdout, stderr = await self._run_compose_command(["pull"], cpu_mode)
|
pull_success = True
|
||||||
|
async for line in self._run_compose_command_streaming(["pull"], cpu_mode):
|
||||||
|
yield False, line
|
||||||
|
# Check for error patterns in the output
|
||||||
|
if "error" in line.lower() or "failed" in line.lower():
|
||||||
|
pull_success = False
|
||||||
|
|
||||||
if not success:
|
if not pull_success:
|
||||||
yield False, f"Failed to pull images: {stderr}"
|
yield False, "Failed to pull some images, but continuing with restart..."
|
||||||
return
|
|
||||||
|
|
||||||
yield False, "Images updated, restarting services..."
|
yield False, "Images updated, restarting services..."
|
||||||
|
|
||||||
# Restart with new images
|
# Restart with new images using streaming output
|
||||||
success, stdout, stderr = await self._run_compose_command(["up", "-d", "--force-recreate"], cpu_mode)
|
restart_success = True
|
||||||
|
async for line in self._run_compose_command_streaming(["up", "-d", "--force-recreate"], cpu_mode):
|
||||||
|
yield False, line
|
||||||
|
# Check for error patterns in the output
|
||||||
|
if "error" in line.lower() or "failed" in line.lower():
|
||||||
|
restart_success = False
|
||||||
|
|
||||||
if success:
|
if restart_success:
|
||||||
yield True, "Services upgraded and restarted successfully"
|
yield True, "Services upgraded and restarted successfully"
|
||||||
else:
|
else:
|
||||||
yield False, f"Failed to restart services after upgrade: {stderr}"
|
yield False, "Some errors occurred during service restart"
|
||||||
|
|
||||||
async def reset_services(self) -> AsyncIterator[tuple[bool, str]]:
|
async def reset_services(self) -> AsyncIterator[tuple[bool, str]]:
|
||||||
"""Reset all services (stop, remove containers/volumes, clear data) and yield progress updates."""
|
"""Reset all services (stop, remove containers/volumes, clear data) and yield progress updates."""
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import os
|
import os
|
||||||
import secrets
|
import secrets
|
||||||
import string
|
import string
|
||||||
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Optional, List
|
from typing import Dict, Optional, List
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
|
@ -181,9 +182,10 @@ class EnvManager:
|
||||||
try:
|
try:
|
||||||
# Ensure secure defaults (including Langflow secret key) are set before saving
|
# Ensure secure defaults (including Langflow secret key) are set before saving
|
||||||
self.setup_secure_defaults()
|
self.setup_secure_defaults()
|
||||||
# Create backup if file exists
|
# Create timestamped backup if file exists
|
||||||
if self.env_file.exists():
|
if self.env_file.exists():
|
||||||
backup_file = self.env_file.with_suffix('.env.backup')
|
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||||
|
backup_file = self.env_file.with_suffix(f'.env.backup.{timestamp}')
|
||||||
self.env_file.rename(backup_file)
|
self.env_file.rename(backup_file)
|
||||||
|
|
||||||
with open(self.env_file, 'w') as f:
|
with open(self.env_file, 'w') as f:
|
||||||
|
|
|
||||||
|
|
@ -399,6 +399,17 @@ class ConfigScreen(Screen):
|
||||||
# Add spacing
|
# Add spacing
|
||||||
yield Static(" ")
|
yield Static(" ")
|
||||||
|
|
||||||
|
def on_mount(self) -> None:
|
||||||
|
"""Initialize the screen when mounted."""
|
||||||
|
# Focus the first input field
|
||||||
|
try:
|
||||||
|
# Find the first input field and focus it
|
||||||
|
inputs = self.query(Input)
|
||||||
|
if inputs:
|
||||||
|
inputs[0].focus()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||||
"""Handle button presses."""
|
"""Handle button presses."""
|
||||||
if event.button.id == "generate-btn":
|
if event.button.id == "generate-btn":
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ from textual.app import ComposeResult
|
||||||
from textual.containers import Container, Vertical, Horizontal, ScrollableContainer
|
from textual.containers import Container, Vertical, Horizontal, ScrollableContainer
|
||||||
from textual.screen import Screen
|
from textual.screen import Screen
|
||||||
from textual.widgets import Header, Footer, Static, Button, Label, Log
|
from textual.widgets import Header, Footer, Static, Button, Label, Log
|
||||||
|
from rich.text import Text
|
||||||
|
|
||||||
from ..managers.container_manager import ContainerManager
|
from ..managers.container_manager import ContainerManager
|
||||||
|
|
||||||
|
|
@ -77,6 +78,12 @@ class DiagnosticsScreen(Screen):
|
||||||
"""Initialize the screen."""
|
"""Initialize the screen."""
|
||||||
self.run_diagnostics()
|
self.run_diagnostics()
|
||||||
|
|
||||||
|
# Focus the first button (refresh-btn)
|
||||||
|
try:
|
||||||
|
self.query_one("#refresh-btn").focus()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||||
"""Handle button presses."""
|
"""Handle button presses."""
|
||||||
if event.button.id == "refresh-btn":
|
if event.button.id == "refresh-btn":
|
||||||
|
|
@ -228,17 +235,31 @@ class DiagnosticsScreen(Screen):
|
||||||
"""Go back to previous screen."""
|
"""Go back to previous screen."""
|
||||||
self.app.pop_screen()
|
self.app.pop_screen()
|
||||||
|
|
||||||
|
def _get_system_info(self) -> Text:
|
||||||
|
"""Get system information text."""
|
||||||
|
info_text = Text()
|
||||||
|
|
||||||
|
runtime_info = self.container_manager.get_runtime_info()
|
||||||
|
|
||||||
|
info_text.append("Container Runtime Information\n", style="bold")
|
||||||
|
info_text.append("=" * 30 + "\n")
|
||||||
|
info_text.append(f"Type: {runtime_info.runtime_type.value}\n")
|
||||||
|
info_text.append(f"Compose Command: {' '.join(runtime_info.compose_command)}\n")
|
||||||
|
info_text.append(f"Runtime Command: {' '.join(runtime_info.runtime_command)}\n")
|
||||||
|
|
||||||
|
if runtime_info.version:
|
||||||
|
info_text.append(f"Version: {runtime_info.version}\n")
|
||||||
|
|
||||||
|
return info_text
|
||||||
|
|
||||||
def run_diagnostics(self) -> None:
|
def run_diagnostics(self) -> None:
|
||||||
"""Run all diagnostics."""
|
"""Run all diagnostics."""
|
||||||
log = self.query_one("#diagnostics-log", Log)
|
log = self.query_one("#diagnostics-log", Log)
|
||||||
log.clear()
|
log.clear()
|
||||||
|
|
||||||
# System information
|
# System information
|
||||||
log.write("[bold green]System Information[/bold green]")
|
system_info = self._get_system_info()
|
||||||
log.write(f"Runtime: {self.container_manager.runtime_info.runtime_type.value}")
|
log.write(str(system_info))
|
||||||
log.write(f"Version: {self.container_manager.runtime_info.version or 'Unknown'}")
|
|
||||||
log.write(f"Compose file: {self.container_manager.compose_file}")
|
|
||||||
log.write(f"CPU mode: {self.container_manager.use_cpu_compose}")
|
|
||||||
log.write("")
|
log.write("")
|
||||||
|
|
||||||
# Run async diagnostics
|
# Run async diagnostics
|
||||||
|
|
|
||||||
|
|
@ -19,41 +19,36 @@ class LogsScreen(Screen):
|
||||||
("f", "follow", "Follow Logs"),
|
("f", "follow", "Follow Logs"),
|
||||||
("c", "clear", "Clear"),
|
("c", "clear", "Clear"),
|
||||||
("r", "refresh", "Refresh"),
|
("r", "refresh", "Refresh"),
|
||||||
|
("a", "toggle_auto_scroll", "Toggle Auto Scroll"),
|
||||||
|
("g", "scroll_top", "Go to Top"),
|
||||||
|
("G", "scroll_bottom", "Go to Bottom"),
|
||||||
|
("j", "scroll_down", "Scroll Down"),
|
||||||
|
("k", "scroll_up", "Scroll Up"),
|
||||||
|
("ctrl+u", "scroll_page_up", "Page Up"),
|
||||||
|
("ctrl+f", "scroll_page_down", "Page Down"),
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, initial_service: str = "openrag-backend"):
|
def __init__(self, initial_service: str = "openrag-backend"):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.container_manager = ContainerManager()
|
self.container_manager = ContainerManager()
|
||||||
|
|
||||||
|
# Validate the initial service against available options
|
||||||
|
valid_services = ["openrag-backend", "openrag-frontend", "opensearch", "langflow", "dashboards"]
|
||||||
|
if initial_service not in valid_services:
|
||||||
|
initial_service = "openrag-backend" # fallback
|
||||||
|
|
||||||
self.current_service = initial_service
|
self.current_service = initial_service
|
||||||
self.logs_area = None
|
self.logs_area = None
|
||||||
self.following = False
|
self.following = False
|
||||||
self.follow_task = None
|
self.follow_task = None
|
||||||
|
self.auto_scroll = True
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
"""Create the logs screen layout."""
|
"""Create the logs screen layout."""
|
||||||
yield Header()
|
|
||||||
yield Container(
|
yield Container(
|
||||||
Vertical(
|
Vertical(
|
||||||
Static("Service Logs", id="logs-title"),
|
Static(f"Service Logs: {self.current_service}", id="logs-title"),
|
||||||
Horizontal(
|
|
||||||
Static("Service:", classes="label"),
|
|
||||||
Select([
|
|
||||||
("openrag-backend", "Backend"),
|
|
||||||
("openrag-frontend", "Frontend"),
|
|
||||||
("opensearch", "OpenSearch"),
|
|
||||||
("langflow", "Langflow"),
|
|
||||||
("dashboards", "Dashboards")
|
|
||||||
], value=self.current_service, id="service-select"),
|
|
||||||
Button("Refresh", variant="default", id="refresh-btn"),
|
|
||||||
Button("Follow", variant="primary", id="follow-btn"),
|
|
||||||
Button("Clear", variant="default", id="clear-btn"),
|
|
||||||
classes="controls-row"
|
|
||||||
),
|
|
||||||
self._create_logs_area(),
|
self._create_logs_area(),
|
||||||
Horizontal(
|
|
||||||
Button("Back", variant="default", id="back-btn"),
|
|
||||||
classes="button-row"
|
|
||||||
),
|
|
||||||
id="logs-content"
|
id="logs-content"
|
||||||
),
|
),
|
||||||
id="main-container"
|
id="main-container"
|
||||||
|
|
@ -72,29 +67,30 @@ class LogsScreen(Screen):
|
||||||
|
|
||||||
async def on_mount(self) -> None:
|
async def on_mount(self) -> None:
|
||||||
"""Initialize the screen when mounted."""
|
"""Initialize the screen when mounted."""
|
||||||
|
# Set the correct service in the select widget after a brief delay
|
||||||
|
try:
|
||||||
|
select = self.query_one("#service-select")
|
||||||
|
# Set a default first, then set the desired value
|
||||||
|
select.value = "openrag-backend"
|
||||||
|
if self.current_service in ["openrag-backend", "openrag-frontend", "opensearch", "langflow", "dashboards"]:
|
||||||
|
select.value = self.current_service
|
||||||
|
except Exception as e:
|
||||||
|
# If setting the service fails, just use the default
|
||||||
|
pass
|
||||||
|
|
||||||
await self._load_logs()
|
await self._load_logs()
|
||||||
|
|
||||||
|
# Focus the logs area since there are no buttons
|
||||||
|
try:
|
||||||
|
self.logs_area.focus()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def on_unmount(self) -> None:
|
def on_unmount(self) -> None:
|
||||||
"""Clean up when unmounting."""
|
"""Clean up when unmounting."""
|
||||||
self._stop_following()
|
self._stop_following()
|
||||||
|
|
||||||
def on_select_changed(self, event: Select.Changed) -> None:
|
|
||||||
"""Handle service selection change."""
|
|
||||||
if event.select.id == "service-select":
|
|
||||||
self.current_service = event.value
|
|
||||||
self._stop_following()
|
|
||||||
self.run_worker(self._load_logs())
|
|
||||||
|
|
||||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
||||||
"""Handle button presses."""
|
|
||||||
if event.button.id == "refresh-btn":
|
|
||||||
self.action_refresh()
|
|
||||||
elif event.button.id == "follow-btn":
|
|
||||||
self.action_follow()
|
|
||||||
elif event.button.id == "clear-btn":
|
|
||||||
self.action_clear()
|
|
||||||
elif event.button.id == "back-btn":
|
|
||||||
self.action_back()
|
|
||||||
|
|
||||||
async def _load_logs(self, lines: int = 200) -> None:
|
async def _load_logs(self, lines: int = 200) -> None:
|
||||||
"""Load recent logs for the current service."""
|
"""Load recent logs for the current service."""
|
||||||
|
|
@ -106,21 +102,19 @@ class LogsScreen(Screen):
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
self.logs_area.text = logs
|
self.logs_area.text = logs
|
||||||
# Scroll to bottom
|
# Scroll to bottom if auto scroll is enabled
|
||||||
self.logs_area.cursor_position = len(logs)
|
if self.auto_scroll:
|
||||||
|
self.logs_area.scroll_end()
|
||||||
else:
|
else:
|
||||||
self.logs_area.text = f"Failed to load logs: {logs}"
|
self.logs_area.text = f"Failed to load logs: {logs}"
|
||||||
|
|
||||||
def _stop_following(self) -> None:
|
def _stop_following(self) -> None:
|
||||||
"""Stop following logs."""
|
"""Stop following logs."""
|
||||||
self.following = False
|
self.following = False
|
||||||
if self.follow_task and not self.follow_task.done():
|
if self.follow_task and not self.follow_task.is_finished:
|
||||||
self.follow_task.cancel()
|
self.follow_task.cancel()
|
||||||
|
|
||||||
# Update button text
|
# No button to update since we removed it
|
||||||
follow_btn = self.query_one("#follow-btn")
|
|
||||||
follow_btn.label = "Follow"
|
|
||||||
follow_btn.variant = "primary"
|
|
||||||
|
|
||||||
async def _follow_logs(self) -> None:
|
async def _follow_logs(self) -> None:
|
||||||
"""Follow logs in real-time."""
|
"""Follow logs in real-time."""
|
||||||
|
|
@ -143,8 +137,9 @@ class LogsScreen(Screen):
|
||||||
new_text = '\n'.join(lines)
|
new_text = '\n'.join(lines)
|
||||||
|
|
||||||
self.logs_area.text = new_text
|
self.logs_area.text = new_text
|
||||||
# Scroll to bottom
|
# Scroll to bottom if auto scroll is enabled
|
||||||
self.logs_area.cursor_position = len(new_text)
|
if self.auto_scroll:
|
||||||
|
self.logs_area.scroll_end()
|
||||||
|
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
pass
|
pass
|
||||||
|
|
@ -165,9 +160,6 @@ class LogsScreen(Screen):
|
||||||
self._stop_following()
|
self._stop_following()
|
||||||
else:
|
else:
|
||||||
self.following = True
|
self.following = True
|
||||||
follow_btn = self.query_one("#follow-btn")
|
|
||||||
follow_btn.label = "Stop Following"
|
|
||||||
follow_btn.variant = "error"
|
|
||||||
|
|
||||||
# Start following
|
# Start following
|
||||||
self.follow_task = self.run_worker(self._follow_logs(), exclusive=False)
|
self.follow_task = self.run_worker(self._follow_logs(), exclusive=False)
|
||||||
|
|
@ -176,6 +168,51 @@ class LogsScreen(Screen):
|
||||||
"""Clear the logs area."""
|
"""Clear the logs area."""
|
||||||
self.logs_area.text = ""
|
self.logs_area.text = ""
|
||||||
|
|
||||||
|
def action_toggle_auto_scroll(self) -> None:
|
||||||
|
"""Toggle auto scroll on/off."""
|
||||||
|
self.auto_scroll = not self.auto_scroll
|
||||||
|
status = "enabled" if self.auto_scroll else "disabled"
|
||||||
|
self.notify(f"Auto scroll {status}", severity="information")
|
||||||
|
|
||||||
|
def action_scroll_top(self) -> None:
|
||||||
|
"""Scroll to the top of logs."""
|
||||||
|
self.logs_area.scroll_home()
|
||||||
|
|
||||||
|
def action_scroll_bottom(self) -> None:
|
||||||
|
"""Scroll to the bottom of logs."""
|
||||||
|
self.logs_area.scroll_end()
|
||||||
|
|
||||||
|
def action_scroll_down(self) -> None:
|
||||||
|
"""Scroll down one line."""
|
||||||
|
self.logs_area.scroll_down()
|
||||||
|
|
||||||
|
def action_scroll_up(self) -> None:
|
||||||
|
"""Scroll up one line."""
|
||||||
|
self.logs_area.scroll_up()
|
||||||
|
|
||||||
|
def action_scroll_page_up(self) -> None:
|
||||||
|
"""Scroll up one page."""
|
||||||
|
self.logs_area.scroll_page_up()
|
||||||
|
|
||||||
|
def action_scroll_page_down(self) -> None:
|
||||||
|
"""Scroll down one page."""
|
||||||
|
self.logs_area.scroll_page_down()
|
||||||
|
|
||||||
|
def on_key(self, event) -> None:
|
||||||
|
"""Handle key presses that might be intercepted by TextArea."""
|
||||||
|
key = event.key
|
||||||
|
|
||||||
|
# Handle keys that TextArea might intercept
|
||||||
|
if key == "ctrl+u":
|
||||||
|
self.action_scroll_page_up()
|
||||||
|
event.prevent_default()
|
||||||
|
elif key == "ctrl+f":
|
||||||
|
self.action_scroll_page_down()
|
||||||
|
event.prevent_default()
|
||||||
|
elif key.upper() == "G":
|
||||||
|
self.action_scroll_bottom()
|
||||||
|
event.prevent_default()
|
||||||
|
|
||||||
def action_back(self) -> None:
|
def action_back(self) -> None:
|
||||||
"""Go back to previous screen."""
|
"""Go back to previous screen."""
|
||||||
self._stop_following()
|
self._stop_following()
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ ButtonVariant = Literal["default", "primary", "success", "warning", "error"]
|
||||||
from textual.app import ComposeResult
|
from textual.app import ComposeResult
|
||||||
from textual.containers import Container, Vertical, Horizontal, ScrollableContainer
|
from textual.containers import Container, Vertical, Horizontal, ScrollableContainer
|
||||||
from textual.screen import Screen
|
from textual.screen import Screen
|
||||||
from textual.widgets import Header, Footer, Static, Button, DataTable, TabbedContent, TabPane
|
from textual.widgets import Header, Footer, Static, Button, DataTable
|
||||||
from textual.timer import Timer
|
from textual.timer import Timer
|
||||||
from rich.text import Text
|
from rich.text import Text
|
||||||
from rich.table import Table
|
from rich.table import Table
|
||||||
|
|
@ -31,6 +31,9 @@ class MonitorScreen(Screen):
|
||||||
("t", "stop", "Stop Services"),
|
("t", "stop", "Stop Services"),
|
||||||
("u", "upgrade", "Upgrade"),
|
("u", "upgrade", "Upgrade"),
|
||||||
("x", "reset", "Reset"),
|
("x", "reset", "Reset"),
|
||||||
|
("l", "logs", "View Logs"),
|
||||||
|
("j", "cursor_down", "Move Down"),
|
||||||
|
("k", "cursor_up", "Move Up"),
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
@ -47,15 +50,8 @@ class MonitorScreen(Screen):
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
"""Create the monitoring screen layout."""
|
"""Create the monitoring screen layout."""
|
||||||
yield Header()
|
# Just show the services content directly (no header, no tabs)
|
||||||
|
yield from self._create_services_tab()
|
||||||
with TabbedContent(id="monitor-tabs"):
|
|
||||||
with TabPane("Services", id="services-tab"):
|
|
||||||
yield from self._create_services_tab()
|
|
||||||
with TabPane("Logs", id="logs-tab"):
|
|
||||||
yield from self._create_logs_tab()
|
|
||||||
with TabPane("System", id="system-tab"):
|
|
||||||
yield from self._create_system_tab()
|
|
||||||
|
|
||||||
yield Footer()
|
yield Footer()
|
||||||
|
|
||||||
|
|
@ -70,7 +66,8 @@ class MonitorScreen(Screen):
|
||||||
)
|
)
|
||||||
# Images summary table (above services)
|
# Images summary table (above services)
|
||||||
yield Static("Container Images", classes="tab-header")
|
yield Static("Container Images", classes="tab-header")
|
||||||
self.images_table = DataTable(id="images-table")
|
self.images_table = DataTable(id="images-table", show_cursor=False)
|
||||||
|
self.images_table.can_focus = False
|
||||||
self.images_table.add_columns("Image", "Digest")
|
self.images_table.add_columns("Image", "Digest")
|
||||||
yield self.images_table
|
yield self.images_table
|
||||||
yield Static(" ")
|
yield Static(" ")
|
||||||
|
|
@ -80,32 +77,7 @@ class MonitorScreen(Screen):
|
||||||
self.services_table = DataTable(id="services-table")
|
self.services_table = DataTable(id="services-table")
|
||||||
self.services_table.add_columns("Service", "Status", "Health", "Ports", "Image", "Digest")
|
self.services_table.add_columns("Service", "Status", "Health", "Ports", "Image", "Digest")
|
||||||
yield self.services_table
|
yield self.services_table
|
||||||
yield Horizontal(
|
|
||||||
Button("Refresh", variant="default", id="refresh-services-btn"),
|
|
||||||
Button("Back", variant="default", id="back-services-btn"),
|
|
||||||
classes="button-row"
|
|
||||||
)
|
|
||||||
|
|
||||||
def _create_logs_tab(self) -> ComposeResult:
|
|
||||||
"""Create the logs viewing tab."""
|
|
||||||
logs_content = Static("Select a service to view logs", id="logs-content", markup=False)
|
|
||||||
|
|
||||||
yield Static("Service Logs", id="logs-header")
|
|
||||||
yield Horizontal(
|
|
||||||
Button("Backend", variant="default", id="logs-backend"),
|
|
||||||
Button("Frontend", variant="default", id="logs-frontend"),
|
|
||||||
Button("OpenSearch", variant="default", id="logs-opensearch"),
|
|
||||||
Button("Langflow", variant="default", id="logs-langflow"),
|
|
||||||
classes="button-row"
|
|
||||||
)
|
|
||||||
yield ScrollableContainer(logs_content, id="logs-scroll")
|
|
||||||
|
|
||||||
def _create_system_tab(self) -> ComposeResult:
|
|
||||||
"""Create the system information tab."""
|
|
||||||
system_info = Static(self._get_system_info(), id="system-info")
|
|
||||||
|
|
||||||
yield Static("System Information", id="system-header")
|
|
||||||
yield system_info
|
|
||||||
|
|
||||||
def _get_runtime_status(self) -> Text:
|
def _get_runtime_status(self) -> Text:
|
||||||
"""Get container runtime status text."""
|
"""Get container runtime status text."""
|
||||||
|
|
@ -136,23 +108,6 @@ class MonitorScreen(Screen):
|
||||||
|
|
||||||
return status_text
|
return status_text
|
||||||
|
|
||||||
def _get_system_info(self) -> Text:
|
|
||||||
"""Get system information text."""
|
|
||||||
info_text = Text()
|
|
||||||
|
|
||||||
runtime_info = self.container_manager.get_runtime_info()
|
|
||||||
|
|
||||||
info_text.append("Container Runtime Information\n", style="bold")
|
|
||||||
info_text.append("=" * 30 + "\n")
|
|
||||||
info_text.append(f"Type: {runtime_info.runtime_type.value}\n")
|
|
||||||
info_text.append(f"Compose Command: {' '.join(runtime_info.compose_command)}\n")
|
|
||||||
info_text.append(f"Runtime Command: {' '.join(runtime_info.runtime_command)}\n")
|
|
||||||
|
|
||||||
if runtime_info.version:
|
|
||||||
info_text.append(f"Version: {runtime_info.version}\n")
|
|
||||||
# Removed compose files section for cleaner display
|
|
||||||
|
|
||||||
return info_text
|
|
||||||
|
|
||||||
async def on_mount(self) -> None:
|
async def on_mount(self) -> None:
|
||||||
"""Initialize the screen when mounted."""
|
"""Initialize the screen when mounted."""
|
||||||
|
|
@ -160,6 +115,12 @@ class MonitorScreen(Screen):
|
||||||
# Set up auto-refresh every 5 seconds
|
# Set up auto-refresh every 5 seconds
|
||||||
self.refresh_timer = self.set_interval(5.0, self._auto_refresh)
|
self.refresh_timer = self.set_interval(5.0, self._auto_refresh)
|
||||||
|
|
||||||
|
# Focus the services table
|
||||||
|
try:
|
||||||
|
self.services_table.focus()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def on_unmount(self) -> None:
|
def on_unmount(self) -> None:
|
||||||
"""Clean up when unmounting."""
|
"""Clean up when unmounting."""
|
||||||
if self.refresh_timer:
|
if self.refresh_timer:
|
||||||
|
|
@ -257,9 +218,9 @@ class MonitorScreen(Screen):
|
||||||
self.run_worker(self._reset_services())
|
self.run_worker(self._reset_services())
|
||||||
elif button_id == "toggle-mode-btn":
|
elif button_id == "toggle-mode-btn":
|
||||||
self.action_toggle_mode()
|
self.action_toggle_mode()
|
||||||
elif button_id == "refresh-services-btn" or button_id == "refresh-btn":
|
elif button_id.startswith("refresh-btn"):
|
||||||
self.action_refresh()
|
self.action_refresh()
|
||||||
elif button_id == "back-services-btn" or button_id == "back-btn":
|
elif button_id.startswith("back-btn"):
|
||||||
self.action_back()
|
self.action_back()
|
||||||
elif button_id.startswith("logs-"):
|
elif button_id.startswith("logs-"):
|
||||||
# Map button IDs to actual service names
|
# Map button IDs to actual service names
|
||||||
|
|
@ -439,6 +400,20 @@ class MonitorScreen(Screen):
|
||||||
"""Refresh services manually."""
|
"""Refresh services manually."""
|
||||||
self.run_worker(self._refresh_services())
|
self.run_worker(self._refresh_services())
|
||||||
|
|
||||||
|
def action_cursor_down(self) -> None:
|
||||||
|
"""Move cursor down in services table."""
|
||||||
|
try:
|
||||||
|
self.services_table.action_cursor_down()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def action_cursor_up(self) -> None:
|
||||||
|
"""Move cursor up in services table."""
|
||||||
|
try:
|
||||||
|
self.services_table.action_cursor_up()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def _update_mode_row(self) -> None:
|
def _update_mode_row(self) -> None:
|
||||||
"""Update the mode indicator and toggle button label."""
|
"""Update the mode indicator and toggle button label."""
|
||||||
try:
|
try:
|
||||||
|
|
@ -522,3 +497,37 @@ class MonitorScreen(Screen):
|
||||||
def action_reset(self) -> None:
|
def action_reset(self) -> None:
|
||||||
"""Reset services."""
|
"""Reset services."""
|
||||||
self.run_worker(self._reset_services())
|
self.run_worker(self._reset_services())
|
||||||
|
|
||||||
|
def action_logs(self) -> None:
|
||||||
|
"""View logs for the selected service."""
|
||||||
|
try:
|
||||||
|
# Get the currently focused row in the services table
|
||||||
|
table = self.query_one("#services-table", DataTable)
|
||||||
|
|
||||||
|
if table.cursor_row is not None and table.cursor_row >= 0:
|
||||||
|
# Get the service name from the first column of the selected row
|
||||||
|
row_data = table.get_row_at(table.cursor_row)
|
||||||
|
if row_data:
|
||||||
|
service_name = str(row_data[0]) # First column is service name
|
||||||
|
|
||||||
|
# Map display names to actual service names
|
||||||
|
service_mapping = {
|
||||||
|
"openrag-backend": "openrag-backend",
|
||||||
|
"openrag-frontend": "openrag-frontend",
|
||||||
|
"opensearch": "opensearch",
|
||||||
|
"langflow": "langflow",
|
||||||
|
"dashboards": "dashboards"
|
||||||
|
}
|
||||||
|
|
||||||
|
actual_service_name = service_mapping.get(service_name, service_name)
|
||||||
|
|
||||||
|
# Push the logs screen with the selected service
|
||||||
|
from .logs import LogsScreen
|
||||||
|
logs_screen = LogsScreen(initial_service=actual_service_name)
|
||||||
|
self.app.push_screen(logs_screen)
|
||||||
|
else:
|
||||||
|
self.notify("No service selected", severity="warning")
|
||||||
|
else:
|
||||||
|
self.notify("No service selected", severity="warning")
|
||||||
|
except Exception as e:
|
||||||
|
self.notify(f"Error opening logs: {e}", severity="error")
|
||||||
|
|
|
||||||
|
|
@ -97,9 +97,6 @@ class WelcomeScreen(Screen):
|
||||||
# Always show monitor option
|
# Always show monitor option
|
||||||
buttons.append(Button("Monitor Services", variant="default", id="monitor-btn"))
|
buttons.append(Button("Monitor Services", variant="default", id="monitor-btn"))
|
||||||
|
|
||||||
# Always show diagnostics button
|
|
||||||
buttons.append(Button("Diagnostics", variant="default", id="diagnostics-btn"))
|
|
||||||
|
|
||||||
return Horizontal(*buttons, classes="button-row")
|
return Horizontal(*buttons, classes="button-row")
|
||||||
|
|
||||||
async def on_mount(self) -> None:
|
async def on_mount(self) -> None:
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,8 @@ from textual.app import ComposeResult
|
||||||
from textual.worker import Worker
|
from textual.worker import Worker
|
||||||
from textual.containers import Container, ScrollableContainer
|
from textual.containers import Container, ScrollableContainer
|
||||||
from textual.screen import ModalScreen
|
from textual.screen import ModalScreen
|
||||||
from textual.widgets import Button, Static, Label, Log
|
from textual.widgets import Button, Static, Label, RichLog
|
||||||
|
from rich.console import Console
|
||||||
|
|
||||||
|
|
||||||
class CommandOutputModal(ModalScreen):
|
class CommandOutputModal(ModalScreen):
|
||||||
|
|
@ -86,7 +87,7 @@ class CommandOutputModal(ModalScreen):
|
||||||
with Container(id="dialog"):
|
with Container(id="dialog"):
|
||||||
yield Label(self.title_text, id="title")
|
yield Label(self.title_text, id="title")
|
||||||
with ScrollableContainer(id="output-container"):
|
with ScrollableContainer(id="output-container"):
|
||||||
yield Log(id="command-output", highlight=True)
|
yield RichLog(id="command-output", highlight=True, markup=True)
|
||||||
with Container(id="button-row"):
|
with Container(id="button-row"):
|
||||||
yield Button("Close", variant="primary", id="close-btn")
|
yield Button("Close", variant="primary", id="close-btn")
|
||||||
|
|
||||||
|
|
@ -102,12 +103,12 @@ class CommandOutputModal(ModalScreen):
|
||||||
|
|
||||||
async def _run_command(self) -> None:
|
async def _run_command(self) -> None:
|
||||||
"""Run the command and update the output in real-time."""
|
"""Run the command and update the output in real-time."""
|
||||||
output = self.query_one("#command-output", Log)
|
output = self.query_one("#command-output", RichLog)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async for is_complete, message in self.command_generator:
|
async for is_complete, message in self.command_generator:
|
||||||
# Add message to output
|
# Simple approach: just append each line as it comes
|
||||||
output.write(message)
|
output.write(message + "\n")
|
||||||
|
|
||||||
# Scroll to bottom
|
# Scroll to bottom
|
||||||
container = self.query_one("#output-container", ScrollableContainer)
|
container = self.query_one("#output-container", ScrollableContainer)
|
||||||
|
|
@ -115,16 +116,17 @@ class CommandOutputModal(ModalScreen):
|
||||||
|
|
||||||
# If command is complete, update UI
|
# If command is complete, update UI
|
||||||
if is_complete:
|
if is_complete:
|
||||||
output.write("[bold green]Command completed successfully[/bold green]")
|
output.write("[bold green]Command completed successfully[/bold green]\n")
|
||||||
# Call the completion callback if provided
|
# Call the completion callback if provided
|
||||||
if self.on_complete:
|
if self.on_complete:
|
||||||
await asyncio.sleep(0.5) # Small delay for better UX
|
await asyncio.sleep(0.5) # Small delay for better UX
|
||||||
self.on_complete()
|
self.on_complete()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
output.write(f"[bold red]Error: {e}[/bold red]")
|
output.write(f"[bold red]Error: {e}[/bold red]\n")
|
||||||
|
|
||||||
# Enable the close button
|
# Enable the close button and focus it
|
||||||
close_btn = self.query_one("#close-btn", Button)
|
close_btn = self.query_one("#close-btn", Button)
|
||||||
close_btn.disabled = False
|
close_btn.disabled = False
|
||||||
|
close_btn.focus()
|
||||||
|
|
||||||
# Made with Bob
|
# Made with Bob
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue