""" Jobs Widget for Python Scheduler Display and manage jobs list """ import socket from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem, QPushButton, QHeaderView, QLabel, QMessageBox, QMenu ) from PyQt6.QtCore import Qt from PyQt6.QtGui import QAction from loguru import logger from core.database import SchedulerDatabase from core.config import SchedulerConfig from gui.job_editor_dialog import JobEditorDialog from gui.schedule_editor_dialog import ScheduleEditorDialog class JobsWidget(QWidget): """Jobs list and management widget""" def __init__(self, db: SchedulerDatabase, config: SchedulerConfig): super().__init__() self.db = db self.config = config self._setup_ui() self.refresh() def _setup_ui(self): """Setup UI""" layout = QVBoxLayout(self) # Header header_layout = QHBoxLayout() header_label = QLabel("Configured Jobs") header_label.setStyleSheet("font-size: 14px; font-weight: bold;") header_layout.addWidget(header_label) header_layout.addStretch() # Action Buttons self.new_job_btn = QPushButton("New Job") self.new_job_btn.setStyleSheet("background-color: #4CAF50; color: white; padding: 5px 15px;") self.new_job_btn.clicked.connect(self._new_job) header_layout.addWidget(self.new_job_btn) self.edit_job_btn = QPushButton("Edit") self.edit_job_btn.clicked.connect(self._edit_job) self.edit_job_btn.setEnabled(False) header_layout.addWidget(self.edit_job_btn) self.run_job_btn = QPushButton("Run Now") self.run_job_btn.clicked.connect(self._run_job) self.run_job_btn.setEnabled(False) header_layout.addWidget(self.run_job_btn) self.create_schedule_btn = QPushButton("Create Schedule") self.create_schedule_btn.clicked.connect(self._create_schedule) self.create_schedule_btn.setEnabled(False) header_layout.addWidget(self.create_schedule_btn) self.delete_job_btn = QPushButton("Delete") self.delete_job_btn.setStyleSheet("background-color: #f44336; color: white;") self.delete_job_btn.clicked.connect(self._delete_job) self.delete_job_btn.setEnabled(False) header_layout.addWidget(self.delete_job_btn) self.refresh_btn = QPushButton("Refresh") self.refresh_btn.clicked.connect(self.refresh) header_layout.addWidget(self.refresh_btn) layout.addLayout(header_layout) # Jobs table self.table = QTableWidget() self.table.setColumnCount(7) self.table.setHorizontalHeaderLabels([ "ID", "Name", "Type", "Enabled", "Criticality", "Tags", "Host" ]) # Configure table self.table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) self.table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows) self.table.setAlternatingRowColors(True) self.table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) self.table.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.table.customContextMenuRequested.connect(self._show_context_menu) self.table.itemSelectionChanged.connect(self._on_selection_changed) self.table.itemDoubleClicked.connect(lambda: self._edit_job()) layout.addWidget(self.table) def refresh(self): """Refresh jobs list — shows only jobs for current host (or any-host jobs)""" import json try: current_hostname = socket.gethostname() jobs = self.db.get_all_jobs(hostname=current_hostname) self.table.setRowCount(len(jobs)) for row, job in enumerate(jobs): # ID self.table.setItem(row, 0, QTableWidgetItem(str(job['id']))) # Name name_item = QTableWidgetItem(job['name']) name_item.setFont(self.table.font()) if job['criticality'] == 'critical': name_item.setForeground(Qt.GlobalColor.red) self.table.setItem(row, 1, name_item) # Type self.table.setItem(row, 2, QTableWidgetItem(job['job_type'])) # Enabled enabled_item = QTableWidgetItem("✓ Yes" if job['enabled'] else "✗ No") if job['enabled']: enabled_item.setForeground(Qt.GlobalColor.darkGreen) else: enabled_item.setForeground(Qt.GlobalColor.gray) self.table.setItem(row, 3, enabled_item) # Criticality crit_item = QTableWidgetItem(job['criticality'].upper()) if job['criticality'] == 'critical': crit_item.setForeground(Qt.GlobalColor.red) elif job['criticality'] == 'high': crit_item.setForeground(Qt.GlobalColor.darkYellow) self.table.setItem(row, 4, crit_item) # Tags tags = json.loads(job['tags']) if job['tags'] else [] tags_str = ", ".join(tags) if tags else "-" self.table.setItem(row, 5, QTableWidgetItem(tags_str)) # Host host_val = job.get('hostname', '') or '(any)' self.table.setItem(row, 6, QTableWidgetItem(host_val)) logger.debug(f"Refreshed jobs table: {len(jobs)} jobs for host '{current_hostname}'") except Exception as e: logger.error(f"Error refreshing jobs: {e}") QMessageBox.critical(self, "Error", f"Failed to load jobs: {e}") def _on_selection_changed(self): """Handle selection change""" has_selection = len(self.table.selectedItems()) > 0 self.edit_job_btn.setEnabled(has_selection) self.run_job_btn.setEnabled(has_selection) self.create_schedule_btn.setEnabled(has_selection) self.delete_job_btn.setEnabled(has_selection) def _get_selected_job_id(self) -> int: """Get selected job ID""" selected_rows = self.table.selectionModel().selectedRows() if not selected_rows: return None row = selected_rows[0].row() job_id_item = self.table.item(row, 0) return int(job_id_item.text()) if job_id_item else None def _new_job(self): """Open dialog to create new job""" dialog = JobEditorDialog(self, self.db) if dialog.exec(): self.refresh() logger.info("New job created, refreshing table") def _edit_job(self): """Open dialog to edit selected job""" job_id = self._get_selected_job_id() if not job_id: return dialog = JobEditorDialog(self, self.db, job_id=job_id) if dialog.exec(): self.refresh() logger.info(f"Job {job_id} edited, refreshing table") def _run_job(self): """Execute selected job immediately""" job_id = self._get_selected_job_id() if not job_id: return job = self.db.get_job(job_id) reply = QMessageBox.question( self, "Run Job", f"Execute job '{job['name']}' now?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No ) if reply == QMessageBox.StandardButton.Yes: try: from daemon.job_executor import JobExecutor executor = JobExecutor(self.db, self.config) execution_id = executor.execute_job(job_id, triggered_by='manual_gui') QMessageBox.information( self, "Job Started", f"Job '{job['name']}' started (Execution ID: {execution_id})\n\nCheck History tab for results." ) logger.info(f"Manually executed job {job_id} from GUI") except Exception as e: logger.error(f"Error executing job: {e}") QMessageBox.critical(self, "Error", f"Failed to execute job: {e}") def _create_schedule(self): """Create schedule for selected job""" job_id = self._get_selected_job_id() if not job_id: return try: dialog = ScheduleEditorDialog( self, self.db, self.config, pre_selected_job_id=job_id ) if dialog.exec(): QMessageBox.information( self, "Success", "Schedule created successfully!\n\nCheck the Schedules tab to see it." ) logger.info(f"Created schedule for job {job_id} from Jobs tab") except Exception as e: logger.error(f"Error creating schedule: {e}") QMessageBox.critical(self, "Error", f"Failed to create schedule: {e}") def _delete_job(self): """Delete selected job""" job_id = self._get_selected_job_id() if not job_id: return job = self.db.get_job(job_id) reply = QMessageBox.warning( self, "Delete Job", f"Are you sure you want to delete job '{job['name']}'?\n\nThis action cannot be undone.", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No ) if reply == QMessageBox.StandardButton.Yes: try: self.db.delete_job(job_id) QMessageBox.information(self, "Success", f"Job '{job['name']}' deleted successfully") self.refresh() logger.info(f"Deleted job {job_id}") except Exception as e: logger.error(f"Error deleting job: {e}") QMessageBox.critical(self, "Error", f"Failed to delete job: {e}") def _toggle_enabled(self): """Toggle enabled status of selected job""" job_id = self._get_selected_job_id() if not job_id: return job = self.db.get_job(job_id) new_enabled = not job['enabled'] try: self.db.update_job(job_id, enabled=new_enabled) status_text = "enabled" if new_enabled else "disabled" QMessageBox.information(self, "Success", f"Job '{job['name']}' {status_text}") self.refresh() logger.info(f"Toggled job {job_id} enabled={new_enabled}") except Exception as e: logger.error(f"Error toggling job: {e}") QMessageBox.critical(self, "Error", f"Failed to toggle job: {e}") def _show_context_menu(self, position): """Show right-click context menu""" job_id = self._get_selected_job_id() if not job_id: return job = self.db.get_job(job_id) menu = QMenu(self) # Edit action edit_action = QAction("Edit...", self) edit_action.triggered.connect(self._edit_job) menu.addAction(edit_action) # Run action run_action = QAction("Run Now", self) run_action.triggered.connect(self._run_job) menu.addAction(run_action) menu.addSeparator() # Create Schedule action schedule_action = QAction("Create Schedule...", self) schedule_action.triggered.connect(self._create_schedule) menu.addAction(schedule_action) menu.addSeparator() # Enable/Disable toggle toggle_text = "Disable" if job['enabled'] else "Enable" toggle_action = QAction(toggle_text, self) toggle_action.triggered.connect(self._toggle_enabled) menu.addAction(toggle_action) menu.addSeparator() # Delete action delete_action = QAction("Delete", self) delete_action.triggered.connect(self._delete_job) menu.addAction(delete_action) menu.exec(self.table.viewport().mapToGlobal(position))