This commit is contained in:
@@ -0,0 +1,329 @@
|
||||
"""Connectors (MCP / REST API) management — the setup UI.
|
||||
|
||||
Lives in Monitoring → Tools → "Connector" sub-tab (moved out of Settings). A
|
||||
tree of the four categories (CAD / CAE / MS365 / Other); each connector has an
|
||||
Enabled checkbox and can be added / edited / deleted (MCP-stdio or REST-API,
|
||||
via ExtConnectorEditDialog). Built-in connectors (MS365 OneDrive / SharePoint,
|
||||
and Jira under "Other") appear as rows in the tree with their checkbox bound to
|
||||
config; double-clicking one opens its setup — nothing spills outside the tree.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import (
|
||||
QCheckBox, QDialog, QFormLayout, QHBoxLayout, QLabel, QLineEdit, QMessageBox,
|
||||
QPushButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget,
|
||||
)
|
||||
|
||||
from ..core.ext_connectors import CATEGORIES as EXT_CATEGORIES
|
||||
from ..core.worker import AgentWorker
|
||||
from ..i18n import on_language_changed, tr
|
||||
from ..state import AppContext
|
||||
from .ext_connector_dialog import ExtConnectorEditDialog
|
||||
from .icons import icon
|
||||
|
||||
|
||||
class JiraConnectDialog(QDialog):
|
||||
"""Minimal Jira connect — paste any Jira link (it fills the base URL) + email
|
||||
+ API token. Once connected, pasting a Jira link into Cowork / Co4E chat is
|
||||
read and processed automatically (no per-request setup)."""
|
||||
|
||||
def __init__(self, ctx: AppContext, parent=None):
|
||||
super().__init__(parent)
|
||||
self.ctx = ctx
|
||||
self.setWindowTitle(tr("connectors.jira_group"))
|
||||
self.setMinimumWidth(460)
|
||||
jira = ctx.config.data.get("jira", {})
|
||||
form = QFormLayout(self)
|
||||
|
||||
hint = QLabel(tr("connectors.jira_hint"))
|
||||
hint.setObjectName("hint"); hint.setWordWrap(True); hint.setOpenExternalLinks(True)
|
||||
form.addRow(hint)
|
||||
self.paste = QLineEdit()
|
||||
self.paste.setPlaceholderText(tr("connectors.jira_paste_placeholder"))
|
||||
self.paste.textChanged.connect(self._on_paste)
|
||||
form.addRow(tr("connectors.jira_paste"), self.paste)
|
||||
self.url = QLineEdit(jira.get("base_url", ""))
|
||||
self.url.setPlaceholderText("https://your-domain.atlassian.net")
|
||||
self.email = QLineEdit(jira.get("email", ""))
|
||||
self.token = QLineEdit(jira.get("api_token", ""))
|
||||
self.token.setEchoMode(QLineEdit.Password)
|
||||
form.addRow(tr("connectors.jira_url"), self.url)
|
||||
form.addRow(tr("connectors.jira_email"), self.email)
|
||||
form.addRow(tr("connectors.jira_token"), self.token)
|
||||
self.status = QLabel(); self.status.setObjectName("hint"); self.status.setWordWrap(True)
|
||||
form.addRow(self.status)
|
||||
|
||||
row = QHBoxLayout()
|
||||
self.test_btn = QPushButton(tr("connectors.jira_test"))
|
||||
self.test_btn.clicked.connect(self._test)
|
||||
self.save_btn = QPushButton(tr("connectors.jira_save"))
|
||||
self.save_btn.setObjectName("primary"); self.save_btn.setIcon(icon("save"))
|
||||
self.save_btn.clicked.connect(self._save_close)
|
||||
row.addWidget(self.test_btn); row.addStretch(1); row.addWidget(self.save_btn)
|
||||
rw = QWidget(); rw.setLayout(row)
|
||||
form.addRow(rw)
|
||||
|
||||
def _on_paste(self, text: str) -> None:
|
||||
from ..core import jira_tool
|
||||
base = jira_tool.base_url_from_link(text)
|
||||
if base:
|
||||
self.url.setText(base)
|
||||
|
||||
def _save(self) -> None:
|
||||
j = self.ctx.config.data.setdefault("jira", {})
|
||||
j.update({"base_url": self.url.text().strip(), "email": self.email.text().strip(),
|
||||
"api_token": self.token.text().strip()})
|
||||
j.setdefault("enabled", True)
|
||||
self.ctx.save()
|
||||
|
||||
def _save_close(self) -> None:
|
||||
self._save()
|
||||
self.accept()
|
||||
|
||||
def _test(self) -> None:
|
||||
from ..core import jira_tool
|
||||
self._save()
|
||||
cfg = self.ctx.config.data.get("jira", {})
|
||||
if not jira_tool.configured(cfg):
|
||||
self.status.setText(tr("connectors.jira_need_fields"))
|
||||
return
|
||||
self.status.setText(tr("connectors.jira_testing"))
|
||||
self.test_btn.setEnabled(False)
|
||||
|
||||
def job(_w):
|
||||
return {"out": jira_tool.search(cfg, "order by created DESC", 1)}
|
||||
|
||||
def done(r):
|
||||
self.test_btn.setEnabled(True)
|
||||
out = r.get("out", "")
|
||||
ok = not out.lower().startswith(("jira is not configured", "jira search failed"))
|
||||
self.status.setText(tr("connectors.jira_ok") if ok
|
||||
else tr("connectors.jira_fail", err=out[:200]))
|
||||
|
||||
w = AgentWorker(job)
|
||||
w.finished_ok.connect(done)
|
||||
w.failed.connect(lambda e: (self.test_btn.setEnabled(True),
|
||||
self.status.setText(tr("connectors.jira_fail", err=str(e)[:200]))))
|
||||
self._jira_worker = w
|
||||
w.start()
|
||||
|
||||
|
||||
class ConnectorsPanel(QWidget):
|
||||
_EXT_CATEGORY_LABELS = {
|
||||
"cad": "CAD (NX / CATIA / SolidWorks / AutoCAD)",
|
||||
"cae": "CAE (ANSA / ABAQUS / HyperWorks / ANSYS)",
|
||||
"ms365": "MS365 (Microsoft 365 / OneDrive / SharePoint)",
|
||||
"other": "Other (any generic MCP server)",
|
||||
}
|
||||
_EXT_CATEGORY_ICONS = {"cad": "wrench", "cae": "ruler", "ms365": "cloud", "other": "plug"}
|
||||
# TEMPORARY: only OneDrive + SharePoint (auto-connect via locally-synced
|
||||
# OneDrive folders, no sign-in). Restore Outlook/Teams/Meeting when cloud
|
||||
# OAuth is re-enabled.
|
||||
_MS365_BUILTIN_LABELS = {"onedrive": "OneDrive", "sharepoint": "SharePoint"}
|
||||
|
||||
def __init__(self, ctx: AppContext):
|
||||
super().__init__()
|
||||
self.ctx = ctx
|
||||
lay = QVBoxLayout(self)
|
||||
|
||||
# Master switch: connect to external connectors at all (default ON).
|
||||
# Off = the agent connects to NO external connector/MCP (see
|
||||
# AppContext.build_mcp_tools), regardless of the per-connector checks below.
|
||||
self.connect_external_chk = QCheckBox(tr("connectors.connect_external"))
|
||||
self.connect_external_chk.setChecked(self.ctx.config.connect_external)
|
||||
self.connect_external_chk.setToolTip(tr("connectors.connect_external_tooltip"))
|
||||
self.connect_external_chk.toggled.connect(self._on_connect_external_toggled)
|
||||
lay.addWidget(self.connect_external_chk)
|
||||
|
||||
hint = QLabel(tr("settings.ext_hint"))
|
||||
hint.setObjectName("hint")
|
||||
hint.setWordWrap(True)
|
||||
lay.addWidget(hint)
|
||||
|
||||
self.ext_tree = QTreeWidget()
|
||||
self.ext_tree.setHeaderHidden(True)
|
||||
self.ext_tree.itemChanged.connect(self._on_ext_check)
|
||||
self.ext_tree.itemDoubleClicked.connect(lambda *_: self._ext_edit())
|
||||
lay.addWidget(self.ext_tree, 1)
|
||||
|
||||
self.dbl_hint = QLabel(tr("connectors.dbl_configure"))
|
||||
self.dbl_hint.setObjectName("hint")
|
||||
lay.addWidget(self.dbl_hint)
|
||||
|
||||
row = QHBoxLayout()
|
||||
self.add_btn = QPushButton(tr("settings.ext_add_btn"))
|
||||
self.add_btn.setIcon(icon("plus"))
|
||||
self.add_btn.setObjectName("primary")
|
||||
self.add_btn.clicked.connect(self._ext_add)
|
||||
self.edit_btn = QPushButton(tr("settings.ext_edit_btn"))
|
||||
self.edit_btn.setIcon(icon("edit"))
|
||||
self.edit_btn.clicked.connect(self._ext_edit)
|
||||
self.del_btn = QPushButton(tr("settings.ext_delete_btn"))
|
||||
self.del_btn.setIcon(icon("trash"))
|
||||
self.del_btn.clicked.connect(self._ext_delete)
|
||||
row.addWidget(self.add_btn)
|
||||
row.addWidget(self.edit_btn)
|
||||
row.addWidget(self.del_btn)
|
||||
row.addStretch(1)
|
||||
lay.addLayout(row)
|
||||
|
||||
self.ms365_local_status = QLabel()
|
||||
self.ms365_local_status.setObjectName("hint")
|
||||
self.ms365_local_status.setWordWrap(True)
|
||||
lay.addWidget(self.ms365_local_status)
|
||||
|
||||
self._refresh_ms365_local_status()
|
||||
self._reload_ext_tree()
|
||||
on_language_changed(self._retranslate)
|
||||
|
||||
# ---- rendering ------------------------------------------------------------
|
||||
def _reload_ext_tree(self) -> None:
|
||||
self.ext_tree.blockSignals(True)
|
||||
self.ext_tree.clear()
|
||||
ext = self.ctx.config.ext_connectors
|
||||
for cat in EXT_CATEGORIES:
|
||||
cat_item = QTreeWidgetItem([self._EXT_CATEGORY_LABELS.get(cat, cat)])
|
||||
cat_item.setIcon(0, icon(self._EXT_CATEGORY_ICONS.get(cat, "plug")))
|
||||
cat_item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
|
||||
cat_item.setData(0, Qt.UserRole, ("category", cat))
|
||||
self.ext_tree.addTopLevelItem(cat_item)
|
||||
if cat == "ms365":
|
||||
conns = self.ctx.config.ms365.get("connectors", {})
|
||||
for key, label in self._MS365_BUILTIN_LABELS.items():
|
||||
b = QTreeWidgetItem([f"{label} — {tr('ext.mode_builtin')}"])
|
||||
b.setFlags(b.flags() | Qt.ItemIsUserCheckable)
|
||||
b.setCheckState(0, Qt.Checked if conns.get(key) else Qt.Unchecked)
|
||||
b.setData(0, Qt.UserRole, ("ms365_builtin", "ms365", key))
|
||||
cat_item.addChild(b)
|
||||
for idx, entry in enumerate(ext.get(cat, [])):
|
||||
mode_label = tr("ext.mode_mcp") if entry.get("mode") == "mcp_stdio" else tr("ext.mode_rest")
|
||||
child = QTreeWidgetItem([f"{entry.get('name', '')} — {mode_label}"])
|
||||
child.setFlags(child.flags() | Qt.ItemIsUserCheckable)
|
||||
child.setCheckState(0, Qt.Checked if entry.get("enabled") else Qt.Unchecked)
|
||||
child.setData(0, Qt.UserRole, ("connector", cat, idx))
|
||||
cat_item.addChild(child)
|
||||
if cat == "other":
|
||||
# Jira is a built-in "Other" connector (like OneDrive under MS365):
|
||||
# checkbox = enabled; double-click opens its minimal setup dialog.
|
||||
jira = self.ctx.config.data.get("jira", {})
|
||||
configured = bool(jira.get("base_url") and jira.get("email")
|
||||
and jira.get("api_token"))
|
||||
jstate = tr("connectors.jira_connected") if configured else tr("connectors.jira_not_set")
|
||||
jrow = QTreeWidgetItem([f"Jira — {tr('ext.mode_builtin')} · {jstate}"])
|
||||
jrow.setIcon(0, icon("link"))
|
||||
jrow.setFlags(jrow.flags() | Qt.ItemIsUserCheckable)
|
||||
on = configured and jira.get("enabled", True)
|
||||
jrow.setCheckState(0, Qt.Checked if on else Qt.Unchecked)
|
||||
jrow.setToolTip(0, tr("connectors.jira_setup_hint"))
|
||||
jrow.setData(0, Qt.UserRole, ("jira_builtin", "other"))
|
||||
cat_item.addChild(jrow)
|
||||
cat_item.setExpanded(True)
|
||||
self.ext_tree.blockSignals(False)
|
||||
|
||||
def _on_ext_check(self, item: QTreeWidgetItem, _col: int) -> None:
|
||||
data = item.data(0, Qt.UserRole)
|
||||
if data and data[0] == "ms365_builtin":
|
||||
self.ctx.config.ms365.setdefault("connectors", {})[data[2]] = (
|
||||
item.checkState(0) == Qt.Checked)
|
||||
self.ctx.save()
|
||||
return
|
||||
if data and data[0] == "jira_builtin":
|
||||
self.ctx.config.data.setdefault("jira", {})["enabled"] = (
|
||||
item.checkState(0) == Qt.Checked)
|
||||
self.ctx.save()
|
||||
return
|
||||
if not data or data[0] != "connector":
|
||||
return
|
||||
_, cat, idx = data
|
||||
entries = self.ctx.config.ext_connectors.get(cat, [])
|
||||
if 0 <= idx < len(entries):
|
||||
entries[idx]["enabled"] = item.checkState(0) == Qt.Checked
|
||||
self.ctx.save()
|
||||
|
||||
def _current_ext_category(self) -> str:
|
||||
item = self.ext_tree.currentItem()
|
||||
data = item.data(0, Qt.UserRole) if item else None
|
||||
return data[1] if data else EXT_CATEGORIES[0]
|
||||
|
||||
def _current_ext_connector(self):
|
||||
item = self.ext_tree.currentItem()
|
||||
data = item.data(0, Qt.UserRole) if item else None
|
||||
if not data or data[0] != "connector":
|
||||
return None
|
||||
_, cat, idx = data
|
||||
entries = self.ctx.config.ext_connectors.get(cat, [])
|
||||
return (cat, entries[idx]) if 0 <= idx < len(entries) else None
|
||||
|
||||
# ---- CRUD -----------------------------------------------------------------
|
||||
def _ext_add(self) -> None:
|
||||
dlg = ExtConnectorEditDialog(self, category=self._current_ext_category())
|
||||
if dlg.exec():
|
||||
entry = dlg.result_connector()
|
||||
self.ctx.config.ext_connectors.setdefault(entry["category"], []).append(entry)
|
||||
self.ctx.save()
|
||||
self._reload_ext_tree()
|
||||
|
||||
def _ext_edit(self) -> None:
|
||||
"""Configure the selected row. Built-in Jira → its minimal dialog;
|
||||
a normal connector → the MCP/REST editor."""
|
||||
item = self.ext_tree.currentItem()
|
||||
data = item.data(0, Qt.UserRole) if item else None
|
||||
if data and data[0] == "jira_builtin":
|
||||
self._open_jira_dialog()
|
||||
return
|
||||
current = self._current_ext_connector()
|
||||
if current is None:
|
||||
return
|
||||
cat, entry = current
|
||||
dlg = ExtConnectorEditDialog(self, category=cat, connector=entry)
|
||||
if dlg.exec():
|
||||
entry.update(dlg.result_connector())
|
||||
self.ctx.save()
|
||||
self._reload_ext_tree()
|
||||
|
||||
def _open_jira_dialog(self) -> None:
|
||||
JiraConnectDialog(self.ctx, self).exec()
|
||||
self._reload_ext_tree()
|
||||
|
||||
def _ext_delete(self) -> None:
|
||||
current = self._current_ext_connector()
|
||||
if current is None:
|
||||
return
|
||||
cat, entry = current
|
||||
if QMessageBox.question(
|
||||
self, tr("settings.ext_delete_btn"),
|
||||
tr("settings.ext_delete_confirm", name=entry.get("name", ""))) != QMessageBox.Yes:
|
||||
return
|
||||
self.ctx.config.ext_connectors[cat].remove(entry)
|
||||
self.ctx.save()
|
||||
self._reload_ext_tree()
|
||||
|
||||
def _refresh_ms365_local_status(self) -> None:
|
||||
from .. import paths
|
||||
root = paths.primary_onedrive_root()
|
||||
if root is not None:
|
||||
self.ms365_local_status.setText(tr("settings.ms365_local_connected", path=str(root)))
|
||||
else:
|
||||
self.ms365_local_status.setText(tr("settings.ms365_local_none"))
|
||||
|
||||
def _on_connect_external_toggled(self, on: bool) -> None:
|
||||
self.ctx.config.set_connect_external(on)
|
||||
self._apply_connect_external_enabled(on)
|
||||
|
||||
def _apply_connect_external_enabled(self, on: bool) -> None:
|
||||
"""Grey out the per-connector setup when the master switch is off — the
|
||||
agent won't connect to any of them anyway."""
|
||||
for w in (self.ext_tree, self.add_btn, self.edit_btn, self.del_btn):
|
||||
w.setEnabled(on)
|
||||
|
||||
def _retranslate(self) -> None:
|
||||
self.connect_external_chk.setText(tr("connectors.connect_external"))
|
||||
self.connect_external_chk.setToolTip(tr("connectors.connect_external_tooltip"))
|
||||
self.add_btn.setText(tr("settings.ext_add_btn"))
|
||||
self.edit_btn.setText(tr("settings.ext_edit_btn"))
|
||||
self.del_btn.setText(tr("settings.ext_delete_btn"))
|
||||
self.dbl_hint.setText(tr("connectors.dbl_configure"))
|
||||
self._refresh_ms365_local_status()
|
||||
self._reload_ext_tree()
|
||||
self._apply_connect_external_enabled(self.ctx.config.connect_external)
|
||||
Reference in New Issue
Block a user