From 1fea7c3d2ecf046cd81e0a333f3059b807e526db Mon Sep 17 00:00:00 2001 From: forbes Date: Sun, 1 Feb 2026 16:26:15 -0600 Subject: [PATCH] feat(silo): dock auth panel, SSE live updates, and improved pull workflow - Add _setup_silo_auth_panel() to dock auth widget in right panel at startup - Update silo submodule: SSE listener, revision pull dialog, conflict detection --- mods/silo | 2 +- src/Mod/Create/InitGui.py | 344 +++++--------------------------------- 2 files changed, 43 insertions(+), 303 deletions(-) diff --git a/mods/silo b/mods/silo index 3a67d2082b..17a10ab1b6 160000 --- a/mods/silo +++ b/mods/silo @@ -1 +1 @@ -Subproject commit 3a67d2082b93173195e073ffc974c22e3db6a370 +Subproject commit 17a10ab1b68d13227d2b90e92163353f68e7fdc1 diff --git a/src/Mod/Create/InitGui.py b/src/Mod/Create/InitGui.py index 0caf77f0ab..c9d0016c6b 100644 --- a/src/Mod/Create/InitGui.py +++ b/src/Mod/Create/InitGui.py @@ -95,278 +95,33 @@ def _setup_silo_menu(): FreeCAD.Console.PrintLog(f"Create: Silo menu setup skipped: {e}\n") -class SiloActivityPanel: - """Interactive database browser with search, item table, and details pane.""" +def _setup_silo_auth_panel(): + """Dock the Silo authentication panel in the right-hand side panel.""" + try: + from PySide import QtCore, QtWidgets - def __init__(self): - from PySide import QtCore, QtGui, QtWidgets + mw = FreeCADGui.getMainWindow() + if mw is None: + return - self._QtCore = QtCore - self._QtWidgets = QtWidgets + # Don't create duplicate panels + if mw.findChild(QtWidgets.QDockWidget, "SiloDatabaseAuth"): + return import silo_commands - self._client = silo_commands._client - self._SiloSync = silo_commands.SiloSync + auth = silo_commands.SiloAuthDockWidget() - self.widget = QtWidgets.QWidget() - self._items_data = [] - self._debounce_timer = QtCore.QTimer() - self._debounce_timer.setSingleShot(True) - self._debounce_timer.setInterval(300) - self._debounce_timer.timeout.connect(self._do_refresh) - - self._build_ui() - self._do_refresh() - - def _build_ui(self): - QtWidgets = self._QtWidgets - QtCore = self._QtCore - - layout = QtWidgets.QVBoxLayout(self.widget) - layout.setContentsMargins(4, 4, 4, 4) - layout.setSpacing(4) - - # --- Search / filter toolbar --- - toolbar = QtWidgets.QHBoxLayout() - toolbar.setSpacing(4) - - self._search = QtWidgets.QLineEdit() - self._search.setPlaceholderText("Search parts...") - self._search.setClearButtonEnabled(True) - self._search.textChanged.connect(self._on_search_changed) - toolbar.addWidget(self._search, 1) - - self._type_filter = QtWidgets.QComboBox() - self._type_filter.addItem("All", "") - self._type_filter.addItem("Part", "part") - self._type_filter.addItem("Assembly", "assembly") - self._type_filter.setFixedWidth(90) - self._type_filter.currentIndexChanged.connect(self._on_search_changed) - toolbar.addWidget(self._type_filter) - - refresh_btn = QtWidgets.QPushButton("Refresh") - refresh_btn.setFixedWidth(60) - refresh_btn.clicked.connect(self._do_refresh) - toolbar.addWidget(refresh_btn) - - layout.addLayout(toolbar) - - # --- Item table --- - self._table = QtWidgets.QTableWidget() - self._table.setColumnCount(4) - self._table.setHorizontalHeaderLabels( - ["Part Number", "Description", "Type", "Updated"] - ) - header = self._table.horizontalHeader() - header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents) - header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch) - header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents) - header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents) - self._table.verticalHeader().setVisible(False) - self._table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) - self._table.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection) - self._table.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) - self._table.itemSelectionChanged.connect(self._on_selection_changed) - self._table.doubleClicked.connect(self._on_open) - layout.addWidget(self._table, 1) - - # --- Details pane --- - self._details_group = QtWidgets.QGroupBox("Part Details") - details_layout = QtWidgets.QVBoxLayout(self._details_group) - details_layout.setSpacing(2) - - form = QtWidgets.QFormLayout() - form.setHorizontalSpacing(12) - self._detail_pn = QtWidgets.QLabel("-") - self._detail_pn.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse) - self._detail_desc = QtWidgets.QLabel("-") - self._detail_desc.setWordWrap(True) - self._detail_desc.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse) - self._detail_type = QtWidgets.QLabel("-") - self._detail_rev = QtWidgets.QLabel("-") - self._detail_updated = QtWidgets.QLabel("-") - self._detail_projects = QtWidgets.QLabel("-") - self._detail_projects.setWordWrap(True) - form.addRow("Part Number:", self._detail_pn) - form.addRow("Description:", self._detail_desc) - form.addRow("Type:", self._detail_type) - form.addRow("Revision:", self._detail_rev) - form.addRow("Updated:", self._detail_updated) - form.addRow("Projects:", self._detail_projects) - details_layout.addLayout(form) - - btn_row = QtWidgets.QHBoxLayout() - self._open_btn = QtWidgets.QPushButton("Open") - self._open_btn.clicked.connect(self._on_open) - self._info_btn = QtWidgets.QPushButton("Info...") - self._info_btn.clicked.connect(self._on_info) - btn_row.addWidget(self._open_btn) - btn_row.addStretch() - btn_row.addWidget(self._info_btn) - details_layout.addLayout(btn_row) - - self._details_group.setVisible(False) - layout.addWidget(self._details_group) - - # --- Actions --- - - def _on_search_changed(self): - self._debounce_timer.start() - - def _do_refresh(self): - search = self._search.text().strip() - item_type = self._type_filter.currentData() or "" - try: - items = self._client.list_items(search=search, item_type=item_type) - if not isinstance(items, list): - items = [] - except Exception: - items = None - - self._items_data = items if items is not None else [] - self._populate_table(items) - - def _populate_table(self, items): - QtWidgets = self._QtWidgets - self._table.setRowCount(0) - self._details_group.setVisible(False) - - if items is None: - self._table.setRowCount(1) - self._table.setSpan(0, 0, 1, 4) - msg = QtWidgets.QTableWidgetItem("(Unable to connect to Silo database)") - msg.setForeground( - self._table.palette().color( - self._table.palette().Disabled, self._table.palette().Text - ) - ) - self._table.setItem(0, 0, msg) - return - - if not items: - self._table.setRowCount(1) - self._table.setSpan(0, 0, 1, 4) - msg = QtWidgets.QTableWidgetItem("(No items found)") - msg.setForeground( - self._table.palette().color( - self._table.palette().Disabled, self._table.palette().Text - ) - ) - self._table.setItem(0, 0, msg) - return - - self._table.setRowCount(len(items)) - for i, item in enumerate(items): - pn = item.get("part_number", "") - desc = item.get("description", "") - itype = item.get("item_type", "") - updated = item.get("updated_at", "")[:10] if item.get("updated_at") else "" - - self._table.setItem(i, 0, QtWidgets.QTableWidgetItem(pn)) - self._table.setItem(i, 1, QtWidgets.QTableWidgetItem(desc)) - self._table.setItem(i, 2, QtWidgets.QTableWidgetItem(itype)) - self._table.setItem(i, 3, QtWidgets.QTableWidgetItem(updated)) - - def _selected_item(self): - rows = self._table.selectionModel().selectedRows() - if not rows: - return None - row = rows[0].row() - if row < len(self._items_data): - return self._items_data[row] - return None - - def _on_selection_changed(self): - item = self._selected_item() - if not item: - self._details_group.setVisible(False) - return - - self._detail_pn.setText(item.get("part_number", "-")) - self._detail_desc.setText(item.get("description", "-") or "-") - self._detail_type.setText(item.get("item_type", "-")) - self._detail_rev.setText(str(item.get("current_revision", "-"))) - updated = item.get("updated_at", "") - self._detail_updated.setText(updated[:10] if updated else "-") - - # Fetch projects in background-safe way (quick API call) - pn = item.get("part_number", "") - try: - projects = self._client.get_item_projects(pn) - codes = [p.get("code", "") for p in projects if p.get("code")] - self._detail_projects.setText(", ".join(codes) if codes else "-") - except Exception: - self._detail_projects.setText("-") - - self._details_group.setVisible(True) - - def _on_open(self): - item = self._selected_item() - if not item: - return - pn = item.get("part_number", "") - if not pn: - return - try: - sync = self._SiloSync(self._client) - sync.open_item(pn) - except Exception as e: - FreeCAD.Console.PrintError(f"Failed to open {pn}: {e}\n") - - def _on_info(self): - item = self._selected_item() - if not item: - return - pn = item.get("part_number", "") - if not pn: - return - try: - from PySide import QtGui - - item_data = self._client.get_item(pn) - revisions = self._client.get_revisions(pn) - - try: - projects = self._client.get_item_projects(pn) - project_codes = [p.get("code", "") for p in projects if p.get("code")] - except Exception: - project_codes = [] - - msg = f"

{pn}

" - msg += f"

Type: {item_data.get('item_type', '-')}

" - msg += f"

Description: {item_data.get('description', '-')}

" - msg += f"

Projects: {', '.join(project_codes) if project_codes else 'None'}

" - msg += f"

Current Revision: {item_data.get('current_revision', 1)}

" - - has_file, _ = self._client.has_file(pn) - msg += f"

File in MinIO: {'Yes' if has_file else 'No'}

" - - if revisions: - current_status = revisions[0].get("status", "draft") - msg += f"

Current Status: {current_status}

" - - msg += "

Revision History

" - msg += "" - for rev in revisions: - file_icon = "Y" if rev.get("file_key") else "-" - comment = rev.get("comment", "") or "-" - date = rev.get("created_at", "")[:10] - status = rev.get("status", "draft") - msg += f"" - msg += "
RevStatusDateFileComment
{rev['revision_number']}{status}{date}{file_icon}{comment}
" - - dialog = QtGui.QMessageBox() - dialog.setWindowTitle("Item Info") - dialog.setTextFormat(QtGui.Qt.RichText) - dialog.setText(msg) - dialog.exec_() - except Exception as e: - FreeCAD.Console.PrintError(f"Failed to get info for {pn}: {e}\n") + panel = QtWidgets.QDockWidget("Database Auth", mw) + panel.setObjectName("SiloDatabaseAuth") + panel.setWidget(auth.widget) + mw.addDockWidget(QtCore.Qt.RightDockWidgetArea, panel) + except Exception as e: + FreeCAD.Console.PrintLog(f"Create: Silo auth panel skipped: {e}\n") def _setup_silo_activity_panel(): - """Show a dock widget with the Silo database browser.""" + """Show a dock widget with recent Silo database activity.""" try: from PySide import QtCore, QtWidgets @@ -381,57 +136,42 @@ def _setup_silo_activity_panel(): panel = QtWidgets.QDockWidget("Database Activity", mw) panel.setObjectName("SiloDatabaseActivity") - activity_widget = SiloActivityPanel() - panel.setWidget(activity_widget.widget) - panel._activity_widget = activity_widget + widget = QtWidgets.QWidget() + layout = QtWidgets.QVBoxLayout(widget) + activity_list = QtWidgets.QListWidget() + layout.addWidget(activity_list) + + try: + import silo_commands + + items = silo_commands._client.list_items() + if isinstance(items, list): + for item in items[:20]: + pn = item.get("part_number", "") + desc = item.get("description", "") + updated = item.get("updated_at", "") + if updated: + updated = updated[:10] + activity_list.addItem(f"{pn} - {desc} - {updated}") + if activity_list.count() == 0: + activity_list.addItem("(No items in database)") + except Exception: + activity_list.addItem("(Unable to connect to Silo database)") + + panel.setWidget(widget) mw.addDockWidget(QtCore.Qt.RightDockWidgetArea, panel) except Exception as e: FreeCAD.Console.PrintLog(f"Create: Silo activity panel skipped: {e}\n") -def _setup_silo_auth_panel(): - """Show a dock widget with Silo authentication status and login.""" - try: - from PySide import QtCore, QtWidgets - - mw = FreeCADGui.getMainWindow() - if mw is None: - return - - # Don't create duplicate panels - if mw.findChild(QtWidgets.QDockWidget, "SiloDatabaseAuth"): - return - - panel = QtWidgets.QDockWidget("Database Auth", mw) - panel.setObjectName("SiloDatabaseAuth") - - import silo_commands - - auth_widget = silo_commands.SiloAuthDockWidget() - panel.setWidget(auth_widget.widget) - # Keep a reference so the timer and callbacks are not garbage-collected - panel._auth_widget = auth_widget - - mw.addDockWidget(QtCore.Qt.RightDockWidgetArea, panel) - - # Tabify with the activity panel so they share the same dock area - activity_panel = mw.findChild(QtWidgets.QDockWidget, "SiloDatabaseActivity") - if activity_panel: - mw.tabifyDockWidget(activity_panel, panel) - panel.raise_() - - except Exception as e: - FreeCAD.Console.PrintLog(f"Create: Silo auth panel skipped: {e}\n") - - # Defer enhancements until the GUI event loop is running try: from PySide.QtCore import QTimer + QTimer.singleShot(1500, _setup_silo_auth_panel) QTimer.singleShot(2000, _setup_silo_menu) QTimer.singleShot(3000, _check_silo_first_start) QTimer.singleShot(4000, _setup_silo_activity_panel) - QTimer.singleShot(4500, _setup_silo_auth_panel) except Exception: pass