From 626790904daa9f7ee875738be30977aa0b44cc0c Mon Sep 17 00:00:00 2001 From: forbes Date: Sat, 31 Jan 2026 20:55:20 -0600 Subject: [PATCH] feat(silo): add interactive database browser to activity panel Replace the static QListWidget with a full SiloActivityPanel class that provides: - Search field with 300ms debounce for filtering items by name - Type filter dropdown (All / Part / Assembly) - Refresh button for manual reload - QTableWidget with Part Number, Description, Type, and Updated columns - Part Details pane that appears on row selection showing part number, description, type, revision, last updated date, and project tags - Open button / double-click to open items via SiloSync.open_item() - Info button showing revision history dialog (reuses Silo_Info pattern) - Graceful error handling for connection failures and empty results --- src/Mod/Create/InitGui.py | 298 +++++++++++++++++++++++++++++++++++--- 1 file changed, 274 insertions(+), 24 deletions(-) diff --git a/src/Mod/Create/InitGui.py b/src/Mod/Create/InitGui.py index 90cbc47ce6..0caf77f0ab 100644 --- a/src/Mod/Create/InitGui.py +++ b/src/Mod/Create/InitGui.py @@ -95,8 +95,278 @@ 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 __init__(self): + from PySide import QtCore, QtGui, QtWidgets + + self._QtCore = QtCore + self._QtWidgets = QtWidgets + + import silo_commands + + self._client = silo_commands._client + self._SiloSync = silo_commands.SiloSync + + 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") + + def _setup_silo_activity_panel(): - """Show a dock widget with recent Silo database activity.""" + """Show a dock widget with the Silo database browser.""" try: from PySide import QtCore, QtWidgets @@ -111,30 +381,10 @@ def _setup_silo_activity_panel(): panel = QtWidgets.QDockWidget("Database Activity", mw) panel.setObjectName("SiloDatabaseActivity") - widget = QtWidgets.QWidget() - layout = QtWidgets.QVBoxLayout(widget) + activity_widget = SiloActivityPanel() + panel.setWidget(activity_widget.widget) + panel._activity_widget = activity_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")