feat(silo): dock auth panel, SSE live updates, and improved pull workflow
Some checks failed
Build and Test / build (push) Has been cancelled

- Add _setup_silo_auth_panel() to dock auth widget in right panel at startup
- Update silo submodule: SSE listener, revision pull dialog, conflict detection
This commit is contained in:
forbes
2026-02-01 16:26:15 -06:00
parent 1056ef1b99
commit 1fea7c3d2e
2 changed files with 43 additions and 303 deletions

View File

@@ -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)
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.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"<h3>{pn}</h3>"
msg += f"<p><b>Type:</b> {item_data.get('item_type', '-')}</p>"
msg += f"<p><b>Description:</b> {item_data.get('description', '-')}</p>"
msg += f"<p><b>Projects:</b> {', '.join(project_codes) if project_codes else 'None'}</p>"
msg += f"<p><b>Current Revision:</b> {item_data.get('current_revision', 1)}</p>"
has_file, _ = self._client.has_file(pn)
msg += f"<p><b>File in MinIO:</b> {'Yes' if has_file else 'No'}</p>"
if revisions:
current_status = revisions[0].get("status", "draft")
msg += f"<p><b>Current Status:</b> {current_status}</p>"
msg += "<h4>Revision History</h4><table border='1' cellpadding='4'>"
msg += "<tr><th>Rev</th><th>Status</th><th>Date</th><th>File</th><th>Comment</th></tr>"
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"<tr><td>{rev['revision_number']}</td><td>{status}</td><td>{date}</td><td>{file_icon}</td><td>{comment}</td></tr>"
msg += "</table>"
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")
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