"""APIClient - Agent - Mock Server Panel.""" from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QTableWidget, QTableWidgetItem, QHeaderView, QDialog, QFormLayout, QLineEdit, QComboBox, QTextEdit, QSpinBox, QDialogButtonBox, QMessageBox ) from PyQt6.QtCore import Qt from PyQt6.QtGui import QBrush, QColor from app.ui.theme import Colors, restyle from app.core import mock_server, storage from app.models import MockEndpoint class EndpointDialog(QDialog): def __init__(self, ep: MockEndpoint = None, parent=None): super().__init__(parent) self.setWindowTitle("Mock Endpoint") self.setMinimumWidth(480) self.ep = MockEndpoint() if ep is None else MockEndpoint( id=ep.id, name=ep.name, method=ep.method, path=ep.path, status_code=ep.status_code, response_headers=ep.response_headers, response_body=ep.response_body ) layout = QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) header = QWidget() header.setObjectName("panelHeader") header.setFixedHeight(44) hl = QHBoxLayout(header) hl.setContentsMargins(16, 0, 16, 0) title = QLabel("Configure Mock Endpoint") title.setObjectName("panelTitle") hl.addWidget(title) layout.addWidget(header) body = QWidget() bl = QVBoxLayout(body) form = QFormLayout() form.setContentsMargins(16, 12, 16, 12) form.setSpacing(10) self.name_input = QLineEdit(self.ep.name or "") self.name_input.setPlaceholderText("Optional display name") self.method_combo = QComboBox() self.method_combo.addItems(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "*"]) self.method_combo.setCurrentText(self.ep.method or "GET") self.path_input = QLineEdit(self.ep.path or "/") self.path_input.setPlaceholderText("/api/v1/resource") self.status_spin = QSpinBox() self.status_spin.setRange(100, 599) self.status_spin.setValue(self.ep.status_code or 200) self.body_editor = QTextEdit() self.body_editor.setPlaceholderText('{"message": "ok"}') self.body_editor.setPlainText(self.ep.response_body or "") self.body_editor.setMaximumHeight(140) form.addRow("Name:", self.name_input) form.addRow("Method:", self.method_combo) form.addRow("Path:", self.path_input) form.addRow("Status Code:", self.status_spin) form.addRow("Response Body:", self.body_editor) bl.addLayout(form) layout.addWidget(body, 1) footer = QWidget() footer.setObjectName("panelFooter") fl = QVBoxLayout(footer) fl.setContentsMargins(12, 8, 12, 8) btns = QDialogButtonBox( QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel ) btns.accepted.connect(self._save) btns.rejected.connect(self.reject) fl.addWidget(btns) layout.addWidget(footer) def _save(self): path = self.path_input.text().strip() if not path: QMessageBox.warning(self, "Validation", "Path is required.") return if not path.startswith("/"): path = "/" + path self.ep.name = self.name_input.text().strip() self.ep.method = self.method_combo.currentText() self.ep.path = path self.ep.status_code = self.status_spin.value() self.ep.response_body = self.body_editor.toPlainText() self.accept() class MockServerPanel(QWidget): def __init__(self, parent=None): super().__init__(parent) layout = QVBoxLayout(self) layout.setContentsMargins(12, 12, 12, 12) layout.setSpacing(8) # ── Server controls ─────────────────────────────────────────────────── top = QHBoxLayout() port_label = QLabel("Port:") port_label.setObjectName("fieldLabel") self.port_input = QLineEdit("8888") self.port_input.setFixedWidth(70) self.port_input.setToolTip("Listening port for the mock server") self.toggle_btn = QPushButton("Start Server") self.toggle_btn.setObjectName("accent") self.toggle_btn.setFixedWidth(120) self.toggle_btn.clicked.connect(self._toggle_server) self.status_label = QLabel("● Stopped") self.status_label.setObjectName("statusErr") top.addWidget(port_label) top.addWidget(self.port_input) top.addWidget(self.toggle_btn) top.addStretch() top.addWidget(self.status_label) layout.addLayout(top) # ── Endpoint table ──────────────────────────────────────────────────── self.table = QTableWidget(0, 4) self.table.setHorizontalHeaderLabels(["Name", "Method", "Path", "Status"]) self.table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) self.table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents) self.table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch) self.table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents) self.table.verticalHeader().setVisible(False) self.table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) self.table.doubleClicked.connect(self._edit_endpoint) layout.addWidget(self.table) # ── Action buttons ──────────────────────────────────────────────────── btn_row = QHBoxLayout() add_btn = QPushButton("+ Add Endpoint") add_btn.clicked.connect(self._add_endpoint) del_btn = QPushButton("Delete") del_btn.setObjectName("danger") del_btn.clicked.connect(self._delete_endpoint) btn_row.addWidget(add_btn) btn_row.addWidget(del_btn) btn_row.addStretch() layout.addLayout(btn_row) self._load_endpoints() # ── Data loading ────────────────────────────────────────────────────────── def _load_endpoints(self): self.table.setRowCount(0) for ep in storage.get_mock_endpoints(): row = self.table.rowCount() self.table.insertRow(row) self.table.setItem(row, 0, QTableWidgetItem(ep.name or "")) method_item = QTableWidgetItem(ep.method) method_item.setForeground(QBrush(QColor(Colors.INFO))) self.table.setItem(row, 1, method_item) self.table.setItem(row, 2, QTableWidgetItem(ep.path)) self.table.setItem(row, 3, QTableWidgetItem(str(ep.status_code))) self.table.item(row, 0).setData(Qt.ItemDataRole.UserRole, ep) # ── Actions ─────────────────────────────────────────────────────────────── def _add_endpoint(self): dlg = EndpointDialog(parent=self) if dlg.exec(): storage.save_mock_endpoint(dlg.ep) self._load_endpoints() def _edit_endpoint(self): row = self.table.currentRow() if row < 0: return item = self.table.item(row, 0) if not item: return ep = item.data(Qt.ItemDataRole.UserRole) dlg = EndpointDialog(ep=ep, parent=self) if dlg.exec(): storage.save_mock_endpoint(dlg.ep) self._load_endpoints() def _delete_endpoint(self): row = self.table.currentRow() if row < 0: return item = self.table.item(row, 0) if not item: return ep = item.data(Qt.ItemDataRole.UserRole) if ep and ep.id: storage.delete_mock_endpoint(ep.id) self._load_endpoints() def _toggle_server(self): if mock_server.is_running(): mock_server.stop() self.toggle_btn.setText("Start Server") restyle(self.status_label, "statusErr") self.status_label.setText("● Stopped") else: try: port = int(self.port_input.text()) except ValueError: port = 8888 msg = mock_server.start(port) if mock_server.is_running(): self.toggle_btn.setText("Stop Server") restyle(self.status_label, "statusOk") self.status_label.setText(f"● Running on :{port}") else: QMessageBox.warning(self, "Mock Server", msg)