Compare commits
6 Commits
feat/editi
...
feat/live-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7c6066030 | ||
| 91f539a18a | |||
| 2ddfea083a | |||
| be8783bf0a | |||
| 069bb7a552 | |||
| 8a6e5cdffa |
191
freecad/open_search.py
Normal file
191
freecad/open_search.py
Normal file
@@ -0,0 +1,191 @@
|
||||
"""Search-and-open widget for Kindred Create.
|
||||
|
||||
Provides :class:`OpenItemWidget`, a plain ``QWidget`` that can be
|
||||
embedded in an MDI sub-window. Searches both the Silo database and
|
||||
local CAD files, presenting results in a unified table. Emits
|
||||
``item_selected`` when the user picks an item and ``cancelled`` when
|
||||
the user clicks Cancel.
|
||||
"""
|
||||
|
||||
import FreeCAD
|
||||
from PySide import QtCore, QtWidgets
|
||||
|
||||
|
||||
class OpenItemWidget(QtWidgets.QWidget):
|
||||
"""Search-and-open widget for embedding in an MDI subwindow.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
client : SiloClient
|
||||
Authenticated Silo API client instance.
|
||||
search_local_fn : callable
|
||||
Function that accepts a search term string and returns an
|
||||
iterable of dicts with keys ``part_number``, ``description``,
|
||||
``path``, ``modified``.
|
||||
parent : QWidget, optional
|
||||
Parent widget.
|
||||
|
||||
Signals
|
||||
-------
|
||||
item_selected(dict)
|
||||
Emitted when the user selects an item. The dict contains
|
||||
keys: *part_number*, *description*, *item_type*, *source*
|
||||
(``"database"``, ``"local"``, or ``"both"``), *modified*,
|
||||
and *path* (str or ``None``).
|
||||
cancelled()
|
||||
Emitted when the user clicks Cancel.
|
||||
"""
|
||||
|
||||
item_selected = QtCore.Signal(dict)
|
||||
cancelled = QtCore.Signal()
|
||||
|
||||
def __init__(self, client, search_local_fn, parent=None):
|
||||
super().__init__(parent)
|
||||
self._client = client
|
||||
self._search_local = search_local_fn
|
||||
self._results_data = []
|
||||
|
||||
self.setMinimumWidth(700)
|
||||
self.setMinimumHeight(500)
|
||||
|
||||
self._build_ui()
|
||||
|
||||
# Debounced search timer (500 ms)
|
||||
self._search_timer = QtCore.QTimer(self)
|
||||
self._search_timer.setSingleShot(True)
|
||||
self._search_timer.setInterval(500)
|
||||
self._search_timer.timeout.connect(self._do_search)
|
||||
|
||||
# Populate on first display
|
||||
QtCore.QTimer.singleShot(0, self._do_search)
|
||||
|
||||
# ---- UI construction ---------------------------------------------------
|
||||
|
||||
def _build_ui(self):
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.setSpacing(8)
|
||||
|
||||
# Search row
|
||||
self._search_input = QtWidgets.QLineEdit()
|
||||
self._search_input.setPlaceholderText("Search by part number or description...")
|
||||
self._search_input.textChanged.connect(self._on_search_changed)
|
||||
layout.addWidget(self._search_input)
|
||||
|
||||
# Filter checkboxes
|
||||
filter_layout = QtWidgets.QHBoxLayout()
|
||||
self._db_checkbox = QtWidgets.QCheckBox("Database")
|
||||
self._db_checkbox.setChecked(True)
|
||||
self._local_checkbox = QtWidgets.QCheckBox("Local Files")
|
||||
self._local_checkbox.setChecked(True)
|
||||
self._db_checkbox.toggled.connect(self._on_filter_changed)
|
||||
self._local_checkbox.toggled.connect(self._on_filter_changed)
|
||||
filter_layout.addWidget(self._db_checkbox)
|
||||
filter_layout.addWidget(self._local_checkbox)
|
||||
filter_layout.addStretch()
|
||||
layout.addLayout(filter_layout)
|
||||
|
||||
# Results table
|
||||
self._results_table = QtWidgets.QTableWidget()
|
||||
self._results_table.setColumnCount(5)
|
||||
self._results_table.setHorizontalHeaderLabels(
|
||||
["Part Number", "Description", "Type", "Source", "Modified"]
|
||||
)
|
||||
self._results_table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
|
||||
self._results_table.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
|
||||
self._results_table.horizontalHeader().setStretchLastSection(True)
|
||||
self._results_table.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
|
||||
self._results_table.doubleClicked.connect(self._open_selected)
|
||||
layout.addWidget(self._results_table, 1)
|
||||
|
||||
# Buttons
|
||||
btn_layout = QtWidgets.QHBoxLayout()
|
||||
btn_layout.addStretch()
|
||||
open_btn = QtWidgets.QPushButton("Open")
|
||||
open_btn.clicked.connect(self._open_selected)
|
||||
cancel_btn = QtWidgets.QPushButton("Cancel")
|
||||
cancel_btn.clicked.connect(self.cancelled.emit)
|
||||
btn_layout.addWidget(open_btn)
|
||||
btn_layout.addWidget(cancel_btn)
|
||||
layout.addLayout(btn_layout)
|
||||
|
||||
# ---- Search logic ------------------------------------------------------
|
||||
|
||||
def _on_search_changed(self, _text):
|
||||
"""Restart debounce timer on each keystroke."""
|
||||
self._search_timer.start()
|
||||
|
||||
def _on_filter_changed(self, _checked):
|
||||
"""Re-run search immediately when filter checkboxes change."""
|
||||
self._do_search()
|
||||
|
||||
def _do_search(self):
|
||||
"""Execute search against database and/or local files."""
|
||||
search_term = self._search_input.text().strip()
|
||||
self._results_data = []
|
||||
|
||||
if self._db_checkbox.isChecked():
|
||||
try:
|
||||
for item in self._client.list_items(search=search_term):
|
||||
self._results_data.append(
|
||||
{
|
||||
"part_number": item.get("part_number", ""),
|
||||
"description": item.get("description", ""),
|
||||
"item_type": item.get("item_type", ""),
|
||||
"source": "database",
|
||||
"modified": item.get("updated_at", "")[:10]
|
||||
if item.get("updated_at")
|
||||
else "",
|
||||
"path": None,
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
FreeCAD.Console.PrintWarning(f"DB search failed: {e}\n")
|
||||
|
||||
if self._local_checkbox.isChecked():
|
||||
try:
|
||||
for item in self._search_local(search_term):
|
||||
existing = next(
|
||||
(r for r in self._results_data if r["part_number"] == item["part_number"]),
|
||||
None,
|
||||
)
|
||||
if existing:
|
||||
existing["source"] = "both"
|
||||
existing["path"] = item.get("path")
|
||||
else:
|
||||
self._results_data.append(
|
||||
{
|
||||
"part_number": item.get("part_number", ""),
|
||||
"description": item.get("description", ""),
|
||||
"item_type": "",
|
||||
"source": "local",
|
||||
"modified": item.get("modified", "")[:10]
|
||||
if item.get("modified")
|
||||
else "",
|
||||
"path": item.get("path"),
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
FreeCAD.Console.PrintWarning(f"Local search failed: {e}\n")
|
||||
|
||||
self._populate_table()
|
||||
|
||||
def _populate_table(self):
|
||||
"""Refresh the results table from ``_results_data``."""
|
||||
self._results_table.setRowCount(len(self._results_data))
|
||||
for row, data in enumerate(self._results_data):
|
||||
self._results_table.setItem(row, 0, QtWidgets.QTableWidgetItem(data["part_number"]))
|
||||
self._results_table.setItem(row, 1, QtWidgets.QTableWidgetItem(data["description"]))
|
||||
self._results_table.setItem(row, 2, QtWidgets.QTableWidgetItem(data["item_type"]))
|
||||
self._results_table.setItem(row, 3, QtWidgets.QTableWidgetItem(data["source"]))
|
||||
self._results_table.setItem(row, 4, QtWidgets.QTableWidgetItem(data["modified"]))
|
||||
self._results_table.resizeColumnsToContents()
|
||||
|
||||
# ---- Selection ---------------------------------------------------------
|
||||
|
||||
def _open_selected(self):
|
||||
"""Emit ``item_selected`` with the data from the selected row."""
|
||||
selected = self._results_table.selectedItems()
|
||||
if not selected:
|
||||
return
|
||||
row = selected[0].row()
|
||||
self.item_selected.emit(dict(self._results_data[row]))
|
||||
@@ -7,6 +7,7 @@ import socket
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
@@ -26,7 +27,28 @@ from silo_client import (
|
||||
_PREF_GROUP = "User parameter:BaseApp/Preferences/Mod/KindredSilo"
|
||||
|
||||
# Configuration - preferences take priority over env vars
|
||||
SILO_PROJECTS_DIR = os.environ.get("SILO_PROJECTS_DIR", os.path.expanduser("~/projects"))
|
||||
SILO_PROJECTS_DIR = os.environ.get(
|
||||
"SILO_PROJECTS_DIR", os.path.expanduser("~/projects")
|
||||
)
|
||||
|
||||
|
||||
def _relative_time(dt):
|
||||
"""Format a datetime as a human-friendly relative string."""
|
||||
now = datetime.now()
|
||||
diff = now - dt
|
||||
seconds = int(diff.total_seconds())
|
||||
if seconds < 60:
|
||||
return "just now"
|
||||
minutes = seconds // 60
|
||||
if minutes < 60:
|
||||
return f"{minutes}m ago"
|
||||
hours = minutes // 60
|
||||
if hours < 24:
|
||||
return f"{hours}h ago"
|
||||
days = hours // 24
|
||||
if days < 30:
|
||||
return f"{days}d ago"
|
||||
return dt.strftime("%Y-%m-%d")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -64,7 +86,9 @@ class FreeCADSiloSettings(SiloSettings):
|
||||
param = FreeCAD.ParamGet(_PREF_GROUP)
|
||||
return param.GetString("SslCertPath", "")
|
||||
|
||||
def save_auth(self, username: str, role: str = "", source: str = "", token: str = ""):
|
||||
def save_auth(
|
||||
self, username: str, role: str = "", source: str = "", token: str = ""
|
||||
):
|
||||
param = FreeCAD.ParamGet(_PREF_GROUP)
|
||||
param.SetString("AuthUsername", username)
|
||||
param.SetString("AuthRole", role)
|
||||
@@ -122,7 +146,9 @@ def _get_ssl_verify() -> bool:
|
||||
def _get_ssl_context():
|
||||
from silo_client._ssl import build_ssl_context
|
||||
|
||||
return build_ssl_context(_fc_settings.get_ssl_verify(), _fc_settings.get_ssl_cert_path())
|
||||
return build_ssl_context(
|
||||
_fc_settings.get_ssl_verify(), _fc_settings.get_ssl_cert_path()
|
||||
)
|
||||
|
||||
|
||||
def _get_auth_headers() -> Dict[str, str]:
|
||||
@@ -179,7 +205,9 @@ def _fetch_server_mode() -> str:
|
||||
# Icon helper
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_ICON_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "resources", "icons")
|
||||
_ICON_DIR = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)), "resources", "icons"
|
||||
)
|
||||
|
||||
|
||||
def _icon(name):
|
||||
@@ -574,7 +602,9 @@ def handle_kindred_url(url: str):
|
||||
parts = [parsed.netloc] + [p for p in parsed.path.split("/") if p]
|
||||
if len(parts) >= 2 and parts[0] == "item":
|
||||
part_number = parts[1]
|
||||
FreeCAD.Console.PrintMessage(f"Silo: Opening item {part_number} from kindred:// URL\n")
|
||||
FreeCAD.Console.PrintMessage(
|
||||
f"Silo: Opening item {part_number} from kindred:// URL\n"
|
||||
)
|
||||
_sync.open_item(part_number)
|
||||
|
||||
|
||||
@@ -594,143 +624,32 @@ class Silo_Open:
|
||||
}
|
||||
|
||||
def Activated(self):
|
||||
from PySide import QtCore, QtGui
|
||||
from open_search import OpenItemWidget
|
||||
from PySide import QtGui, QtWidgets
|
||||
|
||||
dialog = QtGui.QDialog()
|
||||
dialog.setWindowTitle("Silo - Open Item")
|
||||
dialog.setMinimumWidth(700)
|
||||
dialog.setMinimumHeight(500)
|
||||
mw = FreeCADGui.getMainWindow()
|
||||
mdi = mw.findChild(QtWidgets.QMdiArea)
|
||||
if not mdi:
|
||||
return
|
||||
|
||||
layout = QtGui.QVBoxLayout(dialog)
|
||||
widget = OpenItemWidget(_client, search_local_files)
|
||||
|
||||
# Search row
|
||||
search_layout = QtGui.QHBoxLayout()
|
||||
search_input = QtGui.QLineEdit()
|
||||
search_input.setPlaceholderText("Search by part number or description...")
|
||||
search_layout.addWidget(search_input)
|
||||
layout.addLayout(search_layout)
|
||||
sw = mdi.addSubWindow(widget)
|
||||
sw.setWindowTitle("Open Item")
|
||||
sw.setWindowIcon(QtGui.QIcon(_icon("open")))
|
||||
sw.show()
|
||||
mdi.setActiveSubWindow(sw)
|
||||
|
||||
# Filters
|
||||
filter_layout = QtGui.QHBoxLayout()
|
||||
db_checkbox = QtGui.QCheckBox("Database")
|
||||
db_checkbox.setChecked(True)
|
||||
local_checkbox = QtGui.QCheckBox("Local Files")
|
||||
local_checkbox.setChecked(True)
|
||||
filter_layout.addWidget(db_checkbox)
|
||||
filter_layout.addWidget(local_checkbox)
|
||||
filter_layout.addStretch()
|
||||
layout.addLayout(filter_layout)
|
||||
|
||||
# Results table
|
||||
results_table = QtGui.QTableWidget()
|
||||
results_table.setColumnCount(5)
|
||||
results_table.setHorizontalHeaderLabels(
|
||||
["Part Number", "Description", "Type", "Source", "Modified"]
|
||||
)
|
||||
results_table.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
|
||||
results_table.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
|
||||
results_table.horizontalHeader().setStretchLastSection(True)
|
||||
results_table.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
|
||||
layout.addWidget(results_table)
|
||||
|
||||
results_data = []
|
||||
|
||||
def do_search():
|
||||
nonlocal results_data
|
||||
search_term = search_input.text().strip()
|
||||
results_data = []
|
||||
results_table.setRowCount(0)
|
||||
|
||||
if db_checkbox.isChecked():
|
||||
try:
|
||||
for item in _client.list_items(search=search_term):
|
||||
results_data.append(
|
||||
{
|
||||
"part_number": item.get("part_number", ""),
|
||||
"description": item.get("description", ""),
|
||||
"item_type": item.get("item_type", ""),
|
||||
"source": "database",
|
||||
"modified": item.get("updated_at", "")[:10]
|
||||
if item.get("updated_at")
|
||||
else "",
|
||||
"path": None,
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
FreeCAD.Console.PrintWarning(f"DB search failed: {e}\n")
|
||||
|
||||
if local_checkbox.isChecked():
|
||||
try:
|
||||
for item in search_local_files(search_term):
|
||||
existing = next(
|
||||
(r for r in results_data if r["part_number"] == item["part_number"]),
|
||||
None,
|
||||
)
|
||||
if existing:
|
||||
existing["source"] = "both"
|
||||
existing["path"] = item.get("path")
|
||||
else:
|
||||
results_data.append(
|
||||
{
|
||||
"part_number": item.get("part_number", ""),
|
||||
"description": item.get("description", ""),
|
||||
"item_type": "",
|
||||
"source": "local",
|
||||
"modified": item.get("modified", "")[:10]
|
||||
if item.get("modified")
|
||||
else "",
|
||||
"path": item.get("path"),
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
FreeCAD.Console.PrintWarning(f"Local search failed: {e}\n")
|
||||
|
||||
results_table.setRowCount(len(results_data))
|
||||
for row, data in enumerate(results_data):
|
||||
results_table.setItem(row, 0, QtGui.QTableWidgetItem(data["part_number"]))
|
||||
results_table.setItem(row, 1, QtGui.QTableWidgetItem(data["description"]))
|
||||
results_table.setItem(row, 2, QtGui.QTableWidgetItem(data["item_type"]))
|
||||
results_table.setItem(row, 3, QtGui.QTableWidgetItem(data["source"]))
|
||||
results_table.setItem(row, 4, QtGui.QTableWidgetItem(data["modified"]))
|
||||
results_table.resizeColumnsToContents()
|
||||
|
||||
_open_after_close = [None]
|
||||
|
||||
def open_selected():
|
||||
selected = results_table.selectedItems()
|
||||
if not selected:
|
||||
return
|
||||
row = selected[0].row()
|
||||
_open_after_close[0] = dict(results_data[row])
|
||||
dialog.accept()
|
||||
|
||||
search_input.textChanged.connect(lambda: do_search())
|
||||
results_table.doubleClicked.connect(open_selected)
|
||||
|
||||
# Buttons
|
||||
btn_layout = QtGui.QHBoxLayout()
|
||||
open_btn = QtGui.QPushButton("Open")
|
||||
open_btn.clicked.connect(open_selected)
|
||||
cancel_btn = QtGui.QPushButton("Cancel")
|
||||
cancel_btn.clicked.connect(dialog.reject)
|
||||
btn_layout.addStretch()
|
||||
btn_layout.addWidget(open_btn)
|
||||
btn_layout.addWidget(cancel_btn)
|
||||
layout.addLayout(btn_layout)
|
||||
|
||||
do_search()
|
||||
dialog.exec_()
|
||||
|
||||
# Open the document AFTER the dialog has fully closed so that
|
||||
# heavy document loads (especially Assembly files) don't run
|
||||
# inside the dialog's nested event loop, which can cause crashes.
|
||||
data = _open_after_close[0]
|
||||
if data is not None:
|
||||
def _on_selected(data):
|
||||
sw.close()
|
||||
if data.get("path"):
|
||||
FreeCAD.openDocument(data["path"])
|
||||
else:
|
||||
_sync.open_item(data["part_number"])
|
||||
|
||||
widget.item_selected.connect(_on_selected)
|
||||
widget.cancelled.connect(sw.close)
|
||||
|
||||
def IsActive(self):
|
||||
return True
|
||||
|
||||
@@ -753,7 +672,6 @@ class Silo_New:
|
||||
|
||||
def Activated(self):
|
||||
from PySide import QtGui, QtWidgets
|
||||
|
||||
from schema_form import SchemaFormWidget
|
||||
|
||||
mw = FreeCADGui.getMainWindow()
|
||||
@@ -793,7 +711,9 @@ class Silo_New:
|
||||
},
|
||||
)
|
||||
obj.Label = part_number
|
||||
_sync.save_to_canonical_path(FreeCAD.ActiveDocument, force_rename=True)
|
||||
_sync.save_to_canonical_path(
|
||||
FreeCAD.ActiveDocument, force_rename=True
|
||||
)
|
||||
else:
|
||||
_sync.create_document_for_item(result, save=True)
|
||||
|
||||
@@ -874,7 +794,9 @@ class Silo_Save:
|
||||
|
||||
# Try to upload to MinIO
|
||||
try:
|
||||
result = _client._upload_file(part_number, str(file_path), properties, "Auto-save")
|
||||
result = _client._upload_file(
|
||||
part_number, str(file_path), properties, "Auto-save"
|
||||
)
|
||||
|
||||
new_rev = result["revision_number"]
|
||||
FreeCAD.Console.PrintMessage(f"Uploaded as revision {new_rev}\n")
|
||||
@@ -907,7 +829,9 @@ class Silo_Commit:
|
||||
|
||||
obj = get_tracked_object(doc)
|
||||
if not obj:
|
||||
FreeCAD.Console.PrintError("No tracked object. Use 'New' to register first.\n")
|
||||
FreeCAD.Console.PrintError(
|
||||
"No tracked object. Use 'New' to register first.\n"
|
||||
)
|
||||
return
|
||||
|
||||
part_number = obj.SiloPartNumber
|
||||
@@ -924,7 +848,9 @@ class Silo_Commit:
|
||||
if not file_path:
|
||||
return
|
||||
|
||||
result = _client._upload_file(part_number, str(file_path), properties, comment)
|
||||
result = _client._upload_file(
|
||||
part_number, str(file_path), properties, comment
|
||||
)
|
||||
|
||||
new_rev = result["revision_number"]
|
||||
FreeCAD.Console.PrintMessage(f"Committed revision {new_rev}: {comment}\n")
|
||||
@@ -973,7 +899,9 @@ def _check_pull_conflicts(part_number, local_path, doc=None):
|
||||
server_updated = item.get("updated_at", "")
|
||||
if server_updated:
|
||||
# Parse ISO format timestamp
|
||||
server_dt = datetime.datetime.fromisoformat(server_updated.replace("Z", "+00:00"))
|
||||
server_dt = datetime.datetime.fromisoformat(
|
||||
server_updated.replace("Z", "+00:00")
|
||||
)
|
||||
if server_dt > local_mtime:
|
||||
conflicts.append("Server version is newer than local file.")
|
||||
except Exception:
|
||||
@@ -1003,7 +931,9 @@ class SiloPullDialog:
|
||||
# Revision table
|
||||
self._table = QtGui.QTableWidget()
|
||||
self._table.setColumnCount(5)
|
||||
self._table.setHorizontalHeaderLabels(["Rev", "Date", "Comment", "Status", "File"])
|
||||
self._table.setHorizontalHeaderLabels(
|
||||
["Rev", "Date", "Comment", "Status", "File"]
|
||||
)
|
||||
self._table.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
|
||||
self._table.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
|
||||
self._table.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
|
||||
@@ -1075,6 +1005,67 @@ class SiloPullDialog:
|
||||
return None
|
||||
|
||||
|
||||
def _pull_dependencies(part_number, progress_callback=None):
|
||||
"""Recursively pull all BOM children that have files on the server.
|
||||
|
||||
Returns list of (part_number, dest_path) tuples for successfully pulled files.
|
||||
Skips children that already exist locally.
|
||||
"""
|
||||
pulled = []
|
||||
try:
|
||||
bom = _client.get_bom(part_number)
|
||||
except Exception as e:
|
||||
FreeCAD.Console.PrintWarning(f"Could not fetch BOM for {part_number}: {e}\n")
|
||||
return pulled
|
||||
|
||||
for entry in bom:
|
||||
child_pn = entry.get("child_part_number")
|
||||
if not child_pn:
|
||||
continue
|
||||
|
||||
# Skip if already exists locally
|
||||
existing = find_file_by_part_number(child_pn)
|
||||
if existing and existing.exists():
|
||||
FreeCAD.Console.PrintMessage(
|
||||
f" {child_pn}: already exists at {existing}\n"
|
||||
)
|
||||
# Still recurse — this child may itself be an assembly with missing deps
|
||||
_pull_dependencies(child_pn, progress_callback)
|
||||
continue
|
||||
|
||||
# Check if this child has a file on the server
|
||||
try:
|
||||
latest = _client.latest_file_revision(child_pn)
|
||||
except Exception:
|
||||
latest = None
|
||||
|
||||
if not latest or not latest.get("file_key"):
|
||||
FreeCAD.Console.PrintMessage(f" {child_pn}: no file on server, skipping\n")
|
||||
continue
|
||||
|
||||
# Determine destination path
|
||||
child_desc = entry.get("child_description", "")
|
||||
dest_path = get_cad_file_path(child_pn, child_desc)
|
||||
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
rev_num = latest["revision_number"]
|
||||
FreeCAD.Console.PrintMessage(f" Pulling {child_pn} rev {rev_num}...\n")
|
||||
|
||||
try:
|
||||
ok = _client._download_file(
|
||||
child_pn, rev_num, str(dest_path), progress_callback=progress_callback
|
||||
)
|
||||
if ok:
|
||||
pulled.append((child_pn, dest_path))
|
||||
except Exception as e:
|
||||
FreeCAD.Console.PrintWarning(f" Failed to pull {child_pn}: {e}\n")
|
||||
|
||||
# Recurse into child (it may be a sub-assembly)
|
||||
_pull_dependencies(child_pn, progress_callback)
|
||||
|
||||
return pulled
|
||||
|
||||
|
||||
class Silo_Pull:
|
||||
"""Download from MinIO / sync from database."""
|
||||
|
||||
@@ -1117,14 +1108,18 @@ class Silo_Pull:
|
||||
|
||||
if not has_any_file:
|
||||
if existing_local:
|
||||
FreeCAD.Console.PrintMessage(f"Opening existing local file: {existing_local}\n")
|
||||
FreeCAD.Console.PrintMessage(
|
||||
f"Opening existing local file: {existing_local}\n"
|
||||
)
|
||||
FreeCAD.openDocument(str(existing_local))
|
||||
else:
|
||||
try:
|
||||
item = _client.get_item(part_number)
|
||||
new_doc = _sync.create_document_for_item(item, save=True)
|
||||
if new_doc:
|
||||
FreeCAD.Console.PrintMessage(f"Created local file for {part_number}\n")
|
||||
FreeCAD.Console.PrintMessage(
|
||||
f"Created local file for {part_number}\n"
|
||||
)
|
||||
else:
|
||||
QtGui.QMessageBox.warning(
|
||||
None,
|
||||
@@ -1202,6 +1197,19 @@ class Silo_Pull:
|
||||
|
||||
FreeCAD.Console.PrintMessage(f"Pulled revision {rev_num} of {part_number}\n")
|
||||
|
||||
# Pull assembly dependencies before opening so links resolve
|
||||
if item.get("item_type") == "assembly":
|
||||
progress.setLabelText(f"Pulling dependencies for {part_number}...")
|
||||
progress.setValue(0)
|
||||
progress.show()
|
||||
dep_pulled = _pull_dependencies(part_number, progress_callback=on_progress)
|
||||
progress.setValue(100)
|
||||
progress.close()
|
||||
if dep_pulled:
|
||||
FreeCAD.Console.PrintMessage(
|
||||
f"Pulled {len(dep_pulled)} dependency file(s)\n"
|
||||
)
|
||||
|
||||
# Close existing document if open, then reopen
|
||||
if doc and doc.FileName == str(dest_path):
|
||||
FreeCAD.closeDocument(doc.Name)
|
||||
@@ -1255,7 +1263,9 @@ class Silo_Push:
|
||||
server_dt = datetime.fromisoformat(
|
||||
server_time_str.replace("Z", "+00:00")
|
||||
)
|
||||
local_dt = datetime.fromtimestamp(local_mtime, tz=timezone.utc)
|
||||
local_dt = datetime.fromtimestamp(
|
||||
local_mtime, tz=timezone.utc
|
||||
)
|
||||
if local_dt > server_dt:
|
||||
unuploaded.append(lf)
|
||||
else:
|
||||
@@ -1268,7 +1278,9 @@ class Silo_Push:
|
||||
pass # Not in DB, skip
|
||||
|
||||
if not unuploaded:
|
||||
QtGui.QMessageBox.information(None, "Push", "All local files are already uploaded.")
|
||||
QtGui.QMessageBox.information(
|
||||
None, "Push", "All local files are already uploaded."
|
||||
)
|
||||
return
|
||||
|
||||
msg = f"Found {len(unuploaded)} files to upload:\n\n"
|
||||
@@ -1286,7 +1298,9 @@ class Silo_Push:
|
||||
|
||||
uploaded = 0
|
||||
for item in unuploaded:
|
||||
result = _sync.upload_file(item["part_number"], item["path"], "Synced from local")
|
||||
result = _sync.upload_file(
|
||||
item["part_number"], item["path"], "Synced from local"
|
||||
)
|
||||
if result:
|
||||
uploaded += 1
|
||||
|
||||
@@ -1335,9 +1349,7 @@ class Silo_Info:
|
||||
msg = f"<h3>{part_number}</h3>"
|
||||
msg += f"<p><b>Type:</b> {item.get('item_type', '-')}</p>"
|
||||
msg += f"<p><b>Description:</b> {item.get('description', '-')}</p>"
|
||||
msg += (
|
||||
f"<p><b>Projects:</b> {', '.join(project_codes) if project_codes else 'None'}</p>"
|
||||
)
|
||||
msg += f"<p><b>Projects:</b> {', '.join(project_codes) if project_codes else 'None'}</p>"
|
||||
msg += f"<p><b>Current Revision:</b> {item.get('current_revision', 1)}</p>"
|
||||
msg += f"<p><b>Local Revision:</b> {getattr(obj, 'SiloRevision', '-')}</p>"
|
||||
|
||||
@@ -1403,7 +1415,9 @@ class Silo_TagProjects:
|
||||
try:
|
||||
# Get current projects for item
|
||||
current_projects = _client.get_item_projects(part_number)
|
||||
current_codes = {p.get("code", "") for p in current_projects if p.get("code")}
|
||||
current_codes = {
|
||||
p.get("code", "") for p in current_projects if p.get("code")
|
||||
}
|
||||
|
||||
# Get all available projects
|
||||
all_projects = _client.get_projects()
|
||||
@@ -1514,7 +1528,9 @@ class Silo_Rollback:
|
||||
dialog.setMinimumHeight(300)
|
||||
layout = QtGui.QVBoxLayout(dialog)
|
||||
|
||||
label = QtGui.QLabel(f"Select a revision to rollback to (current: Rev {current_rev}):")
|
||||
label = QtGui.QLabel(
|
||||
f"Select a revision to rollback to (current: Rev {current_rev}):"
|
||||
)
|
||||
layout.addWidget(label)
|
||||
|
||||
# Revision table
|
||||
@@ -1529,8 +1545,12 @@ class Silo_Rollback:
|
||||
for i, rev in enumerate(prev_revisions):
|
||||
table.setItem(i, 0, QtGui.QTableWidgetItem(str(rev["revision_number"])))
|
||||
table.setItem(i, 1, QtGui.QTableWidgetItem(rev.get("status", "draft")))
|
||||
table.setItem(i, 2, QtGui.QTableWidgetItem(rev.get("created_at", "")[:10]))
|
||||
table.setItem(i, 3, QtGui.QTableWidgetItem(rev.get("comment", "") or ""))
|
||||
table.setItem(
|
||||
i, 2, QtGui.QTableWidgetItem(rev.get("created_at", "")[:10])
|
||||
)
|
||||
table.setItem(
|
||||
i, 3, QtGui.QTableWidgetItem(rev.get("comment", "") or "")
|
||||
)
|
||||
|
||||
table.resizeColumnsToContents()
|
||||
layout.addWidget(table)
|
||||
@@ -1556,7 +1576,9 @@ class Silo_Rollback:
|
||||
def on_rollback():
|
||||
selected = table.selectedItems()
|
||||
if not selected:
|
||||
QtGui.QMessageBox.warning(dialog, "Rollback", "Please select a revision")
|
||||
QtGui.QMessageBox.warning(
|
||||
dialog, "Rollback", "Please select a revision"
|
||||
)
|
||||
return
|
||||
selected_rev[0] = int(table.item(selected[0].row(), 0).text())
|
||||
dialog.accept()
|
||||
@@ -1654,7 +1676,9 @@ class Silo_SetStatus:
|
||||
# Update status
|
||||
_client.update_revision(part_number, rev_num, status=status)
|
||||
|
||||
FreeCAD.Console.PrintMessage(f"Updated Rev {rev_num} status to '{status}'\n")
|
||||
FreeCAD.Console.PrintMessage(
|
||||
f"Updated Rev {rev_num} status to '{status}'\n"
|
||||
)
|
||||
QtGui.QMessageBox.information(
|
||||
None, "Status Updated", f"Revision {rev_num} status set to '{status}'"
|
||||
)
|
||||
@@ -1718,7 +1742,9 @@ class Silo_Settings:
|
||||
ssl_checkbox.setChecked(param.GetBool("SslVerify", True))
|
||||
layout.addWidget(ssl_checkbox)
|
||||
|
||||
ssl_hint = QtGui.QLabel("Disable only for internal servers with self-signed certificates.")
|
||||
ssl_hint = QtGui.QLabel(
|
||||
"Disable only for internal servers with self-signed certificates."
|
||||
)
|
||||
ssl_hint.setWordWrap(True)
|
||||
ssl_hint.setStyleSheet("color: #888; font-size: 11px;")
|
||||
layout.addWidget(ssl_hint)
|
||||
@@ -1995,7 +2021,9 @@ class Silo_BOM:
|
||||
|
||||
wu_table = QtGui.QTableWidget()
|
||||
wu_table.setColumnCount(5)
|
||||
wu_table.setHorizontalHeaderLabels(["Parent Part Number", "Type", "Qty", "Unit", "Ref Des"])
|
||||
wu_table.setHorizontalHeaderLabels(
|
||||
["Parent Part Number", "Type", "Qty", "Unit", "Ref Des"]
|
||||
)
|
||||
wu_table.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
|
||||
wu_table.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
|
||||
wu_table.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
|
||||
@@ -2024,12 +2052,16 @@ class Silo_BOM:
|
||||
bom_table.setItem(
|
||||
row, 1, QtGui.QTableWidgetItem(entry.get("child_description", ""))
|
||||
)
|
||||
bom_table.setItem(row, 2, QtGui.QTableWidgetItem(entry.get("rel_type", "")))
|
||||
bom_table.setItem(
|
||||
row, 2, QtGui.QTableWidgetItem(entry.get("rel_type", ""))
|
||||
)
|
||||
qty = entry.get("quantity")
|
||||
bom_table.setItem(
|
||||
row, 3, QtGui.QTableWidgetItem(str(qty) if qty is not None else "")
|
||||
)
|
||||
bom_table.setItem(row, 4, QtGui.QTableWidgetItem(entry.get("unit") or ""))
|
||||
bom_table.setItem(
|
||||
row, 4, QtGui.QTableWidgetItem(entry.get("unit") or "")
|
||||
)
|
||||
ref_des = entry.get("reference_designators") or []
|
||||
bom_table.setItem(row, 5, QtGui.QTableWidgetItem(", ".join(ref_des)))
|
||||
bom_table.setItem(
|
||||
@@ -2051,12 +2083,16 @@ class Silo_BOM:
|
||||
wu_table.setItem(
|
||||
row, 0, QtGui.QTableWidgetItem(entry.get("parent_part_number", ""))
|
||||
)
|
||||
wu_table.setItem(row, 1, QtGui.QTableWidgetItem(entry.get("rel_type", "")))
|
||||
wu_table.setItem(
|
||||
row, 1, QtGui.QTableWidgetItem(entry.get("rel_type", ""))
|
||||
)
|
||||
qty = entry.get("quantity")
|
||||
wu_table.setItem(
|
||||
row, 2, QtGui.QTableWidgetItem(str(qty) if qty is not None else "")
|
||||
)
|
||||
wu_table.setItem(row, 3, QtGui.QTableWidgetItem(entry.get("unit") or ""))
|
||||
wu_table.setItem(
|
||||
row, 3, QtGui.QTableWidgetItem(entry.get("unit") or "")
|
||||
)
|
||||
ref_des = entry.get("reference_designators") or []
|
||||
wu_table.setItem(row, 4, QtGui.QTableWidgetItem(", ".join(ref_des)))
|
||||
wu_table.resizeColumnsToContents()
|
||||
@@ -2109,7 +2145,9 @@ class Silo_BOM:
|
||||
try:
|
||||
qty = float(qty_text)
|
||||
except ValueError:
|
||||
QtGui.QMessageBox.warning(dialog, "BOM", "Quantity must be a number.")
|
||||
QtGui.QMessageBox.warning(
|
||||
dialog, "BOM", "Quantity must be a number."
|
||||
)
|
||||
return
|
||||
|
||||
unit = unit_input.text().strip() or None
|
||||
@@ -2188,7 +2226,9 @@ class Silo_BOM:
|
||||
try:
|
||||
new_qty = float(qty_text)
|
||||
except ValueError:
|
||||
QtGui.QMessageBox.warning(dialog, "BOM", "Quantity must be a number.")
|
||||
QtGui.QMessageBox.warning(
|
||||
dialog, "BOM", "Quantity must be a number."
|
||||
)
|
||||
return
|
||||
|
||||
new_unit = unit_input.text().strip() or None
|
||||
@@ -2212,7 +2252,9 @@ class Silo_BOM:
|
||||
)
|
||||
load_bom()
|
||||
except Exception as exc:
|
||||
QtGui.QMessageBox.warning(dialog, "BOM", f"Failed to update entry:\n{exc}")
|
||||
QtGui.QMessageBox.warning(
|
||||
dialog, "BOM", f"Failed to update entry:\n{exc}"
|
||||
)
|
||||
|
||||
def on_remove():
|
||||
selected = bom_table.selectedItems()
|
||||
@@ -2238,7 +2280,9 @@ class Silo_BOM:
|
||||
_client.delete_bom_entry(part_number, child_pn)
|
||||
load_bom()
|
||||
except Exception as exc:
|
||||
QtGui.QMessageBox.warning(dialog, "BOM", f"Failed to remove entry:\n{exc}")
|
||||
QtGui.QMessageBox.warning(
|
||||
dialog, "BOM", f"Failed to remove entry:\n{exc}"
|
||||
)
|
||||
|
||||
add_btn.clicked.connect(on_add)
|
||||
edit_btn.clicked.connect(on_edit)
|
||||
@@ -2277,7 +2321,9 @@ class SiloEventListener(QtCore.QThread):
|
||||
|
||||
item_updated = QtCore.Signal(str) # part_number
|
||||
revision_created = QtCore.Signal(str, int) # part_number, revision
|
||||
connection_status = QtCore.Signal(str, int, str) # (status, retry_count, error_message)
|
||||
connection_status = QtCore.Signal(
|
||||
str, int, str
|
||||
) # (status, retry_count, error_message)
|
||||
server_mode_changed = QtCore.Signal(str) # "normal" / "read-only" / "degraded"
|
||||
|
||||
_MAX_RETRIES = 10
|
||||
@@ -2351,7 +2397,9 @@ class SiloEventListener(QtCore.QThread):
|
||||
req = urllib.request.Request(url, headers=headers, method="GET")
|
||||
|
||||
try:
|
||||
self._response = urllib.request.urlopen(req, context=_get_ssl_context(), timeout=90)
|
||||
self._response = urllib.request.urlopen(
|
||||
req, context=_get_ssl_context(), timeout=90
|
||||
)
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code in (404, 501):
|
||||
raise _SSEUnsupported()
|
||||
@@ -2422,6 +2470,8 @@ class SiloAuthDockWidget:
|
||||
|
||||
self.widget = QtGui.QWidget()
|
||||
self._event_listener = None
|
||||
self._activity_events = [] # list of (datetime, text, part_number)
|
||||
self._activity_seeded = False
|
||||
self._build_ui()
|
||||
self._refresh_status()
|
||||
|
||||
@@ -2429,6 +2479,11 @@ class SiloAuthDockWidget:
|
||||
self._timer.timeout.connect(self._refresh_status)
|
||||
self._timer.start(30000)
|
||||
|
||||
# Refresh relative timestamps every 60s
|
||||
self._ts_timer = QtCore.QTimer(self.widget)
|
||||
self._ts_timer.timeout.connect(self._rebuild_activity_feed)
|
||||
self._ts_timer.start(60000)
|
||||
|
||||
# -- UI construction ----------------------------------------------------
|
||||
|
||||
def _build_ui(self):
|
||||
@@ -2631,8 +2686,11 @@ class SiloAuthDockWidget:
|
||||
self._sse_label.setStyleSheet("font-size: 11px; color: #4CAF50;")
|
||||
self._sse_label.setToolTip("")
|
||||
FreeCAD.Console.PrintMessage("Silo: SSE connected\n")
|
||||
self._seed_activity_feed()
|
||||
elif status == "disconnected":
|
||||
self._sse_label.setText(f"Reconnecting ({retry}/{SiloEventListener._MAX_RETRIES})...")
|
||||
self._sse_label.setText(
|
||||
f"Reconnecting ({retry}/{SiloEventListener._MAX_RETRIES})..."
|
||||
)
|
||||
self._sse_label.setStyleSheet("font-size: 11px; color: #FF9800;")
|
||||
self._sse_label.setToolTip(error or "Connection lost")
|
||||
FreeCAD.Console.PrintWarning(
|
||||
@@ -2642,7 +2700,9 @@ class SiloAuthDockWidget:
|
||||
self._sse_label.setText("Disconnected")
|
||||
self._sse_label.setStyleSheet("font-size: 11px; color: #F44336;")
|
||||
self._sse_label.setToolTip(error or "Max retries reached")
|
||||
FreeCAD.Console.PrintError(f"Silo: SSE gave up after {retry} retries: {error}\n")
|
||||
FreeCAD.Console.PrintError(
|
||||
f"Silo: SSE gave up after {retry} retries: {error}\n"
|
||||
)
|
||||
elif status == "unsupported":
|
||||
self._sse_label.setText("Not available")
|
||||
self._sse_label.setStyleSheet("font-size: 11px; color: #888;")
|
||||
@@ -2651,6 +2711,8 @@ class SiloAuthDockWidget:
|
||||
global _server_mode
|
||||
_server_mode = mode
|
||||
self._update_mode_banner()
|
||||
if mode != "normal":
|
||||
self._append_activity_event(f"Server mode: {mode}")
|
||||
|
||||
def _update_mode_banner(self):
|
||||
_MODE_BANNERS = {
|
||||
@@ -2681,18 +2743,59 @@ class SiloAuthDockWidget:
|
||||
mw = FreeCADGui.getMainWindow()
|
||||
if mw is not None:
|
||||
mw.statusBar().showMessage(f"Silo: {part_number} updated on server", 5000)
|
||||
self._refresh_activity_panel()
|
||||
self._append_activity_event(f"{part_number} updated", part_number)
|
||||
|
||||
def _on_remote_revision(self, part_number, revision):
|
||||
FreeCAD.Console.PrintMessage(f"Silo: New revision {revision} for {part_number}\n")
|
||||
FreeCAD.Console.PrintMessage(
|
||||
f"Silo: New revision {revision} for {part_number}\n"
|
||||
)
|
||||
mw = FreeCADGui.getMainWindow()
|
||||
if mw is not None:
|
||||
mw.statusBar().showMessage(f"Silo: {part_number} rev {revision} available", 5000)
|
||||
self._refresh_activity_panel()
|
||||
mw.statusBar().showMessage(
|
||||
f"Silo: {part_number} rev {revision} available", 5000
|
||||
)
|
||||
self._append_activity_event(
|
||||
f"{part_number} Rev {revision} created", part_number
|
||||
)
|
||||
|
||||
def _refresh_activity_panel(self):
|
||||
"""Refresh the Database Activity panel if it exists."""
|
||||
from PySide import QtCore, QtGui, QtWidgets
|
||||
def _append_activity_event(self, text, pn=""):
|
||||
"""Prepend an event to the activity feed and rebuild the display."""
|
||||
self._activity_events.insert(0, (datetime.now(), text, pn))
|
||||
self._activity_events = self._activity_events[:50]
|
||||
self._rebuild_activity_feed()
|
||||
|
||||
def _seed_activity_feed(self):
|
||||
"""One-time: populate the feed with recent items from the database."""
|
||||
if self._activity_seeded:
|
||||
return
|
||||
self._activity_seeded = True
|
||||
try:
|
||||
items = _client.list_items()
|
||||
if isinstance(items, list):
|
||||
for item in reversed(items[:10]):
|
||||
pn = item.get("part_number", "")
|
||||
desc = item.get("description", "")
|
||||
if desc and len(desc) > 40:
|
||||
desc = desc[:37] + "..."
|
||||
text = f"{pn} \u2013 {desc}" if desc else pn
|
||||
updated = item.get("updated_at", "")
|
||||
ts = datetime.now()
|
||||
if updated:
|
||||
try:
|
||||
ts = datetime.fromisoformat(
|
||||
updated.replace("Z", "+00:00")
|
||||
).replace(tzinfo=None)
|
||||
except (ValueError, AttributeError):
|
||||
pass
|
||||
self._activity_events.insert(0, (ts, text, pn))
|
||||
self._activity_events = self._activity_events[:50]
|
||||
except Exception:
|
||||
pass
|
||||
self._rebuild_activity_feed()
|
||||
|
||||
def _rebuild_activity_feed(self):
|
||||
"""Render _activity_events into the Database Activity QListWidget."""
|
||||
from PySide import QtCore, QtWidgets
|
||||
|
||||
mw = FreeCADGui.getMainWindow()
|
||||
if mw is None:
|
||||
@@ -2714,64 +2817,18 @@ class SiloAuthDockWidget:
|
||||
)
|
||||
activity_list._silo_connected = True
|
||||
|
||||
# Collect local part numbers for badge
|
||||
local_pns = set()
|
||||
try:
|
||||
for lf in search_local_files():
|
||||
local_pns.add(lf.get("part_number", ""))
|
||||
except Exception:
|
||||
pass
|
||||
if not self._activity_events:
|
||||
item = QtWidgets.QListWidgetItem("(No activity yet)")
|
||||
item.setFlags(QtCore.Qt.NoItemFlags)
|
||||
activity_list.addItem(item)
|
||||
return
|
||||
|
||||
try:
|
||||
items = _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]
|
||||
|
||||
# Fetch latest revision info
|
||||
rev_num = ""
|
||||
comment = ""
|
||||
try:
|
||||
revs = _client.get_revisions(pn)
|
||||
if revs:
|
||||
latest = revs[0] if isinstance(revs, list) else revs
|
||||
rev_num = str(latest.get("revision_number", ""))
|
||||
comment = latest.get("comment", "") or ""
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Truncate long descriptions
|
||||
desc_display = desc
|
||||
if len(desc_display) > 40:
|
||||
desc_display = desc_display[:37] + "..."
|
||||
|
||||
# Build display text
|
||||
rev_part = f" \u2013 Rev {rev_num}" if rev_num else ""
|
||||
date_part = f" \u2013 {updated}" if updated else ""
|
||||
local_badge = " \u25cf local" if pn in local_pns else ""
|
||||
line1 = f"{pn} \u2013 {desc_display}{rev_part}{date_part}{local_badge}"
|
||||
|
||||
if comment:
|
||||
line1 += f'\n "{comment}"'
|
||||
else:
|
||||
line1 += "\n (no comment)"
|
||||
|
||||
list_item = QtWidgets.QListWidgetItem(line1)
|
||||
list_item.setData(QtCore.Qt.UserRole, pn)
|
||||
if desc and len(desc) > 40:
|
||||
list_item.setToolTip(desc)
|
||||
if pn in local_pns:
|
||||
list_item.setForeground(QtGui.QColor("#4CAF50"))
|
||||
activity_list.addItem(list_item)
|
||||
|
||||
if activity_list.count() == 0:
|
||||
activity_list.addItem("(No items in database)")
|
||||
except Exception:
|
||||
activity_list.addItem("(Unable to refresh activity)")
|
||||
for ts, text, pn in self._activity_events:
|
||||
label = f"{text} \u00b7 {_relative_time(ts)}"
|
||||
list_item = QtWidgets.QListWidgetItem(label)
|
||||
if pn:
|
||||
list_item.setData(QtCore.Qt.UserRole, pn)
|
||||
activity_list.addItem(list_item)
|
||||
|
||||
def _on_activity_double_click(self, item):
|
||||
"""Open/checkout item from activity pane."""
|
||||
@@ -3209,7 +3266,9 @@ class Silo_StartPanel:
|
||||
dock = QtGui.QDockWidget("Silo", mw)
|
||||
dock.setObjectName("SiloStartPanel")
|
||||
dock.setWidget(content.widget)
|
||||
dock.setAllowedAreas(QtCore.Qt.LeftDockWidgetArea | QtCore.Qt.RightDockWidgetArea)
|
||||
dock.setAllowedAreas(
|
||||
QtCore.Qt.LeftDockWidgetArea | QtCore.Qt.RightDockWidgetArea
|
||||
)
|
||||
mw.addDockWidget(QtCore.Qt.LeftDockWidgetArea, dock)
|
||||
|
||||
def IsActive(self):
|
||||
@@ -3243,9 +3302,9 @@ class _DiagWorker(QtCore.QThread):
|
||||
self.result.emit("DNS", False, "no hostname in URL")
|
||||
return
|
||||
try:
|
||||
addrs = socket.getaddrinfo(hostname, None, socket.AF_UNSPEC, socket.SOCK_STREAM)
|
||||
first_ip = addrs[0][4][0] if addrs else "?"
|
||||
self.result.emit("DNS", True, f"{hostname} -> {first_ip}")
|
||||
addrs = socket.getaddrinfo(
|
||||
hostname, None, socket.AF_UNSPEC, socket.SOCK_STREAM
|
||||
)
|
||||
except socket.gaierror as e:
|
||||
self.result.emit("DNS", False, f"{hostname}: {e}")
|
||||
except Exception as e:
|
||||
|
||||
Reference in New Issue
Block a user