Initial commit: CloudRestoreAS v1.0.0 - Aplicación completa de restauración automática SQL Server
This commit is contained in:
228
app/ui/jobs_tab.py
Normal file
228
app/ui/jobs_tab.py
Normal file
@@ -0,0 +1,228 @@
|
||||
"""Tab de Jobs."""
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QTableWidget,
|
||||
QTableWidgetItem, QPushButton, QLineEdit, QLabel,
|
||||
QComboBox, QSplitter, QTextEdit, QGroupBox
|
||||
)
|
||||
from PySide6.QtCore import Qt
|
||||
|
||||
from ..db.job_repository import JobRepository
|
||||
from ..db.job_step_repository import JobStepRepository
|
||||
from ..constants import JobStatus
|
||||
|
||||
|
||||
class JobsTab(QWidget):
|
||||
"""Tab de gestión y visualización de jobs."""
|
||||
|
||||
def __init__(self, engine):
|
||||
"""Inicializa el tab."""
|
||||
super().__init__()
|
||||
self.engine = engine
|
||||
self.selected_job_id = None
|
||||
self._setup_ui()
|
||||
self.refresh_jobs()
|
||||
|
||||
def _setup_ui(self):
|
||||
"""Configura la interfaz."""
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
# Filtros
|
||||
filters_layout = QHBoxLayout()
|
||||
|
||||
filters_layout.addWidget(QLabel("Buscar:"))
|
||||
self.search_input = QLineEdit()
|
||||
self.search_input.setPlaceholderText("Nombre de archivo o Job ID...")
|
||||
self.search_input.textChanged.connect(self.refresh_jobs)
|
||||
filters_layout.addWidget(self.search_input)
|
||||
|
||||
filters_layout.addWidget(QLabel("Estado:"))
|
||||
self.status_filter = QComboBox()
|
||||
self.status_filter.addItems([
|
||||
"Todos",
|
||||
JobStatus.QUEUED,
|
||||
JobStatus.EXTRACTING,
|
||||
JobStatus.RESTORING,
|
||||
JobStatus.COMPLETED,
|
||||
JobStatus.FAILED
|
||||
])
|
||||
self.status_filter.currentTextChanged.connect(self.refresh_jobs)
|
||||
filters_layout.addWidget(self.status_filter)
|
||||
|
||||
refresh_btn = QPushButton("🔄 Actualizar")
|
||||
refresh_btn.clicked.connect(self.refresh_jobs)
|
||||
filters_layout.addWidget(refresh_btn)
|
||||
|
||||
filters_layout.addStretch()
|
||||
|
||||
layout.addLayout(filters_layout)
|
||||
|
||||
# Splitter para tabla y detalles
|
||||
splitter = QSplitter(Qt.Orientation.Vertical)
|
||||
|
||||
# Tabla de jobs
|
||||
self.jobs_table = QTableWidget()
|
||||
self.jobs_table.setColumnCount(8)
|
||||
self.jobs_table.setHorizontalHeaderLabels([
|
||||
"Job ID", "Fecha", "Archivo", "Nodo", "DB", "Estado", "Intentos", "Tiempo (s)"
|
||||
])
|
||||
self.jobs_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
|
||||
self.jobs_table.setSelectionMode(QTableWidget.SelectionMode.SingleSelection)
|
||||
self.jobs_table.itemSelectionChanged.connect(self._on_selection_changed)
|
||||
|
||||
splitter.addWidget(self.jobs_table)
|
||||
|
||||
# Detalles del job seleccionado
|
||||
details_widget = QWidget()
|
||||
details_layout = QVBoxLayout(details_widget)
|
||||
|
||||
details_group = QGroupBox("Detalles del Job")
|
||||
details_group_layout = QVBoxLayout()
|
||||
|
||||
self.job_details_text = QTextEdit()
|
||||
self.job_details_text.setReadOnly(True)
|
||||
self.job_details_text.setMaximumHeight(200)
|
||||
details_group_layout.addWidget(self.job_details_text)
|
||||
|
||||
details_group.setLayout(details_group_layout)
|
||||
details_layout.addWidget(details_group)
|
||||
|
||||
steps_group = QGroupBox("Pasos del Job")
|
||||
steps_group_layout = QVBoxLayout()
|
||||
|
||||
self.steps_table = QTableWidget()
|
||||
self.steps_table.setColumnCount(5)
|
||||
self.steps_table.setHorizontalHeaderLabels([
|
||||
"Paso", "Inicio", "Duración (ms)", "Exit Code", "Error"
|
||||
])
|
||||
steps_group_layout.addWidget(self.steps_table)
|
||||
|
||||
steps_group.setLayout(steps_group_layout)
|
||||
details_layout.addWidget(steps_group)
|
||||
|
||||
splitter.addWidget(details_widget)
|
||||
|
||||
splitter.setStretchFactor(0, 3)
|
||||
splitter.setStretchFactor(1, 2)
|
||||
|
||||
layout.addWidget(splitter)
|
||||
|
||||
def refresh_jobs(self):
|
||||
"""Refresca la lista de jobs."""
|
||||
# Obtener filtros
|
||||
search_text = self.search_input.text().lower()
|
||||
status_filter = self.status_filter.currentText()
|
||||
|
||||
# Obtener jobs
|
||||
if status_filter == "Todos":
|
||||
jobs = JobRepository.get_all(limit=100)
|
||||
else:
|
||||
jobs = JobRepository.get_all(status=status_filter, limit=100)
|
||||
|
||||
# Filtrar por búsqueda
|
||||
if search_text:
|
||||
jobs = [
|
||||
j for j in jobs
|
||||
if search_text in j.job_id.lower() or
|
||||
search_text in j.source_name.lower()
|
||||
]
|
||||
|
||||
# Llenar tabla
|
||||
self.jobs_table.setRowCount(len(jobs))
|
||||
|
||||
for i, job in enumerate(jobs):
|
||||
self.jobs_table.setItem(i, 0, QTableWidgetItem(job.job_id[:8]))
|
||||
|
||||
# Fecha
|
||||
created = job.created_at.split("T")[0] if "T" in job.created_at else job.created_at
|
||||
self.jobs_table.setItem(i, 1, QTableWidgetItem(created))
|
||||
|
||||
self.jobs_table.setItem(i, 2, QTableWidgetItem(job.source_name))
|
||||
self.jobs_table.setItem(i, 3, QTableWidgetItem(job.node_name or "N/A"))
|
||||
self.jobs_table.setItem(i, 4, QTableWidgetItem(job.db_name or "N/A"))
|
||||
|
||||
status_item = QTableWidgetItem(job.status)
|
||||
if job.status == JobStatus.COMPLETED:
|
||||
status_item.setForeground(Qt.GlobalColor.darkGreen)
|
||||
elif "failed" in job.status:
|
||||
status_item.setForeground(Qt.GlobalColor.red)
|
||||
self.jobs_table.setItem(i, 5, status_item)
|
||||
|
||||
self.jobs_table.setItem(i, 6, QTableWidgetItem(str(job.attempts)))
|
||||
|
||||
# Tiempo
|
||||
if job.total_ms:
|
||||
time_s = job.total_ms / 1000
|
||||
self.jobs_table.setItem(i, 7, QTableWidgetItem(f"{time_s:.1f}"))
|
||||
else:
|
||||
self.jobs_table.setItem(i, 7, QTableWidgetItem("N/A"))
|
||||
|
||||
# Guardar job_id en la fila
|
||||
self.jobs_table.item(i, 0).setData(Qt.ItemDataRole.UserRole, job.job_id)
|
||||
|
||||
self.jobs_table.resizeColumnsToContents()
|
||||
|
||||
def _on_selection_changed(self):
|
||||
"""Maneja el cambio de selección en la tabla."""
|
||||
selected_items = self.jobs_table.selectedItems()
|
||||
if not selected_items:
|
||||
return
|
||||
|
||||
# Obtener job_id de la fila seleccionada
|
||||
row = selected_items[0].row()
|
||||
job_id = self.jobs_table.item(row, 0).data(Qt.ItemDataRole.UserRole)
|
||||
|
||||
if job_id != self.selected_job_id:
|
||||
self.selected_job_id = job_id
|
||||
self._load_job_details()
|
||||
|
||||
def _load_job_details(self):
|
||||
"""Carga los detalles del job seleccionado."""
|
||||
if not self.selected_job_id:
|
||||
return
|
||||
|
||||
job = JobRepository.get(self.selected_job_id)
|
||||
if not job:
|
||||
return
|
||||
|
||||
# Mostrar detalles
|
||||
details = f"""
|
||||
Job ID: {job.job_id}
|
||||
Archivo: {job.source_name}
|
||||
Ruta: {job.source_path}
|
||||
Nodo: {job.node_name or 'N/A'}
|
||||
Base de Datos: {job.db_name or 'N/A'}
|
||||
Estado: {job.status}
|
||||
Intentos: {job.attempts}
|
||||
|
||||
Creado: {job.created_at}
|
||||
Iniciado: {job.started_at or 'N/A'}
|
||||
Finalizado: {job.finished_at or 'N/A'}
|
||||
|
||||
Tiempos:
|
||||
- Total: {job.total_ms or 0} ms
|
||||
- Extracción: {job.extract_ms or 0} ms
|
||||
- Filelist: {job.filelist_ms or 0} ms
|
||||
- Restauración: {job.restore_ms or 0} ms
|
||||
|
||||
Último Error: {job.last_error or 'N/A'}
|
||||
""".strip()
|
||||
|
||||
self.job_details_text.setPlainText(details)
|
||||
|
||||
# Cargar steps
|
||||
steps = JobStepRepository.get_by_job(self.selected_job_id)
|
||||
|
||||
self.steps_table.setRowCount(len(steps))
|
||||
|
||||
for i, step in enumerate(steps):
|
||||
self.steps_table.setItem(i, 0, QTableWidgetItem(step.step))
|
||||
|
||||
started = step.started_at.split("T")[1].split(".")[0] if "T" in step.started_at else step.started_at
|
||||
self.steps_table.setItem(i, 1, QTableWidgetItem(started))
|
||||
|
||||
self.steps_table.setItem(i, 2, QTableWidgetItem(str(step.duration_ms or 0)))
|
||||
self.steps_table.setItem(i, 3, QTableWidgetItem(str(step.exit_code or "")))
|
||||
self.steps_table.setItem(i, 4, QTableWidgetItem(step.error or ""))
|
||||
|
||||
self.steps_table.resizeColumnsToContents()
|
||||
Reference in New Issue
Block a user