""" Group Editor Dialog for Python Scheduler Create and edit job groups (chains) """ from PyQt6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, QLabel, QLineEdit, QTextEdit, QComboBox, QPushButton, QMessageBox, QListWidget, QListWidgetItem, QAbstractItemView, QGroupBox ) from PyQt6.QtCore import Qt from loguru import logger from core.database import SchedulerDatabase from core.config import SchedulerConfig class GroupEditorDialog(QDialog): """Dialog for creating/editing job groups""" def __init__(self, parent, db: SchedulerDatabase, config: SchedulerConfig, group_id: int = None): super().__init__(parent) self.db = db self.config = config self.group_id = group_id self.is_edit_mode = group_id is not None # Window setup self.setWindowTitle("Edit Group" if self.is_edit_mode else "New Group") self.setMinimumSize(600, 500) self.setModal(True) self._setup_ui() if self.is_edit_mode: self._load_group_data() def _setup_ui(self): """Setup dialog UI""" layout = QVBoxLayout(self) # === Basic Information === basic_group = QGroupBox("Basic Information") basic_layout = QFormLayout() self.name_input = QLineEdit() self.name_input.setPlaceholderText("e.g., Daily ETL Pipeline") basic_layout.addRow("Group Name:", self.name_input) self.description_input = QTextEdit() self.description_input.setPlaceholderText("Optional description of what this group does...") self.description_input.setMaximumHeight(80) basic_layout.addRow("Description:", self.description_input) basic_group.setLayout(basic_layout) layout.addWidget(basic_group) # === Error Handling === error_group = QGroupBox("Error Handling") error_layout = QFormLayout() self.error_handling_combo = QComboBox() self.error_handling_combo.addItems([ "stop_on_error", "continue_on_error", "skip_remaining" ]) self.error_handling_combo.setCurrentText("stop_on_error") self.error_handling_combo.currentTextChanged.connect(self._update_error_help) error_layout.addRow("Error Handling:", self.error_handling_combo) self.error_help_label = QLabel() self.error_help_label.setWordWrap(True) self.error_help_label.setStyleSheet("color: #666; font-size: 11px; margin-top: 5px;") error_layout.addRow("", self.error_help_label) self._update_error_help() error_group.setLayout(error_layout) layout.addWidget(error_group) # === Job Members === members_group = QGroupBox("Jobs in Group (Execution Order)") members_layout = QVBoxLayout() # Available jobs list available_label = QLabel("Available Jobs:") members_layout.addWidget(available_label) self.available_jobs_list = QListWidget() self.available_jobs_list.setSelectionMode(QAbstractItemView.SelectionMode.MultiSelection) self.available_jobs_list.setMaximumHeight(120) members_layout.addWidget(self.available_jobs_list) # Buttons to add/remove buttons_layout = QHBoxLayout() self.add_jobs_btn = QPushButton("Add Selected ↓") self.add_jobs_btn.clicked.connect(self._add_selected_jobs) buttons_layout.addWidget(self.add_jobs_btn) self.remove_jobs_btn = QPushButton("Remove Selected ↑") self.remove_jobs_btn.clicked.connect(self._remove_selected_jobs) buttons_layout.addWidget(self.remove_jobs_btn) buttons_layout.addStretch() members_layout.addLayout(buttons_layout) # Selected jobs list (execution order) selected_label = QLabel("Jobs in Group (top to bottom = execution order):") members_layout.addWidget(selected_label) self.selected_jobs_list = QListWidget() self.selected_jobs_list.setMaximumHeight(120) members_layout.addWidget(self.selected_jobs_list) # Move up/down buttons order_layout = QHBoxLayout() self.move_up_btn = QPushButton("Move Up ↑") self.move_up_btn.clicked.connect(self._move_job_up) order_layout.addWidget(self.move_up_btn) self.move_down_btn = QPushButton("Move Down ↓") self.move_down_btn.clicked.connect(self._move_job_down) order_layout.addWidget(self.move_down_btn) order_layout.addStretch() members_layout.addLayout(order_layout) members_group.setLayout(members_layout) layout.addWidget(members_group) # === Action Buttons === actions_layout = QHBoxLayout() actions_layout.addStretch() self.save_btn = QPushButton("Save" if self.is_edit_mode else "Create") self.save_btn.setStyleSheet("background-color: #4CAF50; color: white; padding: 8px 20px; font-weight: bold;") self.save_btn.clicked.connect(self._save_group) actions_layout.addWidget(self.save_btn) cancel_btn = QPushButton("Cancel") cancel_btn.clicked.connect(self.reject) actions_layout.addWidget(cancel_btn) layout.addLayout(actions_layout) # Load available jobs self._load_available_jobs() def _update_error_help(self): """Update error handling help text""" error_type = self.error_handling_combo.currentText() help_texts = { "stop_on_error": "Stop the entire group execution if any job fails (safest)", "continue_on_error": "Continue executing remaining jobs even if one fails", "skip_remaining": "Skip remaining jobs but mark group execution as failed" } self.error_help_label.setText(help_texts.get(error_type, "")) def _load_available_jobs(self): """Load all available jobs""" try: all_jobs = self.db.get_all_jobs() for job in all_jobs: item = QListWidgetItem(f"{job['name']} (ID: {job['id']})") item.setData(Qt.ItemDataRole.UserRole, job['id']) self.available_jobs_list.addItem(item) except Exception as e: logger.error(f"Error loading available jobs: {e}") QMessageBox.warning(self, "Error", f"Failed to load jobs: {e}") def _load_group_data(self): """Load existing group data""" try: group = self.db.get_job_group(self.group_id) if not group: QMessageBox.warning(self, "Error", f"Group {self.group_id} not found") self.reject() return # Load basic info self.name_input.setText(group['name']) if group['description']: self.description_input.setPlainText(group['description']) self.error_handling_combo.setCurrentText(group['error_handling']) # Load members members = self.db.get_job_group_members(self.group_id) for member in members: item = QListWidgetItem(f"{member['job_name']} (ID: {member['job_id']})") item.setData(Qt.ItemDataRole.UserRole, member['job_id']) self.selected_jobs_list.addItem(item) # Remove from available list for i in range(self.available_jobs_list.count()): available_item = self.available_jobs_list.item(i) if available_item.data(Qt.ItemDataRole.UserRole) == member['job_id']: self.available_jobs_list.takeItem(i) break except Exception as e: logger.error(f"Error loading group data: {e}") QMessageBox.critical(self, "Error", f"Failed to load group: {e}") self.reject() def _add_selected_jobs(self): """Add selected jobs from available to selected list""" selected_items = self.available_jobs_list.selectedItems() if not selected_items: return for item in selected_items: # Add to selected list new_item = QListWidgetItem(item.text()) new_item.setData(Qt.ItemDataRole.UserRole, item.data(Qt.ItemDataRole.UserRole)) self.selected_jobs_list.addItem(new_item) # Remove from available list row = self.available_jobs_list.row(item) self.available_jobs_list.takeItem(row) def _remove_selected_jobs(self): """Remove selected jobs from selected list back to available""" selected_items = self.selected_jobs_list.selectedItems() if not selected_items: return for item in selected_items: # Add back to available list new_item = QListWidgetItem(item.text()) new_item.setData(Qt.ItemDataRole.UserRole, item.data(Qt.ItemDataRole.UserRole)) self.available_jobs_list.addItem(new_item) # Remove from selected list row = self.selected_jobs_list.row(item) self.selected_jobs_list.takeItem(row) def _move_job_up(self): """Move selected job up in execution order""" current_row = self.selected_jobs_list.currentRow() if current_row > 0: item = self.selected_jobs_list.takeItem(current_row) self.selected_jobs_list.insertItem(current_row - 1, item) self.selected_jobs_list.setCurrentRow(current_row - 1) def _move_job_down(self): """Move selected job down in execution order""" current_row = self.selected_jobs_list.currentRow() if current_row < self.selected_jobs_list.count() - 1 and current_row >= 0: item = self.selected_jobs_list.takeItem(current_row) self.selected_jobs_list.insertItem(current_row + 1, item) self.selected_jobs_list.setCurrentRow(current_row + 1) def _validate_inputs(self) -> bool: """Validate form inputs""" if not self.name_input.text().strip(): QMessageBox.warning(self, "Validation Error", "Group name is required") self.name_input.setFocus() return False if self.selected_jobs_list.count() == 0: QMessageBox.warning(self, "Validation Error", "Please add at least one job to the group") return False return True def _save_group(self): """Save group to database""" if not self._validate_inputs(): return try: group_data = { 'name': self.name_input.text().strip(), 'description': self.description_input.toPlainText().strip() or None, 'error_handling': self.error_handling_combo.currentText() } if self.is_edit_mode: # Update existing group self.db.update_job_group(self.group_id, **group_data) # Clear existing members self.db.clear_group_members(self.group_id) # Add new members for i in range(self.selected_jobs_list.count()): item = self.selected_jobs_list.item(i) job_id = item.data(Qt.ItemDataRole.UserRole) self.db.add_job_to_group(self.group_id, job_id, execution_order=i+1) logger.info(f"Updated group {self.group_id}: {group_data['name']}") QMessageBox.information(self, "Success", f"Group '{group_data['name']}' updated successfully") else: # Create new group group_id = self.db.create_job_group(**group_data) # Add members for i in range(self.selected_jobs_list.count()): item = self.selected_jobs_list.item(i) job_id = item.data(Qt.ItemDataRole.UserRole) self.db.add_job_to_group(group_id, job_id, execution_order=i+1) logger.info(f"Created group {group_id}: {group_data['name']} with {self.selected_jobs_list.count()} jobs") QMessageBox.information(self, "Success", f"Group '{group_data['name']}' created successfully (ID: {group_id})") self.accept() except Exception as e: logger.error(f"Error saving group: {e}") QMessageBox.critical(self, "Error", f"Failed to save group: {e}")