Initial release — APIClient - Agent v2.0.0
AI-first API testing desktop client built with Python + PyQt6. Features: - Multi-tab HTTP request editor with params/headers/body/auth/tests - KeyValueTable with per-row enable/disable checkboxes and 36px rows - Format JSON button, syntax highlighting, pre-request & test scripts - Collections, environments, history, import/export (Postman v2.1, cURL) - OpenAPI 3.x / Swagger 2.0 local parser (no AI tokens) - EKIKA Odoo API Framework generator — JSON-API, REST JSON, GraphQL, Custom REST JSON with all auth types (instant, no AI tokens) - Persistent AI chat sidebar (Claude-powered co-pilot) with streaming, context-aware suggestions, and one-click Apply to request editor - AI collection generator from any docs URL or pasted spec - WebSocket client, Mock server, Collection runner, Code generator - Dark/light theme engine (global QSS, object-name selectors) - SSL error detection with actionable hints - MIT License Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
238
app/ui/environment_dialog.py
Normal file
238
app/ui/environment_dialog.py
Normal file
@@ -0,0 +1,238 @@
|
||||
"""APIClient - Agent — Environment Manager Dialog."""
|
||||
from PyQt6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QListWidget, QListWidgetItem,
|
||||
QPushButton, QTableWidget, QTableWidgetItem, QHeaderView,
|
||||
QLabel, QInputDialog, QMessageBox, QSplitter, QWidget
|
||||
)
|
||||
from PyQt6.QtCore import Qt
|
||||
from PyQt6.QtGui import QBrush, QColor
|
||||
|
||||
from app.ui.theme import Colors
|
||||
from app.core import storage
|
||||
from app.models import Environment
|
||||
|
||||
|
||||
class EnvironmentDialog(QDialog):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Manage Environments")
|
||||
self.setMinimumSize(760, 520)
|
||||
self._current_env: Environment | None = None
|
||||
self._dirty = False
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
|
||||
# ── Title bar ─────────────────────────────────────────────────────────
|
||||
title_bar = QWidget()
|
||||
title_bar.setObjectName("panelHeader")
|
||||
title_bar.setFixedHeight(48)
|
||||
tl = QHBoxLayout(title_bar)
|
||||
tl.setContentsMargins(16, 0, 16, 0)
|
||||
title = QLabel("Manage Environments")
|
||||
title.setObjectName("panelTitle")
|
||||
tl.addWidget(title)
|
||||
layout.addWidget(title_bar)
|
||||
|
||||
# ── Splitter ──────────────────────────────────────────────────────────
|
||||
splitter = QSplitter(Qt.Orientation.Horizontal)
|
||||
splitter.setHandleWidth(1)
|
||||
|
||||
# Left: environment list
|
||||
left = QWidget()
|
||||
left.setObjectName("sidebarPanel")
|
||||
left.setFixedWidth(220)
|
||||
ll = QVBoxLayout(left)
|
||||
ll.setContentsMargins(0, 0, 0, 0)
|
||||
ll.setSpacing(0)
|
||||
|
||||
list_header = QWidget()
|
||||
list_header.setObjectName("sectionHeader")
|
||||
list_header.setFixedHeight(36)
|
||||
lh = QHBoxLayout(list_header)
|
||||
lh.setContentsMargins(12, 0, 8, 0)
|
||||
env_heading = QLabel("ENVIRONMENTS")
|
||||
env_heading.setObjectName("sectionLabel")
|
||||
lh.addWidget(env_heading)
|
||||
lh.addStretch()
|
||||
add_env_btn = QPushButton("+")
|
||||
add_env_btn.setObjectName("ghost")
|
||||
add_env_btn.setFixedSize(26, 26)
|
||||
add_env_btn.setToolTip("Add Environment")
|
||||
add_env_btn.clicked.connect(self._add_env)
|
||||
lh.addWidget(add_env_btn)
|
||||
ll.addWidget(list_header)
|
||||
|
||||
self.env_list = QListWidget()
|
||||
self.env_list.setObjectName("sidebarList")
|
||||
self.env_list.currentItemChanged.connect(self._on_env_selected)
|
||||
ll.addWidget(self.env_list)
|
||||
|
||||
btn_row = QHBoxLayout()
|
||||
btn_row.setContentsMargins(8, 6, 8, 6)
|
||||
btn_row.setSpacing(6)
|
||||
self.activate_btn = QPushButton("Set Active")
|
||||
self.activate_btn.clicked.connect(self._set_active)
|
||||
self.del_btn = QPushButton("Delete")
|
||||
self.del_btn.setObjectName("danger")
|
||||
self.del_btn.clicked.connect(self._delete_env)
|
||||
btn_row.addWidget(self.activate_btn)
|
||||
btn_row.addWidget(self.del_btn)
|
||||
ll.addLayout(btn_row)
|
||||
splitter.addWidget(left)
|
||||
|
||||
# Right: variable table
|
||||
right = QWidget()
|
||||
right.setObjectName("panelBody")
|
||||
rl = QVBoxLayout(right)
|
||||
rl.setContentsMargins(0, 0, 0, 0)
|
||||
rl.setSpacing(0)
|
||||
|
||||
var_header = QWidget()
|
||||
var_header.setObjectName("panelHeader")
|
||||
var_header.setFixedHeight(36)
|
||||
vh = QHBoxLayout(var_header)
|
||||
vh.setContentsMargins(16, 0, 12, 0)
|
||||
var_label = QLabel("Variables")
|
||||
var_label.setObjectName("fieldLabel")
|
||||
vh.addWidget(var_label)
|
||||
vh.addStretch()
|
||||
add_var_btn = QPushButton("+ Add Variable")
|
||||
add_var_btn.setObjectName("ghost")
|
||||
add_var_btn.clicked.connect(self._add_var_row)
|
||||
vh.addWidget(add_var_btn)
|
||||
rl.addWidget(var_header)
|
||||
|
||||
self.var_table = QTableWidget(0, 2)
|
||||
self.var_table.setHorizontalHeaderLabels(["Variable", "Value"])
|
||||
self.var_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
|
||||
self.var_table.verticalHeader().setVisible(False)
|
||||
self.var_table.setAlternatingRowColors(True)
|
||||
self.var_table.itemChanged.connect(self._on_var_changed)
|
||||
rl.addWidget(self.var_table, 1)
|
||||
splitter.addWidget(right)
|
||||
splitter.setSizes([220, 520])
|
||||
|
||||
layout.addWidget(splitter, 1)
|
||||
|
||||
# ── Bottom bar ────────────────────────────────────────────────────────
|
||||
bottom = QWidget()
|
||||
bottom.setObjectName("panelFooter")
|
||||
bottom.setFixedHeight(52)
|
||||
bl = QHBoxLayout(bottom)
|
||||
bl.setContentsMargins(16, 0, 16, 0)
|
||||
bl.addStretch()
|
||||
save_btn = QPushButton("Save & Close")
|
||||
save_btn.setObjectName("accent")
|
||||
save_btn.setFixedWidth(120)
|
||||
save_btn.clicked.connect(self._save_and_close)
|
||||
bl.addWidget(save_btn)
|
||||
layout.addWidget(bottom)
|
||||
|
||||
self._load_envs()
|
||||
|
||||
# ── Data loading ──────────────────────────────────────────────────────────
|
||||
|
||||
def _load_envs(self):
|
||||
self.env_list.clear()
|
||||
for env in storage.get_environments():
|
||||
label = f"{'● ' if env.is_active else ' '} {env.name}"
|
||||
item = QListWidgetItem(label)
|
||||
item.setData(Qt.ItemDataRole.UserRole, env)
|
||||
if env.is_active:
|
||||
item.setForeground(QBrush(QColor(Colors.ACCENT)))
|
||||
self.env_list.addItem(item)
|
||||
|
||||
def _on_env_selected(self, current, _previous):
|
||||
if self._current_env and self._dirty:
|
||||
self._current_env.variables = self._get_vars()
|
||||
|
||||
if not current:
|
||||
return
|
||||
self._current_env = current.data(Qt.ItemDataRole.UserRole)
|
||||
self._dirty = False
|
||||
self._load_vars(self._current_env.variables)
|
||||
|
||||
def _load_vars(self, variables: dict):
|
||||
self.var_table.blockSignals(True)
|
||||
self.var_table.setRowCount(0)
|
||||
for k, v in variables.items():
|
||||
row = self.var_table.rowCount()
|
||||
self.var_table.insertRow(row)
|
||||
self.var_table.setItem(row, 0, QTableWidgetItem(k))
|
||||
self.var_table.setItem(row, 1, QTableWidgetItem(str(v)))
|
||||
self.var_table.blockSignals(False)
|
||||
|
||||
def _on_var_changed(self):
|
||||
if self._current_env:
|
||||
self._dirty = True
|
||||
|
||||
def _get_vars(self) -> dict:
|
||||
result = {}
|
||||
for row in range(self.var_table.rowCount()):
|
||||
k = self.var_table.item(row, 0)
|
||||
v = self.var_table.item(row, 1)
|
||||
if k and k.text().strip():
|
||||
result[k.text().strip()] = v.text() if v else ""
|
||||
return result
|
||||
|
||||
# ── Actions ───────────────────────────────────────────────────────────────
|
||||
|
||||
def _add_var_row(self):
|
||||
if not self._current_env:
|
||||
QMessageBox.information(
|
||||
self, "No Environment Selected",
|
||||
"Select or create an environment first."
|
||||
)
|
||||
return
|
||||
row = self.var_table.rowCount()
|
||||
self.var_table.insertRow(row)
|
||||
self.var_table.setItem(row, 0, QTableWidgetItem(""))
|
||||
self.var_table.setItem(row, 1, QTableWidgetItem(""))
|
||||
self.var_table.editItem(self.var_table.item(row, 0))
|
||||
|
||||
def _add_env(self):
|
||||
name, ok = QInputDialog.getText(self, "New Environment", "Name:")
|
||||
if not ok or not name.strip():
|
||||
return
|
||||
env = Environment(name=name.strip())
|
||||
env_id = storage.save_environment(env)
|
||||
env.id = env_id
|
||||
self._load_envs()
|
||||
for i in range(self.env_list.count()):
|
||||
item = self.env_list.item(i)
|
||||
if item.data(Qt.ItemDataRole.UserRole).id == env_id:
|
||||
self.env_list.setCurrentItem(item)
|
||||
break
|
||||
|
||||
def _delete_env(self):
|
||||
item = self.env_list.currentItem()
|
||||
if not item:
|
||||
return
|
||||
env = item.data(Qt.ItemDataRole.UserRole)
|
||||
reply = QMessageBox.question(
|
||||
self, "Delete Environment",
|
||||
f"Delete '{env.name}'? This cannot be undone.",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel
|
||||
)
|
||||
if reply == QMessageBox.StandardButton.Yes:
|
||||
storage.delete_environment(env.id)
|
||||
self._current_env = None
|
||||
self._dirty = False
|
||||
self.var_table.setRowCount(0)
|
||||
self._load_envs()
|
||||
|
||||
def _set_active(self):
|
||||
item = self.env_list.currentItem()
|
||||
if not item:
|
||||
return
|
||||
env = item.data(Qt.ItemDataRole.UserRole)
|
||||
storage.set_active_environment(env.id)
|
||||
self._load_envs()
|
||||
|
||||
def _save_and_close(self):
|
||||
if self._current_env:
|
||||
self._current_env.variables = self._get_vars()
|
||||
storage.save_environment(self._current_env)
|
||||
self.accept()
|
||||
Reference in New Issue
Block a user