223 lines
9.0 KiB
Python
223 lines
9.0 KiB
Python
"""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)
|