From ad9e329bffb338ccf1ccad70eb19cf19cf2d7497 Mon Sep 17 00:00:00 2001 From: phact Date: Tue, 16 Sep 2025 10:17:24 -0400 Subject: [PATCH] split tables --- src/tui/managers/docling_manager.py | 11 +- src/tui/screens/monitor.py | 209 ++++++++++++++++++++++------ 2 files changed, 176 insertions(+), 44 deletions(-) diff --git a/src/tui/managers/docling_manager.py b/src/tui/managers/docling_manager.py index 804f892b..5b1100cc 100644 --- a/src/tui/managers/docling_manager.py +++ b/src/tui/managers/docling_manager.py @@ -106,13 +106,15 @@ class DoclingManager: def get_status(self) -> Dict[str, Any]: """Get current status of docling serve.""" if self.is_running(): + pid = self._process.pid if self._process else None return { "status": "running", "port": self._port, "host": self._host, "endpoint": f"http://{self._host}:{self._port}", "docs_url": f"http://{self._host}:{self._port}/docs", - "ui_url": f"http://{self._host}:{self._port}/ui" + "ui_url": f"http://{self._host}:{self._port}/ui", + "pid": pid } else: return { @@ -121,10 +123,11 @@ class DoclingManager: "host": self._host, "endpoint": None, "docs_url": None, - "ui_url": None + "ui_url": None, + "pid": None } - async def start(self, port: int = 5001, host: str = "127.0.0.1", enable_ui: bool = True) -> Tuple[bool, str]: + async def start(self, port: int = 5001, host: str = "127.0.0.1", enable_ui: bool = False) -> Tuple[bool, str]: """Start docling serve as external process.""" if self.is_running(): return False, "Docling serve is already running" @@ -329,7 +332,7 @@ class DoclingManager: self._add_log_entry(f"Error stopping docling serve: {e}") return False, f"Error stopping docling serve: {str(e)}" - async def restart(self, port: Optional[int] = None, host: Optional[str] = None, enable_ui: bool = True) -> Tuple[bool, str]: + async def restart(self, port: Optional[int] = None, host: Optional[str] = None, enable_ui: bool = False) -> Tuple[bool, str]: """Restart docling serve.""" # Use current settings if not specified if port is None: diff --git a/src/tui/screens/monitor.py b/src/tui/screens/monitor.py index 3bc20be7..7c329f2e 100644 --- a/src/tui/screens/monitor.py +++ b/src/tui/screens/monitor.py @@ -42,12 +42,16 @@ class MonitorScreen(Screen): self.container_manager = ContainerManager() self.docling_manager = DoclingManager() self.services_table = None + self.docling_table = None self.images_table = None self.status_text = None self.refresh_timer = None self.operation_in_progress = False self._follow_task = None + # Track which table was last selected for mutual exclusion + self._last_selected_table = None + def on_unmount(self) -> None: """Clean up when the screen is unmounted.""" if hasattr(self, 'docling_manager'): @@ -72,13 +76,17 @@ class MonitorScreen(Screen): classes="button-row", id="mode-row", ) - # Images summary table (above services) + + # Container Images table yield Static("Container Images", classes="tab-header") self.images_table = DataTable(id="images-table", show_cursor=False) self.images_table.can_focus = False self.images_table.add_columns("Image", "Digest") yield self.images_table yield Static(" ") + + # Container Services table + yield Static("Container Services", classes="tab-header") # Dynamic controls container; populated based on running state yield Horizontal(id="services-controls", classes="button-row") # Create services table with image + digest info @@ -87,6 +95,16 @@ class MonitorScreen(Screen): "Service", "Status", "Health", "Ports", "Image", "Digest" ) yield self.services_table + yield Static(" ") + + # Docling Services table + yield Static("Native Services", classes="tab-header") + # Dynamic controls for docling service + yield Horizontal(id="docling-controls", classes="button-row") + # Create docling table with relevant columns only + self.docling_table = DataTable(id="docling-table") + self.docling_table.add_columns("Service", "Status", "Port", "PID", "Actions") + yield self.docling_table def _get_runtime_status(self) -> Text: """Get container runtime status text.""" @@ -172,10 +190,12 @@ class MonitorScreen(Screen): # Clear existing rows self.services_table.clear() + if self.docling_table: + self.docling_table.clear() if self.images_table: self.images_table.clear() - # Add service rows + # Add container service rows for service_name, service_info in services.items(): status_style = self._get_status_style(service_info.status) @@ -187,22 +207,23 @@ class MonitorScreen(Screen): service_info.image or "N/A", digest_map.get(service_info.image or "", "-"), ) - - # Add docling serve as a service + + # Add docling serve to its own table docling_status = self.docling_manager.get_status() docling_running = docling_status["status"] == "running" docling_status_text = "running" if docling_running else "stopped" docling_style = "bold green" if docling_running else "bold red" docling_port = f"{docling_status['host']}:{docling_status['port']}" if docling_running else "N/A" - - self.services_table.add_row( - "docling-serve", - Text(docling_status_text, style=docling_style), - "N/A", - docling_port, - "docling-serve (subprocess)", - "N/A", - ) + docling_pid = str(docling_status.get("pid")) if docling_status.get("pid") else "N/A" + + if self.docling_table: + self.docling_table.add_row( + "docling-serve", + Text(docling_status_text, style=docling_style), + docling_port, + docling_pid, + "Start/Stop/Logs" + ) # Populate images table (unique images as reported by runtime) if self.images_table: for image in sorted(images): @@ -250,6 +271,10 @@ class MonitorScreen(Screen): self.run_worker(self._start_docling_serve()) elif button_id.startswith("docling-stop-btn"): self.run_worker(self._stop_docling_serve()) + elif button_id.startswith("docling-restart-btn"): + self.run_worker(self._restart_docling_serve()) + elif button_id.startswith("docling-logs-btn"): + self._view_docling_logs() elif button_id == "toggle-mode-btn": self.action_toggle_mode() elif button_id.startswith("refresh-btn"): @@ -381,6 +406,27 @@ class MonitorScreen(Screen): finally: self.operation_in_progress = False + async def _restart_docling_serve(self) -> None: + """Restart docling serve.""" + self.operation_in_progress = True + try: + success, message = await self.docling_manager.restart() + if success: + self.notify(message, severity="information") + else: + self.notify(f"Failed to restart docling serve: {message}", severity="error") + # Refresh the services table to show updated status + await self._refresh_services() + except Exception as e: + self.notify(f"Error restarting docling serve: {str(e)}", severity="error") + finally: + self.operation_in_progress = False + + def _view_docling_logs(self) -> None: + """View docling serve logs.""" + from .logs import LogsScreen + self.app.push_screen(LogsScreen(initial_service="docling-serve")) + def _strip_ansi_codes(self, text: str) -> str: """Strip ANSI escape sequences from text.""" ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") @@ -545,20 +591,47 @@ class MonitorScreen(Screen): ) controls.mount(Button("Reset", variant="error", id=f"reset-btn{suffix}")) + except Exception as e: + notify_with_diagnostics( + self.app, f"Error updating controls: {e}", severity="error" + ) + + # Update docling controls separately + self._update_docling_controls() + + def _update_docling_controls(self) -> None: + """Update docling control buttons.""" + try: + # Get the docling controls container + docling_controls = self.query_one("#docling-controls", Horizontal) + + # Clear existing buttons + docling_controls.remove_children() + + # Use a random suffix for unique IDs + import random + suffix = f"-{random.randint(10000, 99999)}" + # Add docling serve controls docling_running = self.docling_manager.is_running() if docling_running: - controls.mount( - Button("Stop Docling", variant="error", id=f"docling-stop-btn{suffix}") + docling_controls.mount( + Button("Stop", variant="error", id=f"docling-stop-btn{suffix}") + ) + docling_controls.mount( + Button("Restart", variant="primary", id=f"docling-restart-btn{suffix}") + ) + docling_controls.mount( + Button("View Logs", variant="default", id=f"docling-logs-btn{suffix}") ) else: - controls.mount( - Button("Start Docling", variant="success", id=f"docling-start-btn{suffix}") + docling_controls.mount( + Button("Start", variant="success", id=f"docling-start-btn{suffix}") ) except Exception as e: notify_with_diagnostics( - self.app, f"Error updating controls: {e}", severity="error" + self.app, f"Error updating docling controls: {e}", severity="error" ) def action_back(self) -> None: @@ -584,37 +657,93 @@ class MonitorScreen(Screen): 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) + selected_service = self._get_selected_service() + if selected_service: + # Push the logs screen with the selected service + from .logs import LogsScreen + logs_screen = LogsScreen(initial_service=selected_service) + self.app.push_screen(logs_screen) + else: + self.notify("No service selected", severity="warning") + except Exception as e: + self.notify(f"Error opening logs: {e}", severity="error") - 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) + def _get_selected_service(self) -> str | None: + """Get the currently selected service from either table.""" + try: + # Check both tables regardless of last_selected_table to handle cursor navigation + services_table = self.query_one("#services-table", DataTable) + services_cursor = services_table.cursor_row + + docling_cursor = None + if self.docling_table: + docling_cursor = self.docling_table.cursor_row + + # If we have a last selected table preference, use it if that table has a valid selection + if self._last_selected_table == "docling" and self.docling_table: + if docling_cursor is not None and docling_cursor >= 0: + row_data = self.docling_table.get_row_at(docling_cursor) + if row_data: + return "docling-serve" + + elif self._last_selected_table == "services": + if services_cursor is not None and services_cursor >= 0: + row_data = services_table.get_row_at(services_cursor) + if row_data: + service_name = str(row_data[0]) + service_mapping = { + "openrag-backend": "openrag-backend", + "openrag-frontend": "openrag-frontend", + "opensearch": "opensearch", + "langflow": "langflow", + "dashboards": "dashboards", + } + selected_service = service_mapping.get(service_name, service_name) + return selected_service + + # Fallback: check both tables if no last_selected_table or it doesn't have a selection + if self.docling_table and docling_cursor is not None and docling_cursor >= 0: + row_data = self.docling_table.get_row_at(docling_cursor) if row_data: - service_name = str(row_data[0]) # First column is service name + return "docling-serve" - # Map display names to actual service names + if services_cursor is not None and services_cursor >= 0: + row_data = services_table.get_row_at(services_cursor) + if row_data: + service_name = str(row_data[0]) service_mapping = { "openrag-backend": "openrag-backend", "openrag-frontend": "openrag-frontend", "opensearch": "opensearch", "langflow": "langflow", "dashboards": "dashboards", - "docling-serve": "docling-serve", # Add docling-serve mapping } + selected_service = service_mapping.get(service_name, service_name) + return selected_service - 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") + return None except Exception as e: - self.notify(f"Error opening logs: {e}", severity="error") + self.notify(f"Error getting selected service: {e}", severity="error") + return None + + def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None: + """Handle row selection events to ensure mutual exclusivity.""" + selected_table = event.data_table + self.notify(f"DEBUG: Row selected in {selected_table.id}", severity="information") + + try: + # Track which table was selected + if selected_table.id == "services-table": + self._last_selected_table = "services" + self.notify(f"DEBUG: Set last_selected_table to services", severity="information") + # Clear docling table selection + if self.docling_table: + self.docling_table.cursor_row = -1 + elif selected_table.id == "docling-table": + self._last_selected_table = "docling" + self.notify(f"DEBUG: Set last_selected_table to docling", severity="information") + # Clear services table selection + services_table = self.query_one("#services-table", DataTable) + services_table.cursor_row = -1 + except Exception as e: + self.notify(f"DEBUG: Error in row selection handler: {e}", severity="error")