feat(silo): dock auth panel, SSE live updates, and improved pull workflow
Some checks failed
Build and Test / build (push) Has been cancelled
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:
Submodule mods/silo updated: 3a67d2082b...17a10ab1b6
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user