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")
|
FreeCAD.Console.PrintLog(f"Create: Silo menu setup skipped: {e}\n")
|
||||||
|
|
||||||
|
|
||||||
class SiloActivityPanel:
|
def _setup_silo_auth_panel():
|
||||||
"""Interactive database browser with search, item table, and details pane."""
|
"""Dock the Silo authentication panel in the right-hand side panel."""
|
||||||
|
try:
|
||||||
|
from PySide import QtCore, QtWidgets
|
||||||
|
|
||||||
def __init__(self):
|
mw = FreeCADGui.getMainWindow()
|
||||||
from PySide import QtCore, QtGui, QtWidgets
|
if mw is None:
|
||||||
|
return
|
||||||
|
|
||||||
self._QtCore = QtCore
|
# Don't create duplicate panels
|
||||||
self._QtWidgets = QtWidgets
|
if mw.findChild(QtWidgets.QDockWidget, "SiloDatabaseAuth"):
|
||||||
|
return
|
||||||
|
|
||||||
import silo_commands
|
import silo_commands
|
||||||
|
|
||||||
self._client = silo_commands._client
|
auth = silo_commands.SiloAuthDockWidget()
|
||||||
self._SiloSync = silo_commands.SiloSync
|
|
||||||
|
|
||||||
self.widget = QtWidgets.QWidget()
|
panel = QtWidgets.QDockWidget("Database Auth", mw)
|
||||||
self._items_data = []
|
panel.setObjectName("SiloDatabaseAuth")
|
||||||
self._debounce_timer = QtCore.QTimer()
|
panel.setWidget(auth.widget)
|
||||||
self._debounce_timer.setSingleShot(True)
|
mw.addDockWidget(QtCore.Qt.RightDockWidgetArea, panel)
|
||||||
self._debounce_timer.setInterval(300)
|
except Exception as e:
|
||||||
self._debounce_timer.timeout.connect(self._do_refresh)
|
FreeCAD.Console.PrintLog(f"Create: Silo auth panel skipped: {e}\n")
|
||||||
|
|
||||||
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"<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")
|
|
||||||
|
|
||||||
|
|
||||||
def _setup_silo_activity_panel():
|
def _setup_silo_activity_panel():
|
||||||
"""Show a dock widget with the Silo database browser."""
|
"""Show a dock widget with recent Silo database activity."""
|
||||||
try:
|
try:
|
||||||
from PySide import QtCore, QtWidgets
|
from PySide import QtCore, QtWidgets
|
||||||
|
|
||||||
@@ -381,57 +136,42 @@ def _setup_silo_activity_panel():
|
|||||||
panel = QtWidgets.QDockWidget("Database Activity", mw)
|
panel = QtWidgets.QDockWidget("Database Activity", mw)
|
||||||
panel.setObjectName("SiloDatabaseActivity")
|
panel.setObjectName("SiloDatabaseActivity")
|
||||||
|
|
||||||
activity_widget = SiloActivityPanel()
|
widget = QtWidgets.QWidget()
|
||||||
panel.setWidget(activity_widget.widget)
|
layout = QtWidgets.QVBoxLayout(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)
|
mw.addDockWidget(QtCore.Qt.RightDockWidgetArea, panel)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
FreeCAD.Console.PrintLog(f"Create: Silo activity panel skipped: {e}\n")
|
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
|
# Defer enhancements until the GUI event loop is running
|
||||||
try:
|
try:
|
||||||
from PySide.QtCore import QTimer
|
from PySide.QtCore import QTimer
|
||||||
|
|
||||||
|
QTimer.singleShot(1500, _setup_silo_auth_panel)
|
||||||
QTimer.singleShot(2000, _setup_silo_menu)
|
QTimer.singleShot(2000, _setup_silo_menu)
|
||||||
QTimer.singleShot(3000, _check_silo_first_start)
|
QTimer.singleShot(3000, _check_silo_first_start)
|
||||||
QTimer.singleShot(4000, _setup_silo_activity_panel)
|
QTimer.singleShot(4000, _setup_silo_activity_panel)
|
||||||
QTimer.singleShot(4500, _setup_silo_auth_panel)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|||||||
Reference in New Issue
Block a user