""" Job Editor Dialog Form for creating and editing scheduled jobs """ import socket from PyQt6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, QGroupBox, QLineEdit, QTextEdit, QComboBox, QSpinBox, QCheckBox, QPushButton, QFileDialog, QLabel, QMessageBox ) from PyQt6.QtCore import Qt from loguru import logger import json from pathlib import Path from core.database import SchedulerDatabase class JobEditorDialog(QDialog): """Dialog for creating/editing jobs""" def __init__(self, parent, db: SchedulerDatabase, job_id: int = None): super().__init__(parent) self.db = db self.job_id = job_id self.is_edit_mode = job_id is not None self.setWindowTitle("Edit Job" if self.is_edit_mode else "New Job") self.setModal(True) self.setMinimumWidth(600) self._setup_ui() if self.is_edit_mode: self._load_job_data() def _setup_ui(self): """Setup dialog UI""" layout = QVBoxLayout() # Basic Info Group basic_group = QGroupBox("Basic Information") basic_layout = QFormLayout() self.name_input = QLineEdit() self.name_input.setPlaceholderText("e.g., DataHub Daily Refresh") basic_layout.addRow("Job Name:*", self.name_input) self.description_input = QTextEdit() self.description_input.setMaximumHeight(60) self.description_input.setPlaceholderText("Description of what this job does...") basic_layout.addRow("Description:", self.description_input) basic_group.setLayout(basic_layout) layout.addWidget(basic_group) # Execution Group exec_group = QGroupBox("Execution Settings") exec_layout = QFormLayout() self.job_type_combo = QComboBox() self.job_type_combo.addItems(["batch", "script", "python_module"]) exec_layout.addRow("Job Type:*", self.job_type_combo) # Executable path with file picker exec_path_layout = QHBoxLayout() self.executable_input = QLineEdit() self.executable_input.setPlaceholderText("C:\\path\\to\\script.bat") exec_path_layout.addWidget(self.executable_input) self.browse_button = QPushButton("Browse...") self.browse_button.clicked.connect(self._browse_executable) exec_path_layout.addWidget(self.browse_button) exec_layout.addRow("Executable:*", exec_path_layout) self.arguments_input = QLineEdit() self.arguments_input.setPlaceholderText("Optional command line arguments") exec_layout.addRow("Arguments:", self.arguments_input) # Working directory with folder picker workdir_layout = QHBoxLayout() self.working_dir_input = QLineEdit() self.working_dir_input.setPlaceholderText("C:\\path\\to\\working\\directory") workdir_layout.addWidget(self.working_dir_input) self.browse_dir_button = QPushButton("Browse...") self.browse_dir_button.clicked.connect(self._browse_directory) workdir_layout.addWidget(self.browse_dir_button) exec_layout.addRow("Working Dir:", workdir_layout) exec_group.setLayout(exec_layout) layout.addWidget(exec_group) # Schedule Group schedule_group = QGroupBox("Schedule Settings") schedule_layout = QFormLayout() self.schedule_type_combo = QComboBox() self.schedule_type_combo.addItems(["cron", "interval", "manual"]) self.schedule_type_combo.currentTextChanged.connect(self._on_schedule_type_changed) schedule_layout.addRow("Schedule Type:*", self.schedule_type_combo) # Cron fields self.cron_input = QLineEdit() self.cron_input.setPlaceholderText("e.g., 0 7 * * mon-fri (7am weekdays)") schedule_layout.addRow("Cron Expression:", self.cron_input) # Cron help label self.cron_help = QLabel( 'Cron Help | ' 'Format: minute hour day month weekday' ) self.cron_help.setOpenExternalLinks(True) self.cron_help.setStyleSheet("color: gray; font-size: 10pt;") schedule_layout.addRow("", self.cron_help) # Interval fields self.interval_weeks = QSpinBox() self.interval_weeks.setMinimum(0) self.interval_weeks.setMaximum(52) schedule_layout.addRow("Weeks:", self.interval_weeks) self.interval_days = QSpinBox() self.interval_days.setMinimum(0) self.interval_days.setMaximum(365) schedule_layout.addRow("Days:", self.interval_days) self.interval_hours = QSpinBox() self.interval_hours.setMinimum(0) self.interval_hours.setMaximum(23) schedule_layout.addRow("Hours:", self.interval_hours) self.interval_minutes = QSpinBox() self.interval_minutes.setMinimum(0) self.interval_minutes.setMaximum(59) schedule_layout.addRow("Minutes:", self.interval_minutes) self.interval_seconds = QSpinBox() self.interval_seconds.setMinimum(0) self.interval_seconds.setMaximum(59) schedule_layout.addRow("Seconds:", self.interval_seconds) # Hide interval fields initially self.interval_weeks.setVisible(False) self.interval_days.setVisible(False) self.interval_hours.setVisible(False) self.interval_minutes.setVisible(False) self.interval_seconds.setVisible(False) schedule_group.setLayout(schedule_layout) layout.addWidget(schedule_group) # Advanced Settings Group advanced_group = QGroupBox("Advanced Settings") advanced_layout = QFormLayout() self.enabled_checkbox = QCheckBox("Job is enabled") self.enabled_checkbox.setChecked(True) advanced_layout.addRow("", self.enabled_checkbox) self.show_console_checkbox = QCheckBox("Show Console Window (Debug Mode)") self.show_console_checkbox.setToolTip( "Display a visible CMD window during job execution.\n" "Useful for debugging. Output is still logged to file." ) advanced_layout.addRow("", self.show_console_checkbox) self.show_on_dashboard_checkbox = QCheckBox("Mostra in Dashboard") self.show_on_dashboard_checkbox.setChecked(True) self.show_on_dashboard_checkbox.setToolTip( "Se deselezionato, questo job non compare nella Dashboard\n" "(né in Recent Executions né nella Weekly Timeline).\n" "Utile per job frequenti/di servizio che non richiedono monitoraggio visivo." ) advanced_layout.addRow("", self.show_on_dashboard_checkbox) self.timeout_input = QSpinBox() self.timeout_input.setRange(0, 86400) # 0 to 24 hours self.timeout_input.setValue(3600) self.timeout_input.setSuffix(" seconds") advanced_layout.addRow("Timeout:", self.timeout_input) self.retry_count_input = QSpinBox() self.retry_count_input.setRange(0, 10) self.retry_count_input.setValue(0) advanced_layout.addRow("Retry Count:", self.retry_count_input) self.retry_delay_input = QSpinBox() self.retry_delay_input.setRange(0, 3600) self.retry_delay_input.setValue(60) self.retry_delay_input.setSuffix(" seconds") advanced_layout.addRow("Retry Delay:", self.retry_delay_input) self.criticality_combo = QComboBox() self.criticality_combo.addItems(["normal", "important", "critical"]) self.criticality_combo.setCurrentText("normal") advanced_layout.addRow("Criticality:", self.criticality_combo) self.tags_input = QLineEdit() self.tags_input.setPlaceholderText("production, daily, datahub (comma-separated)") advanced_layout.addRow("Tags:", self.tags_input) self.hostname_input = QLineEdit() self.hostname_input.setPlaceholderText("lascia vuoto per 'tutti i PC'") self.hostname_input.setText(socket.gethostname()) self.hostname_input.setToolTip( "Hostname del PC su cui gira questo job.\n" "Lascia vuoto per eseguire su tutti gli host.\n" "Default: hostname del PC corrente." ) advanced_layout.addRow("Host:", self.hostname_input) advanced_group.setLayout(advanced_layout) layout.addWidget(advanced_group) # Buttons button_layout = QHBoxLayout() button_layout.addStretch() if self.is_edit_mode: self.test_button = QPushButton("Test Run") self.test_button.clicked.connect(self._test_run) button_layout.addWidget(self.test_button) self.save_button = QPushButton("Save" if self.is_edit_mode else "Create") self.save_button.setDefault(True) self.save_button.clicked.connect(self._save_job) button_layout.addWidget(self.save_button) self.cancel_button = QPushButton("Cancel") self.cancel_button.clicked.connect(self.reject) button_layout.addWidget(self.cancel_button) layout.addLayout(button_layout) self.setLayout(layout) def _on_schedule_type_changed(self, schedule_type: str): """Handle schedule type change""" # Show/hide fields based on schedule type is_cron = schedule_type == "cron" is_interval = schedule_type == "interval" # Cron fields visibility self.cron_input.setVisible(is_cron) self.cron_help.setVisible(is_cron) # Interval fields visibility self.interval_weeks.setVisible(is_interval) self.interval_days.setVisible(is_interval) self.interval_hours.setVisible(is_interval) self.interval_minutes.setVisible(is_interval) self.interval_seconds.setVisible(is_interval) # Update placeholder for manual if schedule_type == "manual": self.cron_input.setPlaceholderText("Manual execution only") else: self.cron_input.setPlaceholderText("e.g., 0 7 * * mon-fri") def _browse_executable(self): """Open file picker for executable""" file_path, _ = QFileDialog.getOpenFileName( self, "Select Executable", "", "All Files (*.bat *.cmd *.py *.exe *.vbs);;Batch Files (*.bat *.cmd);;Python Files (*.py);;VBScript Files (*.vbs);;Executables (*.exe)" ) if file_path: self.executable_input.setText(file_path) # Auto-detect working directory if not set if not self.working_dir_input.text(): self.working_dir_input.setText(str(Path(file_path).parent)) def _browse_directory(self): """Open folder picker for working directory""" dir_path = QFileDialog.getExistingDirectory( self, "Select Working Directory", "" ) if dir_path: self.working_dir_input.setText(dir_path) def _load_job_data(self): """Load existing job data for editing""" try: job = self.db.get_job(self.job_id) if not job: QMessageBox.warning(self, "Error", f"Job ID {self.job_id} not found") self.reject() return self.name_input.setText(job['name']) self.description_input.setPlainText(job['description'] or "") self.job_type_combo.setCurrentText(job['job_type']) self.executable_input.setText(job['executable_path']) self.arguments_input.setText(job['arguments'] or "") self.working_dir_input.setText(job['working_directory'] or "") self.schedule_type_combo.setCurrentText(job['schedule_type']) schedule_config = json.loads(job['schedule_config']) if job['schedule_type'] == 'cron': self.cron_input.setText(schedule_config.get('cron', '')) elif job['schedule_type'] == 'interval': self.interval_weeks.setValue(schedule_config.get('weeks', 0)) self.interval_days.setValue(schedule_config.get('days', 0)) self.interval_hours.setValue(schedule_config.get('hours', 0)) self.interval_minutes.setValue(schedule_config.get('minutes', 0)) self.interval_seconds.setValue(schedule_config.get('seconds', 0)) self.enabled_checkbox.setChecked(bool(job['enabled'])) self.timeout_input.setValue(job['timeout_seconds']) self.retry_count_input.setValue(job['retry_count']) self.retry_delay_input.setValue(job['retry_delay_seconds']) self.criticality_combo.setCurrentText(job['criticality']) tags = json.loads(job['tags']) if job['tags'] else [] self.tags_input.setText(", ".join(tags)) self.show_console_checkbox.setChecked(job.get('show_console', False)) self.show_on_dashboard_checkbox.setChecked(bool(job.get('show_on_dashboard', True))) self.hostname_input.setText(job.get('hostname', '') or '') logger.info(f"Loaded job {self.job_id} for editing") except Exception as e: logger.error(f"Error loading job data: {e}") QMessageBox.critical(self, "Error", f"Failed to load job: {e}") self.reject() def _validate_inputs(self) -> bool: """Validate form inputs""" if not self.name_input.text().strip(): QMessageBox.warning(self, "Validation Error", "Job name is required") self.name_input.setFocus() return False if not self.executable_input.text().strip(): QMessageBox.warning(self, "Validation Error", "Executable path is required") self.executable_input.setFocus() return False if self.schedule_type_combo.currentText() == 'cron': if not self.cron_input.text().strip(): QMessageBox.warning(self, "Validation Error", "Cron expression is required for cron schedule type") self.cron_input.setFocus() return False return True def _save_job(self): """Save job to database""" if not self._validate_inputs(): return try: # Prepare schedule config schedule_type = self.schedule_type_combo.currentText() schedule_config = {} if schedule_type == 'cron': schedule_config['cron'] = self.cron_input.text().strip() elif schedule_type == 'interval': schedule_config['weeks'] = self.interval_weeks.value() schedule_config['days'] = self.interval_days.value() schedule_config['hours'] = self.interval_hours.value() schedule_config['minutes'] = self.interval_minutes.value() schedule_config['seconds'] = self.interval_seconds.value() # Parse tags tags_text = self.tags_input.text().strip() tags = [t.strip() for t in tags_text.split(',') if t.strip()] if tags_text else [] # Prepare job data job_data = { 'name': self.name_input.text().strip(), 'description': self.description_input.toPlainText().strip() or None, 'job_type': self.job_type_combo.currentText(), 'executable_path': self.executable_input.text().strip(), 'arguments': self.arguments_input.text().strip() or None, 'working_directory': self.working_dir_input.text().strip() or None, 'schedule_type': schedule_type, 'schedule_config': schedule_config, 'enabled': self.enabled_checkbox.isChecked(), 'timeout_seconds': self.timeout_input.value(), 'retry_count': self.retry_count_input.value(), 'retry_delay_seconds': self.retry_delay_input.value(), 'criticality': self.criticality_combo.currentText(), 'tags': tags, 'show_console': self.show_console_checkbox.isChecked(), 'show_on_dashboard': self.show_on_dashboard_checkbox.isChecked(), 'hostname': self.hostname_input.text().strip() } if self.is_edit_mode: # Update existing job self.db.update_job(self.job_id, **job_data) logger.info(f"Updated job {self.job_id}: {job_data['name']}") QMessageBox.information(self, "Success", f"Job '{job_data['name']}' updated successfully") else: # Create new job job_id = self.db.create_job(**job_data) logger.info(f"Created job {job_id}: {job_data['name']}") QMessageBox.information(self, "Success", f"Job '{job_data['name']}' created successfully (ID: {job_id})") self.accept() except Exception as e: logger.error(f"Error saving job: {e}") QMessageBox.critical(self, "Error", f"Failed to save job: {e}") def _test_run(self): """Execute test run of the job""" if not self.job_id: return reply = QMessageBox.question( self, "Test Run", f"Execute job '{self.name_input.text()}' now?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No ) if reply == QMessageBox.StandardButton.Yes: try: from daemon.job_executor import JobExecutor from core.config import SchedulerConfig config = SchedulerConfig() executor = JobExecutor(self.db, config) execution_id = executor.execute_job(self.job_id, triggered_by='manual_test') QMessageBox.information( self, "Test Run Started", f"Job execution started (ID: {execution_id})\n\nCheck History tab for results." ) logger.info(f"Test run started for job {self.job_id}, execution {execution_id}") except Exception as e: logger.error(f"Error starting test run: {e}") QMessageBox.critical(self, "Error", f"Failed to start test run: {e}")