Merge branch 'main' into issue-552-known-01-dec-25
This commit is contained in:
commit
28f09e3264
10 changed files with 730 additions and 47 deletions
|
|
@ -101,7 +101,7 @@ services:
|
||||||
langflow:
|
langflow:
|
||||||
volumes:
|
volumes:
|
||||||
- ./flows:/app/flows:U,z
|
- ./flows:/app/flows:U,z
|
||||||
image: langflowai/openrag-langflow:${LANGFLOW_VERSION:-latest}
|
image: langflowai/openrag-langflow:${OPENRAG_VERSION:-latest}
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile.langflow
|
dockerfile: Dockerfile.langflow
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
|
import { useEffect } from "react";
|
||||||
import { useGetSettingsQuery } from "@/app/api/queries/useGetSettingsQuery";
|
import { useGetSettingsQuery } from "@/app/api/queries/useGetSettingsQuery";
|
||||||
import {
|
import {
|
||||||
DoclingHealthBanner,
|
DoclingHealthBanner,
|
||||||
|
|
@ -22,6 +23,7 @@ import { ChatRenderer } from "./chat-renderer";
|
||||||
|
|
||||||
export function LayoutWrapper({ children }: { children: React.ReactNode }) {
|
export function LayoutWrapper({ children }: { children: React.ReactNode }) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
const router = useRouter();
|
||||||
const { isMenuOpen } = useTask();
|
const { isMenuOpen } = useTask();
|
||||||
const { isPanelOpen } = useKnowledgeFilter();
|
const { isPanelOpen } = useKnowledgeFilter();
|
||||||
const { isLoading, isAuthenticated, isNoAuthMode } = useAuth();
|
const { isLoading, isAuthenticated, isNoAuthMode } = useAuth();
|
||||||
|
|
@ -30,6 +32,14 @@ export function LayoutWrapper({ children }: { children: React.ReactNode }) {
|
||||||
const authPaths = ["/login", "/auth/callback"];
|
const authPaths = ["/login", "/auth/callback"];
|
||||||
const isAuthPage = authPaths.includes(pathname);
|
const isAuthPage = authPaths.includes(pathname);
|
||||||
|
|
||||||
|
// Redirect to login when not authenticated (and not in no-auth mode)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isLoading && !isAuthenticated && !isNoAuthMode && !isAuthPage) {
|
||||||
|
const redirectUrl = `/login?redirect=${encodeURIComponent(pathname)}`;
|
||||||
|
router.push(redirectUrl);
|
||||||
|
}
|
||||||
|
}, [isLoading, isAuthenticated, isNoAuthMode, isAuthPage, pathname, router]);
|
||||||
|
|
||||||
// Call all hooks unconditionally (React rules)
|
// Call all hooks unconditionally (React rules)
|
||||||
// But disable queries for auth pages to prevent unnecessary requests
|
// But disable queries for auth pages to prevent unnecessary requests
|
||||||
const { data: settings, isLoading: isSettingsLoading } = useGetSettingsQuery({
|
const { data: settings, isLoading: isSettingsLoading } = useGetSettingsQuery({
|
||||||
|
|
@ -49,9 +59,10 @@ export function LayoutWrapper({ children }: { children: React.ReactNode }) {
|
||||||
|
|
||||||
const isSettingsLoadingOrError = isSettingsLoading || !settings;
|
const isSettingsLoadingOrError = isSettingsLoading || !settings;
|
||||||
|
|
||||||
// Show loading state when backend isn't ready
|
// Show loading state when backend isn't ready or when not authenticated (redirect will happen)
|
||||||
if (
|
if (
|
||||||
isLoading ||
|
isLoading ||
|
||||||
|
(!isAuthenticated && !isNoAuthMode) ||
|
||||||
(isSettingsLoadingOrError && (isNoAuthMode || isAuthenticated))
|
(isSettingsLoadingOrError && (isNoAuthMode || isAuthenticated))
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
"""OpenRAG Terminal User Interface package."""
|
"""OpenRAG Terminal User Interface package."""
|
||||||
|
|
||||||
from importlib.metadata import version
|
from .utils.version_check import get_current_version
|
||||||
|
|
||||||
try:
|
__version__ = get_current_version()
|
||||||
__version__ = version("openrag")
|
|
||||||
except Exception:
|
|
||||||
__version__ = "unknown"
|
|
||||||
|
|
|
||||||
|
|
@ -479,6 +479,100 @@ class ContainerManager:
|
||||||
image=image,
|
image=image,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def get_container_version(self) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Get the version tag from existing containers.
|
||||||
|
Checks the backend container image tag to determine version.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Version string if found, None if no containers exist or version can't be determined
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Check for backend container first (most reliable)
|
||||||
|
success, stdout, _ = await self._run_runtime_command(
|
||||||
|
["ps", "--all", "--filter", "name=openrag-backend", "--format", "{{.Image}}"]
|
||||||
|
)
|
||||||
|
|
||||||
|
if success and stdout.strip():
|
||||||
|
image_tag = stdout.strip().splitlines()[0].strip()
|
||||||
|
if not image_tag or image_tag == "N/A":
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Extract version from image tag (e.g., langflowai/openrag-backend:0.1.47)
|
||||||
|
if ":" in image_tag:
|
||||||
|
version = image_tag.split(":")[-1]
|
||||||
|
# If version is "latest", check .env file for OPENRAG_VERSION
|
||||||
|
if version == "latest":
|
||||||
|
# Try to get version from .env file
|
||||||
|
try:
|
||||||
|
from pathlib import Path
|
||||||
|
env_file = Path(".env")
|
||||||
|
if env_file.exists():
|
||||||
|
env_content = env_file.read_text()
|
||||||
|
for line in env_content.splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if line.startswith("OPENRAG_VERSION"):
|
||||||
|
env_version = line.split("=", 1)[1].strip()
|
||||||
|
# Remove quotes if present
|
||||||
|
env_version = env_version.strip("'\"")
|
||||||
|
if env_version and env_version != "latest":
|
||||||
|
return env_version
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
# If still "latest", we can't determine version - return None
|
||||||
|
return None
|
||||||
|
# Return version if it looks like a version number (not "latest")
|
||||||
|
if version and version != "latest":
|
||||||
|
return version
|
||||||
|
|
||||||
|
# Fallback: check all containers for version tags
|
||||||
|
success, stdout, _ = await self._run_runtime_command(
|
||||||
|
["ps", "--all", "--format", "{{.Image}}"]
|
||||||
|
)
|
||||||
|
|
||||||
|
if success and stdout.strip():
|
||||||
|
images = stdout.strip().splitlines()
|
||||||
|
for image in images:
|
||||||
|
image = image.strip()
|
||||||
|
if "openrag" in image.lower() and ":" in image:
|
||||||
|
version = image.split(":")[-1]
|
||||||
|
if version and version != "latest":
|
||||||
|
return version
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Error getting container version: {e}")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def check_version_mismatch(self) -> tuple[bool, Optional[str], str]:
|
||||||
|
"""
|
||||||
|
Check if existing containers have a different version than the current TUI.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (has_mismatch, container_version, tui_version)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from ..utils.version_check import get_current_version
|
||||||
|
|
||||||
|
tui_version = get_current_version()
|
||||||
|
if tui_version == "unknown":
|
||||||
|
return False, None, tui_version
|
||||||
|
|
||||||
|
container_version = await self.get_container_version()
|
||||||
|
|
||||||
|
if container_version is None:
|
||||||
|
# No containers exist, no mismatch
|
||||||
|
return False, None, tui_version
|
||||||
|
|
||||||
|
# Compare versions
|
||||||
|
from ..utils.version_check import compare_versions
|
||||||
|
comparison = compare_versions(container_version, tui_version)
|
||||||
|
has_mismatch = comparison != 0
|
||||||
|
|
||||||
|
return has_mismatch, container_version, tui_version
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Error checking version mismatch: {e}")
|
||||||
|
return False, None, "unknown"
|
||||||
|
|
||||||
async def get_service_status(
|
async def get_service_status(
|
||||||
self, force_refresh: bool = False
|
self, force_refresh: bool = False
|
||||||
) -> Dict[str, ServiceInfo]:
|
) -> Dict[str, ServiceInfo]:
|
||||||
|
|
@ -764,6 +858,14 @@ class ContainerManager:
|
||||||
yield False, "No container runtime available"
|
yield False, "No container runtime available"
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Ensure OPENRAG_VERSION is set in .env file
|
||||||
|
try:
|
||||||
|
from ..managers.env_manager import EnvManager
|
||||||
|
env_manager = EnvManager()
|
||||||
|
env_manager.ensure_openrag_version()
|
||||||
|
except Exception:
|
||||||
|
pass # Continue even if version setting fails
|
||||||
|
|
||||||
# Determine GPU mode
|
# Determine GPU mode
|
||||||
if cpu_mode is None:
|
if cpu_mode is None:
|
||||||
use_gpu = self.use_gpu_compose
|
use_gpu = self.use_gpu_compose
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,9 @@ class EnvConfig:
|
||||||
|
|
||||||
# OpenSearch data path
|
# OpenSearch data path
|
||||||
opensearch_data_path: str = "./opensearch-data"
|
opensearch_data_path: str = "./opensearch-data"
|
||||||
|
|
||||||
|
# Container version (linked to TUI version)
|
||||||
|
openrag_version: str = ""
|
||||||
|
|
||||||
# Validation errors
|
# Validation errors
|
||||||
validation_errors: Dict[str, str] = field(default_factory=dict)
|
validation_errors: Dict[str, str] = field(default_factory=dict)
|
||||||
|
|
@ -149,6 +152,7 @@ class EnvManager:
|
||||||
"LANGFLOW_NEW_USER_IS_ACTIVE": "langflow_new_user_is_active",
|
"LANGFLOW_NEW_USER_IS_ACTIVE": "langflow_new_user_is_active",
|
||||||
"LANGFLOW_ENABLE_SUPERUSER_CLI": "langflow_enable_superuser_cli",
|
"LANGFLOW_ENABLE_SUPERUSER_CLI": "langflow_enable_superuser_cli",
|
||||||
"DISABLE_INGEST_WITH_LANGFLOW": "disable_ingest_with_langflow",
|
"DISABLE_INGEST_WITH_LANGFLOW": "disable_ingest_with_langflow",
|
||||||
|
"OPENRAG_VERSION": "openrag_version",
|
||||||
}
|
}
|
||||||
|
|
||||||
loaded_from_file = False
|
loaded_from_file = False
|
||||||
|
|
@ -193,6 +197,17 @@ class EnvManager:
|
||||||
|
|
||||||
if not self.config.langflow_secret_key:
|
if not self.config.langflow_secret_key:
|
||||||
self.config.langflow_secret_key = self.generate_langflow_secret_key()
|
self.config.langflow_secret_key = self.generate_langflow_secret_key()
|
||||||
|
|
||||||
|
# Set OPENRAG_VERSION to TUI version if not already set
|
||||||
|
if not self.config.openrag_version:
|
||||||
|
try:
|
||||||
|
from ..utils.version_check import get_current_version
|
||||||
|
current_version = get_current_version()
|
||||||
|
if current_version != "unknown":
|
||||||
|
self.config.openrag_version = current_version
|
||||||
|
except Exception:
|
||||||
|
# If we can't get version, leave it empty (will use 'latest' from compose)
|
||||||
|
pass
|
||||||
|
|
||||||
# Configure autologin based on whether password is set
|
# Configure autologin based on whether password is set
|
||||||
if not self.config.langflow_superuser_password:
|
if not self.config.langflow_superuser_password:
|
||||||
|
|
@ -344,6 +359,18 @@ class EnvManager:
|
||||||
f.write(
|
f.write(
|
||||||
f"OPENSEARCH_DATA_PATH={self._quote_env_value(self.config.opensearch_data_path)}\n"
|
f"OPENSEARCH_DATA_PATH={self._quote_env_value(self.config.opensearch_data_path)}\n"
|
||||||
)
|
)
|
||||||
|
# Set OPENRAG_VERSION to TUI version
|
||||||
|
if self.config.openrag_version:
|
||||||
|
f.write(f"OPENRAG_VERSION={self._quote_env_value(self.config.openrag_version)}\n")
|
||||||
|
else:
|
||||||
|
# Fallback: try to get current version
|
||||||
|
try:
|
||||||
|
from ..utils.version_check import get_current_version
|
||||||
|
current_version = get_current_version()
|
||||||
|
if current_version != "unknown":
|
||||||
|
f.write(f"OPENRAG_VERSION={self._quote_env_value(current_version)}\n")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
f.write("\n")
|
f.write("\n")
|
||||||
|
|
||||||
# Provider API keys and endpoints (optional - can be set during onboarding)
|
# Provider API keys and endpoints (optional - can be set during onboarding)
|
||||||
|
|
@ -514,6 +541,73 @@ class EnvManager:
|
||||||
|
|
||||||
return base_fields + oauth_fields + flow_fields + optional_fields
|
return base_fields + oauth_fields + flow_fields + optional_fields
|
||||||
|
|
||||||
|
def ensure_openrag_version(self) -> None:
|
||||||
|
"""Ensure OPENRAG_VERSION is set in .env file to match TUI version."""
|
||||||
|
try:
|
||||||
|
from ..utils.version_check import get_current_version
|
||||||
|
current_version = get_current_version()
|
||||||
|
if current_version == "unknown":
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if OPENRAG_VERSION is already set in .env
|
||||||
|
if self.env_file.exists():
|
||||||
|
env_content = self.env_file.read_text()
|
||||||
|
if "OPENRAG_VERSION" in env_content:
|
||||||
|
# Already set, check if it needs updating
|
||||||
|
for line in env_content.splitlines():
|
||||||
|
if line.strip().startswith("OPENRAG_VERSION"):
|
||||||
|
existing_value = line.split("=", 1)[1].strip()
|
||||||
|
existing_value = sanitize_env_value(existing_value)
|
||||||
|
if existing_value == current_version:
|
||||||
|
# Already correct, no update needed
|
||||||
|
return
|
||||||
|
break
|
||||||
|
|
||||||
|
# Set or update OPENRAG_VERSION
|
||||||
|
self.config.openrag_version = current_version
|
||||||
|
|
||||||
|
# Update .env file
|
||||||
|
if self.env_file.exists():
|
||||||
|
# Read existing content
|
||||||
|
lines = self.env_file.read_text().splitlines()
|
||||||
|
updated = False
|
||||||
|
new_lines = []
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
if line.strip().startswith("OPENRAG_VERSION"):
|
||||||
|
# Replace existing line
|
||||||
|
new_lines.append(f"OPENRAG_VERSION={self._quote_env_value(current_version)}")
|
||||||
|
updated = True
|
||||||
|
else:
|
||||||
|
new_lines.append(line)
|
||||||
|
|
||||||
|
# If not found, add it after OPENSEARCH_DATA_PATH or at the end
|
||||||
|
if not updated:
|
||||||
|
insert_pos = len(new_lines)
|
||||||
|
for i, line in enumerate(new_lines):
|
||||||
|
if "OPENSEARCH_DATA_PATH" in line:
|
||||||
|
insert_pos = i + 1
|
||||||
|
break
|
||||||
|
new_lines.insert(insert_pos, f"OPENRAG_VERSION={self._quote_env_value(current_version)}")
|
||||||
|
|
||||||
|
with open(self.env_file, 'w') as f:
|
||||||
|
f.write("\n".join(new_lines) + "\n")
|
||||||
|
f.flush()
|
||||||
|
os.fsync(f.fileno())
|
||||||
|
else:
|
||||||
|
# Create new .env file with just OPENRAG_VERSION
|
||||||
|
with open(self.env_file, 'w') as f:
|
||||||
|
content = (
|
||||||
|
f"# OpenRAG Environment Configuration\n"
|
||||||
|
f"# Generated by OpenRAG TUI\n\n"
|
||||||
|
f"OPENRAG_VERSION={self._quote_env_value(current_version)}\n"
|
||||||
|
)
|
||||||
|
f.write(content)
|
||||||
|
f.flush()
|
||||||
|
os.fsync(f.fileno())
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Error ensuring OPENRAG_VERSION: {e}")
|
||||||
|
|
||||||
def generate_compose_volume_mounts(self) -> List[str]:
|
def generate_compose_volume_mounts(self) -> List[str]:
|
||||||
"""Generate Docker Compose volume mount strings from documents paths."""
|
"""Generate Docker Compose volume mount strings from documents paths."""
|
||||||
is_valid, _, validated_paths = validate_documents_paths(
|
is_valid, _, validated_paths = validate_documents_paths(
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ from ..managers.docling_manager import DoclingManager
|
||||||
from ..utils.platform import RuntimeType
|
from ..utils.platform import RuntimeType
|
||||||
from ..widgets.command_modal import CommandOutputModal
|
from ..widgets.command_modal import CommandOutputModal
|
||||||
from ..widgets.flow_backup_warning_modal import FlowBackupWarningModal
|
from ..widgets.flow_backup_warning_modal import FlowBackupWarningModal
|
||||||
|
from ..widgets.version_mismatch_warning_modal import VersionMismatchWarningModal
|
||||||
|
from ..widgets.upgrade_instructions_modal import UpgradeInstructionsModal
|
||||||
from ..widgets.diagnostics_notification import notify_with_diagnostics
|
from ..widgets.diagnostics_notification import notify_with_diagnostics
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -63,7 +65,7 @@ class MonitorScreen(Screen):
|
||||||
|
|
||||||
def on_unmount(self) -> None:
|
def on_unmount(self) -> None:
|
||||||
"""Clean up when the screen is unmounted."""
|
"""Clean up when the screen is unmounted."""
|
||||||
if hasattr(self, 'docling_manager'):
|
if hasattr(self, "docling_manager"):
|
||||||
self.docling_manager.cleanup()
|
self.docling_manager.cleanup()
|
||||||
super().on_unmount()
|
super().on_unmount()
|
||||||
self._follow_service = None
|
self._follow_service = None
|
||||||
|
|
@ -218,7 +220,7 @@ class MonitorScreen(Screen):
|
||||||
docling_status_value = docling_status["status"]
|
docling_status_value = docling_status["status"]
|
||||||
docling_running = docling_status_value == "running"
|
docling_running = docling_status_value == "running"
|
||||||
docling_starting = docling_status_value == "starting"
|
docling_starting = docling_status_value == "starting"
|
||||||
|
|
||||||
if docling_running:
|
if docling_running:
|
||||||
docling_status_text = "running"
|
docling_status_text = "running"
|
||||||
docling_style = "bold green"
|
docling_style = "bold green"
|
||||||
|
|
@ -228,9 +230,15 @@ class MonitorScreen(Screen):
|
||||||
else:
|
else:
|
||||||
docling_status_text = "stopped"
|
docling_status_text = "stopped"
|
||||||
docling_style = "bold red"
|
docling_style = "bold red"
|
||||||
|
|
||||||
docling_port = f"{docling_status['host']}:{docling_status['port']}" if (docling_running or docling_starting) else "N/A"
|
docling_port = (
|
||||||
docling_pid = str(docling_status.get("pid")) if docling_status.get("pid") else "N/A"
|
f"{docling_status['host']}:{docling_status['port']}"
|
||||||
|
if (docling_running or docling_starting)
|
||||||
|
else "N/A"
|
||||||
|
)
|
||||||
|
docling_pid = (
|
||||||
|
str(docling_status.get("pid")) if docling_status.get("pid") else "N/A"
|
||||||
|
)
|
||||||
|
|
||||||
if self.docling_table:
|
if self.docling_table:
|
||||||
self.docling_table.add_row(
|
self.docling_table.add_row(
|
||||||
|
|
@ -238,7 +246,7 @@ class MonitorScreen(Screen):
|
||||||
Text(docling_status_text, style=docling_style),
|
Text(docling_status_text, style=docling_style),
|
||||||
docling_port,
|
docling_port,
|
||||||
docling_pid,
|
docling_pid,
|
||||||
"Start/Stop/Logs"
|
"Start/Stop/Logs",
|
||||||
)
|
)
|
||||||
# Restore docling selection when it was the last active table
|
# Restore docling selection when it was the last active table
|
||||||
if self._last_selected_table == "docling":
|
if self._last_selected_table == "docling":
|
||||||
|
|
@ -321,27 +329,55 @@ class MonitorScreen(Screen):
|
||||||
self.operation_in_progress = True
|
self.operation_in_progress = True
|
||||||
try:
|
try:
|
||||||
# Check for port conflicts before attempting to start
|
# Check for port conflicts before attempting to start
|
||||||
ports_available, conflicts = await self.container_manager.check_ports_available()
|
(
|
||||||
|
ports_available,
|
||||||
|
conflicts,
|
||||||
|
) = await self.container_manager.check_ports_available()
|
||||||
if not ports_available:
|
if not ports_available:
|
||||||
# Show error notification instead of modal
|
# Show error notification instead of modal
|
||||||
conflict_msgs = []
|
conflict_msgs = []
|
||||||
for service_name, port, error_msg in conflicts[:3]: # Show first 3
|
for service_name, port, error_msg in conflicts[:3]: # Show first 3
|
||||||
conflict_msgs.append(f"{service_name} (port {port})")
|
conflict_msgs.append(f"{service_name} (port {port})")
|
||||||
|
|
||||||
conflict_str = ", ".join(conflict_msgs)
|
conflict_str = ", ".join(conflict_msgs)
|
||||||
if len(conflicts) > 3:
|
if len(conflicts) > 3:
|
||||||
conflict_str += f" and {len(conflicts) - 3} more"
|
conflict_str += f" and {len(conflicts) - 3} more"
|
||||||
|
|
||||||
self.notify(
|
self.notify(
|
||||||
f"Cannot start services: Port conflicts detected for {conflict_str}. "
|
f"Cannot start services: Port conflicts detected for {conflict_str}. "
|
||||||
f"Please stop the conflicting services first.",
|
f"Please stop the conflicting services first.",
|
||||||
severity="error",
|
severity="error",
|
||||||
timeout=10
|
timeout=10,
|
||||||
)
|
)
|
||||||
# Refresh to show current state
|
# Refresh to show current state
|
||||||
await self._refresh_services()
|
await self._refresh_services()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Check for version mismatch
|
||||||
|
(
|
||||||
|
has_mismatch,
|
||||||
|
container_version,
|
||||||
|
tui_version,
|
||||||
|
) = await self.container_manager.check_version_mismatch()
|
||||||
|
if has_mismatch and container_version:
|
||||||
|
# Show warning modal and wait for user decision
|
||||||
|
should_continue = await self.app.push_screen_wait(
|
||||||
|
VersionMismatchWarningModal(container_version, tui_version)
|
||||||
|
)
|
||||||
|
if not should_continue:
|
||||||
|
self.notify("Start cancelled", severity="information")
|
||||||
|
return
|
||||||
|
# Ensure OPENRAG_VERSION is set in .env BEFORE starting services
|
||||||
|
# This ensures docker compose reads the correct version
|
||||||
|
try:
|
||||||
|
from ..managers.env_manager import EnvManager
|
||||||
|
env_manager = EnvManager()
|
||||||
|
env_manager.ensure_openrag_version()
|
||||||
|
# Small delay to ensure .env file is written and flushed
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
except Exception:
|
||||||
|
pass # Continue even if version setting fails
|
||||||
|
|
||||||
# Show command output in modal dialog
|
# Show command output in modal dialog
|
||||||
command_generator = self.container_manager.start_services(cpu_mode)
|
command_generator = self.container_manager.start_services(cpu_mode)
|
||||||
modal = CommandOutputModal(
|
modal = CommandOutputModal(
|
||||||
|
|
@ -391,27 +427,30 @@ class MonitorScreen(Screen):
|
||||||
self.operation_in_progress = False
|
self.operation_in_progress = False
|
||||||
|
|
||||||
async def _upgrade_services(self) -> None:
|
async def _upgrade_services(self) -> None:
|
||||||
"""Upgrade services with progress updates."""
|
"""Check TUI version and show upgrade instructions."""
|
||||||
self.operation_in_progress = True
|
self.operation_in_progress = True
|
||||||
try:
|
try:
|
||||||
# Check for flow backups before upgrading
|
from ..utils.version_check import check_if_latest
|
||||||
if self._check_flow_backups():
|
|
||||||
# Show warning modal and wait for user decision
|
# Check if current version is latest
|
||||||
should_continue = await self.app.push_screen_wait(
|
is_latest, latest_version, current_version = await check_if_latest()
|
||||||
FlowBackupWarningModal(operation="upgrade")
|
|
||||||
|
if is_latest:
|
||||||
|
# Show "this is the latest version" toast
|
||||||
|
self.notify(
|
||||||
|
f"You are running the latest version ({current_version}).",
|
||||||
|
severity="success",
|
||||||
|
timeout=5,
|
||||||
)
|
)
|
||||||
if not should_continue:
|
else:
|
||||||
self.notify("Upgrade cancelled", severity="information")
|
# Show upgrade instructions in a modal dialog
|
||||||
return
|
await self.app.push_screen_wait(
|
||||||
|
UpgradeInstructionsModal(current_version, latest_version)
|
||||||
# Show command output in modal dialog
|
)
|
||||||
command_generator = self.container_manager.upgrade_services()
|
except Exception as e:
|
||||||
modal = CommandOutputModal(
|
self.notify(
|
||||||
"Upgrading Services",
|
f"Error checking version: {str(e)}", severity="error", timeout=10
|
||||||
command_generator,
|
|
||||||
on_complete=None, # We'll refresh in on_screen_resume instead
|
|
||||||
)
|
)
|
||||||
self.app.push_screen(modal)
|
|
||||||
finally:
|
finally:
|
||||||
self.operation_in_progress = False
|
self.operation_in_progress = False
|
||||||
|
|
||||||
|
|
@ -428,7 +467,7 @@ class MonitorScreen(Screen):
|
||||||
if not should_continue:
|
if not should_continue:
|
||||||
self.notify("Reset cancelled", severity="information")
|
self.notify("Reset cancelled", severity="information")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Show command output in modal dialog
|
# Show command output in modal dialog
|
||||||
command_generator = self.container_manager.reset_services()
|
command_generator = self.container_manager.reset_services()
|
||||||
modal = CommandOutputModal(
|
modal = CommandOutputModal(
|
||||||
|
|
@ -443,10 +482,11 @@ class MonitorScreen(Screen):
|
||||||
def _check_flow_backups(self) -> bool:
|
def _check_flow_backups(self) -> bool:
|
||||||
"""Check if there are any flow backups in ./flows/backup directory."""
|
"""Check if there are any flow backups in ./flows/backup directory."""
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
backup_dir = Path("flows/backup")
|
backup_dir = Path("flows/backup")
|
||||||
if not backup_dir.exists():
|
if not backup_dir.exists():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Check if there are any .json files in the backup directory
|
# Check if there are any .json files in the backup directory
|
||||||
backup_files = list(backup_dir.glob("*.json"))
|
backup_files = list(backup_dir.glob("*.json"))
|
||||||
|
|
@ -465,7 +505,7 @@ class MonitorScreen(Screen):
|
||||||
f"Cannot start docling serve: {error_msg}. "
|
f"Cannot start docling serve: {error_msg}. "
|
||||||
f"Please stop the conflicting service first.",
|
f"Please stop the conflicting service first.",
|
||||||
severity="error",
|
severity="error",
|
||||||
timeout=10
|
timeout=10,
|
||||||
)
|
)
|
||||||
# Refresh to show current state
|
# Refresh to show current state
|
||||||
await self._refresh_services()
|
await self._refresh_services()
|
||||||
|
|
@ -483,7 +523,9 @@ class MonitorScreen(Screen):
|
||||||
if success:
|
if success:
|
||||||
self.notify(message, severity="information")
|
self.notify(message, severity="information")
|
||||||
else:
|
else:
|
||||||
self.notify(f"Failed to start docling serve: {message}", severity="error")
|
self.notify(
|
||||||
|
f"Failed to start docling serve: {message}", severity="error"
|
||||||
|
)
|
||||||
# Refresh again to show final status (running or stopped)
|
# Refresh again to show final status (running or stopped)
|
||||||
await self._refresh_services()
|
await self._refresh_services()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -501,7 +543,9 @@ class MonitorScreen(Screen):
|
||||||
if success:
|
if success:
|
||||||
self.notify(message, severity="information")
|
self.notify(message, severity="information")
|
||||||
else:
|
else:
|
||||||
self.notify(f"Failed to stop docling serve: {message}", severity="error")
|
self.notify(
|
||||||
|
f"Failed to stop docling serve: {message}", severity="error"
|
||||||
|
)
|
||||||
# Refresh the services table to show updated status
|
# Refresh the services table to show updated status
|
||||||
await self._refresh_services()
|
await self._refresh_services()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -517,7 +561,9 @@ class MonitorScreen(Screen):
|
||||||
if success:
|
if success:
|
||||||
self.notify(message, severity="information")
|
self.notify(message, severity="information")
|
||||||
else:
|
else:
|
||||||
self.notify(f"Failed to restart docling serve: {message}", severity="error")
|
self.notify(
|
||||||
|
f"Failed to restart docling serve: {message}", severity="error"
|
||||||
|
)
|
||||||
# Refresh the services table to show updated status
|
# Refresh the services table to show updated status
|
||||||
await self._refresh_services()
|
await self._refresh_services()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -528,6 +574,7 @@ class MonitorScreen(Screen):
|
||||||
def _view_docling_logs(self) -> None:
|
def _view_docling_logs(self) -> None:
|
||||||
"""View docling serve logs."""
|
"""View docling serve logs."""
|
||||||
from .logs import LogsScreen
|
from .logs import LogsScreen
|
||||||
|
|
||||||
self.app.push_screen(LogsScreen(initial_service="docling-serve"))
|
self.app.push_screen(LogsScreen(initial_service="docling-serve"))
|
||||||
|
|
||||||
def _strip_ansi_codes(self, text: str) -> str:
|
def _strip_ansi_codes(self, text: str) -> str:
|
||||||
|
|
@ -728,7 +775,7 @@ class MonitorScreen(Screen):
|
||||||
Button("Upgrade", variant="warning", id=f"upgrade-btn{suffix}")
|
Button("Upgrade", variant="warning", id=f"upgrade-btn{suffix}")
|
||||||
)
|
)
|
||||||
controls.mount(Button("Reset", variant="error", id=f"reset-btn{suffix}"))
|
controls.mount(Button("Reset", variant="error", id=f"reset-btn{suffix}"))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
notify_with_diagnostics(
|
notify_with_diagnostics(
|
||||||
self.app, f"Error updating controls: {e}", severity="error"
|
self.app, f"Error updating controls: {e}", severity="error"
|
||||||
|
|
@ -748,6 +795,7 @@ class MonitorScreen(Screen):
|
||||||
|
|
||||||
# Use a random suffix for unique IDs
|
# Use a random suffix for unique IDs
|
||||||
import random
|
import random
|
||||||
|
|
||||||
suffix = f"-{random.randint(10000, 99999)}"
|
suffix = f"-{random.randint(10000, 99999)}"
|
||||||
|
|
||||||
# Add docling serve controls
|
# Add docling serve controls
|
||||||
|
|
@ -755,17 +803,21 @@ class MonitorScreen(Screen):
|
||||||
docling_status_value = docling_status["status"]
|
docling_status_value = docling_status["status"]
|
||||||
docling_running = docling_status_value == "running"
|
docling_running = docling_status_value == "running"
|
||||||
docling_starting = docling_status_value == "starting"
|
docling_starting = docling_status_value == "starting"
|
||||||
|
|
||||||
if docling_running:
|
if docling_running:
|
||||||
docling_controls.mount(
|
docling_controls.mount(
|
||||||
Button("Stop", variant="error", id=f"docling-stop-btn{suffix}")
|
Button("Stop", variant="error", id=f"docling-stop-btn{suffix}")
|
||||||
)
|
)
|
||||||
docling_controls.mount(
|
docling_controls.mount(
|
||||||
Button("Restart", variant="primary", id=f"docling-restart-btn{suffix}")
|
Button(
|
||||||
|
"Restart", variant="primary", id=f"docling-restart-btn{suffix}"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
elif docling_starting:
|
elif docling_starting:
|
||||||
# Show disabled button or no button when starting
|
# Show disabled button or no button when starting
|
||||||
start_btn = Button("Starting...", variant="warning", id=f"docling-start-btn{suffix}")
|
start_btn = Button(
|
||||||
|
"Starting...", variant="warning", id=f"docling-start-btn{suffix}"
|
||||||
|
)
|
||||||
start_btn.disabled = True
|
start_btn.disabled = True
|
||||||
docling_controls.mount(start_btn)
|
docling_controls.mount(start_btn)
|
||||||
else:
|
else:
|
||||||
|
|
@ -805,6 +857,7 @@ class MonitorScreen(Screen):
|
||||||
if selected_service:
|
if selected_service:
|
||||||
# Push the logs screen with the selected service
|
# Push the logs screen with the selected service
|
||||||
from .logs import LogsScreen
|
from .logs import LogsScreen
|
||||||
|
|
||||||
logs_screen = LogsScreen(initial_service=selected_service)
|
logs_screen = LogsScreen(initial_service=selected_service)
|
||||||
self.app.push_screen(logs_screen)
|
self.app.push_screen(logs_screen)
|
||||||
else:
|
else:
|
||||||
|
|
@ -927,7 +980,9 @@ class MonitorScreen(Screen):
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _focus_services_table(self, row: str | None = None, set_last: bool = True) -> None:
|
def _focus_services_table(
|
||||||
|
self, row: str | None = None, set_last: bool = True
|
||||||
|
) -> None:
|
||||||
"""Focus the services table and update selection."""
|
"""Focus the services table and update selection."""
|
||||||
if not self.services_table:
|
if not self.services_table:
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ from ..managers.container_manager import ContainerManager, ServiceStatus
|
||||||
from ..managers.env_manager import EnvManager
|
from ..managers.env_manager import EnvManager
|
||||||
from ..managers.docling_manager import DoclingManager
|
from ..managers.docling_manager import DoclingManager
|
||||||
from ..widgets.command_modal import CommandOutputModal
|
from ..widgets.command_modal import CommandOutputModal
|
||||||
|
from ..widgets.version_mismatch_warning_modal import VersionMismatchWarningModal
|
||||||
|
|
||||||
|
|
||||||
class WelcomeScreen(Screen):
|
class WelcomeScreen(Screen):
|
||||||
|
|
@ -450,6 +451,28 @@ class WelcomeScreen(Screen):
|
||||||
|
|
||||||
# Step 1: Start container services first (to create the network)
|
# Step 1: Start container services first (to create the network)
|
||||||
if self.container_manager.is_available():
|
if self.container_manager.is_available():
|
||||||
|
# Check for version mismatch before starting
|
||||||
|
has_mismatch, container_version, tui_version = await self.container_manager.check_version_mismatch()
|
||||||
|
if has_mismatch and container_version:
|
||||||
|
# Show warning modal and wait for user decision
|
||||||
|
should_continue = await self.app.push_screen_wait(
|
||||||
|
VersionMismatchWarningModal(container_version, tui_version)
|
||||||
|
)
|
||||||
|
if not should_continue:
|
||||||
|
self.notify("Start cancelled", severity="information")
|
||||||
|
return
|
||||||
|
# Ensure OPENRAG_VERSION is set in .env BEFORE starting services
|
||||||
|
# This ensures docker compose reads the correct version
|
||||||
|
try:
|
||||||
|
from ..managers.env_manager import EnvManager
|
||||||
|
env_manager = EnvManager()
|
||||||
|
env_manager.ensure_openrag_version()
|
||||||
|
# Small delay to ensure .env file is written and flushed
|
||||||
|
import asyncio
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
except Exception:
|
||||||
|
pass # Continue even if version setting fails
|
||||||
|
|
||||||
command_generator = self.container_manager.start_services()
|
command_generator = self.container_manager.start_services()
|
||||||
modal = CommandOutputModal(
|
modal = CommandOutputModal(
|
||||||
"Starting Container Services",
|
"Starting Container Services",
|
||||||
|
|
|
||||||
175
src/tui/utils/version_check.py
Normal file
175
src/tui/utils/version_check.py
Normal file
|
|
@ -0,0 +1,175 @@
|
||||||
|
"""Version checking utilities for OpenRAG TUI."""
|
||||||
|
|
||||||
|
from typing import Optional, Tuple
|
||||||
|
from utils.logging_config import get_logger
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_latest_docker_version(image_name: str = "langflowai/openrag-backend") -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Get the latest version tag from Docker Hub for OpenRAG containers.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
image_name: Name of the Docker image to check (default: "langflowai/openrag-backend")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Latest version string if found, None otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||||
|
# Docker Hub API v2 endpoint for tags
|
||||||
|
url = f"https://hub.docker.com/v2/repositories/{image_name}/tags/"
|
||||||
|
params = {"page_size": 100, "ordering": "-last_updated"}
|
||||||
|
|
||||||
|
response = await client.get(url, params=params)
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
tags = data.get("results", [])
|
||||||
|
|
||||||
|
# Filter out non-version tags and find the latest version
|
||||||
|
version_tags = []
|
||||||
|
for tag in tags:
|
||||||
|
tag_name = tag.get("name", "")
|
||||||
|
# Skip architecture-specific tags (amd64, arm64) and "latest"
|
||||||
|
if tag_name in ["latest", "amd64", "arm64"]:
|
||||||
|
continue
|
||||||
|
# Skip tags that don't look like version numbers
|
||||||
|
# Version format: X.Y.Z (e.g., 0.1.47)
|
||||||
|
# Check if it starts with a digit and contains only digits, dots, and hyphens
|
||||||
|
if tag_name and tag_name[0].isdigit():
|
||||||
|
# Remove dots and hyphens, check if rest is digits
|
||||||
|
cleaned = tag_name.replace(".", "").replace("-", "")
|
||||||
|
if cleaned.isdigit():
|
||||||
|
version_tags.append(tag_name)
|
||||||
|
|
||||||
|
if not version_tags:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Sort versions properly and return the latest
|
||||||
|
# Use a tuple-based sort key for proper version comparison
|
||||||
|
def version_sort_key(v: str) -> tuple:
|
||||||
|
"""Convert version string to tuple for sorting."""
|
||||||
|
try:
|
||||||
|
parts = []
|
||||||
|
for part in v.split('.'):
|
||||||
|
# Extract numeric part
|
||||||
|
numeric_part = ''
|
||||||
|
for char in part:
|
||||||
|
if char.isdigit():
|
||||||
|
numeric_part += char
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
parts.append(int(numeric_part) if numeric_part else 0)
|
||||||
|
# Pad to at least 3 parts for consistent comparison
|
||||||
|
while len(parts) < 3:
|
||||||
|
parts.append(0)
|
||||||
|
return tuple(parts)
|
||||||
|
except Exception:
|
||||||
|
# Fallback: return tuple of zeros if parsing fails
|
||||||
|
return (0, 0, 0)
|
||||||
|
|
||||||
|
version_tags.sort(key=version_sort_key)
|
||||||
|
return version_tags[-1]
|
||||||
|
else:
|
||||||
|
logger.warning(f"Docker Hub API returned status {response.status_code}")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Error checking Docker Hub for latest version: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_version() -> str:
|
||||||
|
"""
|
||||||
|
Get the current installed version of OpenRAG.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Version string or "unknown" if not available
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from importlib.metadata import version
|
||||||
|
return version("openrag")
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
from tui import __version__
|
||||||
|
return __version__
|
||||||
|
except Exception:
|
||||||
|
return "unknown"
|
||||||
|
|
||||||
|
|
||||||
|
def compare_versions(version1: str, version2: str) -> int:
|
||||||
|
"""
|
||||||
|
Compare two version strings.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
version1: First version string
|
||||||
|
version2: Second version string
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
-1 if version1 < version2, 0 if equal, 1 if version1 > version2
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Simple version comparison by splitting on dots and comparing parts
|
||||||
|
def normalize_version(v: str) -> list:
|
||||||
|
parts = []
|
||||||
|
for part in v.split('.'):
|
||||||
|
# Split on non-numeric characters and take the first numeric part
|
||||||
|
numeric_part = ''
|
||||||
|
for char in part:
|
||||||
|
if char.isdigit():
|
||||||
|
numeric_part += char
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
parts.append(int(numeric_part) if numeric_part else 0)
|
||||||
|
return parts
|
||||||
|
|
||||||
|
v1_parts = normalize_version(version1)
|
||||||
|
v2_parts = normalize_version(version2)
|
||||||
|
|
||||||
|
# Pad shorter version with zeros
|
||||||
|
max_len = max(len(v1_parts), len(v2_parts))
|
||||||
|
v1_parts.extend([0] * (max_len - len(v1_parts)))
|
||||||
|
v2_parts.extend([0] * (max_len - len(v2_parts)))
|
||||||
|
|
||||||
|
for i in range(max_len):
|
||||||
|
if v1_parts[i] < v2_parts[i]:
|
||||||
|
return -1
|
||||||
|
elif v1_parts[i] > v2_parts[i]:
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Error comparing versions: {e}")
|
||||||
|
# Fallback: string comparison
|
||||||
|
if version1 < version2:
|
||||||
|
return -1
|
||||||
|
elif version1 > version2:
|
||||||
|
return 1
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
async def check_if_latest() -> Tuple[bool, Optional[str], str]:
|
||||||
|
"""
|
||||||
|
Check if the current version is the latest available on Docker Hub.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (is_latest, latest_version, current_version)
|
||||||
|
"""
|
||||||
|
current = get_current_version()
|
||||||
|
latest = await get_latest_docker_version()
|
||||||
|
|
||||||
|
if latest is None:
|
||||||
|
# If we can't check, assume current is latest
|
||||||
|
return True, None, current
|
||||||
|
|
||||||
|
if current == "unknown":
|
||||||
|
# If we can't determine current version, assume it's not latest
|
||||||
|
return False, latest, current
|
||||||
|
|
||||||
|
comparison = compare_versions(current, latest)
|
||||||
|
is_latest = comparison >= 0
|
||||||
|
|
||||||
|
return is_latest, latest, current
|
||||||
|
|
||||||
112
src/tui/widgets/upgrade_instructions_modal.py
Normal file
112
src/tui/widgets/upgrade_instructions_modal.py
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
"""Upgrade instructions modal for OpenRAG TUI."""
|
||||||
|
|
||||||
|
from textual.app import ComposeResult
|
||||||
|
from textual.containers import Container, Horizontal
|
||||||
|
from textual.screen import ModalScreen
|
||||||
|
from textual.widgets import Button, Static, Label
|
||||||
|
|
||||||
|
|
||||||
|
class UpgradeInstructionsModal(ModalScreen[bool]):
|
||||||
|
"""Modal dialog showing upgrade instructions when not on latest version."""
|
||||||
|
|
||||||
|
DEFAULT_CSS = """
|
||||||
|
UpgradeInstructionsModal {
|
||||||
|
align: center middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dialog {
|
||||||
|
width: 75;
|
||||||
|
height: auto;
|
||||||
|
border: solid #3f3f46;
|
||||||
|
background: #27272a;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#title {
|
||||||
|
background: #3f3f46;
|
||||||
|
color: #fafafa;
|
||||||
|
padding: 1 2;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
text-style: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
#message {
|
||||||
|
padding: 2;
|
||||||
|
color: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
#button-row {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
align: center middle;
|
||||||
|
padding: 1;
|
||||||
|
margin-top: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#button-row Button {
|
||||||
|
margin: 0 1;
|
||||||
|
min-width: 16;
|
||||||
|
background: #27272a;
|
||||||
|
color: #fafafa;
|
||||||
|
border: round #52525b;
|
||||||
|
text-style: none;
|
||||||
|
tint: transparent 0%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#button-row Button:hover {
|
||||||
|
background: #27272a !important;
|
||||||
|
color: #fafafa !important;
|
||||||
|
border: round #52525b;
|
||||||
|
tint: transparent 0%;
|
||||||
|
text-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#button-row Button:focus {
|
||||||
|
background: #27272a !important;
|
||||||
|
color: #fafafa !important;
|
||||||
|
border: round #ec4899;
|
||||||
|
tint: transparent 0%;
|
||||||
|
text-style: none;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, current_version: str, latest_version: str):
|
||||||
|
"""Initialize the upgrade instructions modal.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
current_version: Current TUI version
|
||||||
|
latest_version: Latest available version
|
||||||
|
"""
|
||||||
|
super().__init__()
|
||||||
|
self.current_version = current_version
|
||||||
|
self.latest_version = latest_version
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
"""Create the modal dialog layout."""
|
||||||
|
with Container(id="dialog"):
|
||||||
|
yield Label("📦 Upgrade Available", id="title")
|
||||||
|
yield Static(
|
||||||
|
f"Current version: {self.current_version}\n"
|
||||||
|
f"Latest version: {self.latest_version}\n\n"
|
||||||
|
"To upgrade the TUI:\n"
|
||||||
|
"1. Exit TUI (press 'q')\n"
|
||||||
|
"2. Run one of:\n"
|
||||||
|
" • pip install --upgrade openrag\n"
|
||||||
|
" • uv pip install --upgrade openrag\n"
|
||||||
|
" • uvx --from openrag openrag\n"
|
||||||
|
"3. Restart: openrag\n\n"
|
||||||
|
"After upgrading, containers will automatically use the new version.",
|
||||||
|
id="message"
|
||||||
|
)
|
||||||
|
with Horizontal(id="button-row"):
|
||||||
|
yield Button("Close", id="close-btn")
|
||||||
|
|
||||||
|
def on_mount(self) -> None:
|
||||||
|
"""Focus the close button."""
|
||||||
|
self.query_one("#close-btn", Button).focus()
|
||||||
|
|
||||||
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||||
|
"""Handle button presses."""
|
||||||
|
self.dismiss(True) # Just close the modal
|
||||||
|
|
||||||
114
src/tui/widgets/version_mismatch_warning_modal.py
Normal file
114
src/tui/widgets/version_mismatch_warning_modal.py
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
"""Version mismatch warning modal for OpenRAG TUI."""
|
||||||
|
|
||||||
|
from textual.app import ComposeResult
|
||||||
|
from textual.containers import Container, Horizontal
|
||||||
|
from textual.screen import ModalScreen
|
||||||
|
from textual.widgets import Button, Static, Label
|
||||||
|
|
||||||
|
|
||||||
|
class VersionMismatchWarningModal(ModalScreen[bool]):
|
||||||
|
"""Modal dialog to warn about version mismatch before starting services."""
|
||||||
|
|
||||||
|
DEFAULT_CSS = """
|
||||||
|
VersionMismatchWarningModal {
|
||||||
|
align: center middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dialog {
|
||||||
|
width: 70;
|
||||||
|
height: auto;
|
||||||
|
border: solid #3f3f46;
|
||||||
|
background: #27272a;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#title {
|
||||||
|
background: #3f3f46;
|
||||||
|
color: #fafafa;
|
||||||
|
padding: 1 2;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
text-style: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
#message {
|
||||||
|
padding: 2;
|
||||||
|
color: #fafafa;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#button-row {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
align: center middle;
|
||||||
|
padding: 1;
|
||||||
|
margin-top: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#button-row Button {
|
||||||
|
margin: 0 1;
|
||||||
|
min-width: 16;
|
||||||
|
background: #27272a;
|
||||||
|
color: #fafafa;
|
||||||
|
border: round #52525b;
|
||||||
|
text-style: none;
|
||||||
|
tint: transparent 0%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#button-row Button:hover {
|
||||||
|
background: #27272a !important;
|
||||||
|
color: #fafafa !important;
|
||||||
|
border: round #52525b;
|
||||||
|
tint: transparent 0%;
|
||||||
|
text-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#button-row Button:focus {
|
||||||
|
background: #27272a !important;
|
||||||
|
color: #fafafa !important;
|
||||||
|
border: round #ec4899;
|
||||||
|
tint: transparent 0%;
|
||||||
|
text-style: none;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, container_version: str, tui_version: str):
|
||||||
|
"""Initialize the warning modal.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
container_version: Version of existing containers
|
||||||
|
tui_version: Current TUI version
|
||||||
|
"""
|
||||||
|
super().__init__()
|
||||||
|
self.container_version = container_version
|
||||||
|
self.tui_version = tui_version
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
"""Create the modal dialog layout."""
|
||||||
|
with Container(id="dialog"):
|
||||||
|
yield Label("⚠ Version Mismatch Detected", id="title")
|
||||||
|
yield Static(
|
||||||
|
f"Existing containers are running version {self.container_version}\n"
|
||||||
|
f"Current TUI version is {self.tui_version}\n\n"
|
||||||
|
f"Starting services will update containers to version {self.tui_version}.\n"
|
||||||
|
f"This may cause compatibility issues with your flows.\n\n"
|
||||||
|
f"⚠️ Please backup your flows before continuing:\n"
|
||||||
|
f" Your flows are in ./flows/ directory\n\n"
|
||||||
|
f"Do you want to continue?",
|
||||||
|
id="message"
|
||||||
|
)
|
||||||
|
with Horizontal(id="button-row"):
|
||||||
|
yield Button("Cancel", id="cancel-btn")
|
||||||
|
yield Button("Continue", id="continue-btn")
|
||||||
|
|
||||||
|
def on_mount(self) -> None:
|
||||||
|
"""Focus the cancel button by default for safety."""
|
||||||
|
self.query_one("#cancel-btn", Button).focus()
|
||||||
|
|
||||||
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||||
|
"""Handle button presses."""
|
||||||
|
if event.button.id == "continue-btn":
|
||||||
|
self.dismiss(True) # User wants to continue
|
||||||
|
else:
|
||||||
|
self.dismiss(False) # User cancelled
|
||||||
|
|
||||||
Loading…
Add table
Reference in a new issue