vector-inspector 0.2.5__tar.gz → 0.2.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. {vector_inspector-0.2.5 → vector_inspector-0.2.6}/PKG-INFO +3 -3
  2. {vector_inspector-0.2.5 → vector_inspector-0.2.6}/README.md +1 -1
  3. {vector_inspector-0.2.5 → vector_inspector-0.2.6}/pyproject.toml +2 -2
  4. {vector_inspector-0.2.5 → vector_inspector-0.2.6}/src/vector_inspector/services/settings_service.py +1 -1
  5. {vector_inspector-0.2.5 → vector_inspector-0.2.6}/src/vector_inspector/services/visualization_service.py +11 -7
  6. {vector_inspector-0.2.5 → vector_inspector-0.2.6}/src/vector_inspector/ui/main_window.py +14 -1
  7. {vector_inspector-0.2.5 → vector_inspector-0.2.6}/src/vector_inspector/ui/views/connection_view.py +43 -8
  8. {vector_inspector-0.2.5 → vector_inspector-0.2.6}/src/vector_inspector/ui/views/info_panel.py +1 -25
  9. {vector_inspector-0.2.5 → vector_inspector-0.2.6}/src/vector_inspector/ui/views/metadata_view.py +65 -25
  10. {vector_inspector-0.2.5 → vector_inspector-0.2.6}/src/vector_inspector/__init__.py +0 -0
  11. {vector_inspector-0.2.5 → vector_inspector-0.2.6}/src/vector_inspector/__main__.py +0 -0
  12. {vector_inspector-0.2.5 → vector_inspector-0.2.6}/src/vector_inspector/core/__init__.py +0 -0
  13. {vector_inspector-0.2.5 → vector_inspector-0.2.6}/src/vector_inspector/core/connections/__init__.py +0 -0
  14. {vector_inspector-0.2.5 → vector_inspector-0.2.6}/src/vector_inspector/core/connections/base_connection.py +0 -0
  15. {vector_inspector-0.2.5 → vector_inspector-0.2.6}/src/vector_inspector/core/connections/chroma_connection.py +0 -0
  16. {vector_inspector-0.2.5 → vector_inspector-0.2.6}/src/vector_inspector/core/connections/qdrant_connection.py +0 -0
  17. {vector_inspector-0.2.5 → vector_inspector-0.2.6}/src/vector_inspector/core/connections/template_connection.py +0 -0
  18. {vector_inspector-0.2.5 → vector_inspector-0.2.6}/src/vector_inspector/main.py +0 -0
  19. {vector_inspector-0.2.5 → vector_inspector-0.2.6}/src/vector_inspector/services/__init__.py +0 -0
  20. {vector_inspector-0.2.5 → vector_inspector-0.2.6}/src/vector_inspector/services/backup_restore_service.py +0 -0
  21. {vector_inspector-0.2.5 → vector_inspector-0.2.6}/src/vector_inspector/services/filter_service.py +0 -0
  22. {vector_inspector-0.2.5 → vector_inspector-0.2.6}/src/vector_inspector/services/import_export_service.py +0 -0
  23. {vector_inspector-0.2.5 → vector_inspector-0.2.6}/src/vector_inspector/ui/__init__.py +0 -0
  24. {vector_inspector-0.2.5 → vector_inspector-0.2.6}/src/vector_inspector/ui/components/__init__.py +0 -0
  25. {vector_inspector-0.2.5 → vector_inspector-0.2.6}/src/vector_inspector/ui/components/backup_restore_dialog.py +0 -0
  26. {vector_inspector-0.2.5 → vector_inspector-0.2.6}/src/vector_inspector/ui/components/filter_builder.py +0 -0
  27. {vector_inspector-0.2.5 → vector_inspector-0.2.6}/src/vector_inspector/ui/components/item_dialog.py +0 -0
  28. {vector_inspector-0.2.5 → vector_inspector-0.2.6}/src/vector_inspector/ui/components/loading_dialog.py +0 -0
  29. {vector_inspector-0.2.5 → vector_inspector-0.2.6}/src/vector_inspector/ui/views/__init__.py +0 -0
  30. {vector_inspector-0.2.5 → vector_inspector-0.2.6}/src/vector_inspector/ui/views/collection_browser.py +0 -0
  31. {vector_inspector-0.2.5 → vector_inspector-0.2.6}/src/vector_inspector/ui/views/search_view.py +0 -0
  32. {vector_inspector-0.2.5 → vector_inspector-0.2.6}/src/vector_inspector/ui/views/visualization_view.py +0 -0
  33. {vector_inspector-0.2.5 → vector_inspector-0.2.6}/src/vector_inspector/utils/__init__.py +0 -0
  34. {vector_inspector-0.2.5 → vector_inspector-0.2.6}/src/vector_inspector/utils/lazy_imports.py +0 -0
  35. {vector_inspector-0.2.5 → vector_inspector-0.2.6}/tests/test_connections.py +0 -0
  36. {vector_inspector-0.2.5 → vector_inspector-0.2.6}/tests/test_filter_service.py +0 -0
  37. {vector_inspector-0.2.5 → vector_inspector-0.2.6}/tests/test_settings_service.py +0 -0
  38. {vector_inspector-0.2.5 → vector_inspector-0.2.6}/tests/vector_inspector.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: vector-inspector
3
- Version: 0.2.5
3
+ Version: 0.2.6
4
4
  Summary: A comprehensive desktop application for visualizing, querying, and managing vector database data
5
5
  Author-Email: Anthony Dawson <anthonypdawson+github@gmail.com>
6
6
  License: MIT
@@ -8,7 +8,7 @@ Project-URL: Homepage, https://vector-inspector.divinedevops.com
8
8
  Project-URL: Source, https://github.com/anthonypdawson/vector-inspector
9
9
  Project-URL: Issues, https://github.com/anthonypdawson/vector-inspector/issues
10
10
  Project-URL: Documentation, https://github.com/anthonypdawson/vector-inspector#readme
11
- Requires-Python: ==3.12.*
11
+ Requires-Python: <3.13,>=3.10
12
12
  Requires-Dist: chromadb>=0.4.22
13
13
  Requires-Dist: qdrant-client>=1.7.0
14
14
  Requires-Dist: pyside6>=6.6.0
@@ -27,7 +27,7 @@ Description-Content-Type: text/markdown
27
27
 
28
28
  # Vector Inspector
29
29
 
30
- > **Disclaimer:** This tool is currently under active development and is **not production ready**. Not all features have been thoroughly tested. Use with caution in critical or production environments.
30
+ > **Disclaimer:** This tool is currently under active development and is **not production ready**. Not all features have been thoroughly tested and code is released frequently. Use with caution in critical or production environments.
31
31
 
32
32
  ![PyPI](https://img.shields.io/pypi/v/vector-inspector)
33
33
  [![PyPI Downloads](https://static.pepy.tech/personalized-badge/vector-inspector?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/vector-inspector)
@@ -1,7 +1,7 @@
1
1
 
2
2
  # Vector Inspector
3
3
 
4
- > **Disclaimer:** This tool is currently under active development and is **not production ready**. Not all features have been thoroughly tested. Use with caution in critical or production environments.
4
+ > **Disclaimer:** This tool is currently under active development and is **not production ready**. Not all features have been thoroughly tested and code is released frequently. Use with caution in critical or production environments.
5
5
 
6
6
  ![PyPI](https://img.shields.io/pypi/v/vector-inspector)
7
7
  [![PyPI Downloads](https://static.pepy.tech/personalized-badge/vector-inspector?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/vector-inspector)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "vector-inspector"
3
- version = "0.2.5"
3
+ version = "0.2.6"
4
4
  description = "A comprehensive desktop application for visualizing, querying, and managing vector database data"
5
5
  authors = [
6
6
  { name = "Anthony Dawson", email = "anthonypdawson+github@gmail.com" },
@@ -20,7 +20,7 @@ dependencies = [
20
20
  "pyarrow>=14.0.0",
21
21
  "pinecone>=8.0.0",
22
22
  ]
23
- requires-python = "==3.12.*"
23
+ requires-python = ">=3.10,<3.13"
24
24
  readme = "README.md"
25
25
 
26
26
  [project.license]
@@ -10,7 +10,7 @@ class SettingsService:
10
10
 
11
11
  def __init__(self):
12
12
  """Initialize settings service."""
13
- self.settings_dir = Path.home() / ".vector-viewer"
13
+ self.settings_dir = Path.home() / ".vector-inspector"
14
14
  self.settings_file = self.settings_dir / "settings.json"
15
15
  self.settings: Dict[str, Any] = {}
16
16
  self._load_settings()
@@ -1,6 +1,7 @@
1
1
  """Visualization service for dimensionality reduction."""
2
2
 
3
3
  from typing import Optional, List, Tuple, Any
4
+ import warnings
4
5
 
5
6
 
6
7
  class VisualizationService:
@@ -40,7 +41,7 @@ class VisualizationService:
40
41
  reducer = PCA(n_components=n_components)
41
42
  reduced = reducer.fit_transform(X)
42
43
 
43
- elif method.lower() == "t-sne":
44
+ elif method.lower() in ["t-sne", "tsne"]:
44
45
  TSNE = get_sklearn_model('TSNE')
45
46
  perplexity = kwargs.get("perplexity", min(30, len(embeddings) - 1))
46
47
  reducer = TSNE(
@@ -53,12 +54,15 @@ class VisualizationService:
53
54
  elif method.lower() == "umap":
54
55
  UMAP = get_sklearn_model('UMAP')
55
56
  n_neighbors = kwargs.get("n_neighbors", min(15, len(embeddings) - 1))
56
- reducer = UMAP(
57
- n_components=n_components,
58
- n_neighbors=n_neighbors,
59
- random_state=42
60
- )
61
- reduced = reducer.fit_transform(X)
57
+ # Suppress n_jobs warning when using random_state
58
+ with warnings.catch_warnings():
59
+ warnings.filterwarnings("ignore", message=".*n_jobs.*overridden.*")
60
+ reducer = UMAP(
61
+ n_components=n_components,
62
+ n_neighbors=n_neighbors,
63
+ random_state=42
64
+ )
65
+ reduced = reducer.fit_transform(X)
62
66
 
63
67
  else:
64
68
  print(f"Unknown method: {method}")
@@ -5,7 +5,7 @@ from PySide6.QtWidgets import (
5
5
  QSplitter, QTabWidget, QStatusBar, QToolBar,
6
6
  QMessageBox, QInputDialog, QFileDialog
7
7
  )
8
- from PySide6.QtCore import Qt, Signal
8
+ from PySide6.QtCore import Qt, Signal, QTimer
9
9
  from PySide6.QtGui import QAction
10
10
 
11
11
  from vector_inspector.core.connections.base_connection import VectorDBConnection
@@ -16,6 +16,7 @@ from vector_inspector.ui.views.info_panel import InfoPanel
16
16
  from vector_inspector.ui.views.metadata_view import MetadataView
17
17
  from vector_inspector.ui.views.search_view import SearchView
18
18
  from vector_inspector.ui.components.backup_restore_dialog import BackupRestoreDialog
19
+ from vector_inspector.ui.components.loading_dialog import LoadingDialog
19
20
 
20
21
 
21
22
  class MainWindow(QMainWindow):
@@ -27,6 +28,7 @@ class MainWindow(QMainWindow):
27
28
  super().__init__()
28
29
  self.connection: VectorDBConnection = ChromaDBConnection()
29
30
  self.current_collection: str = ""
31
+ self.loading_dialog = LoadingDialog("Loading collection...", self)
30
32
 
31
33
  self.setWindowTitle("Vector Inspector")
32
34
  self.setGeometry(100, 100, 1400, 900)
@@ -241,6 +243,14 @@ class MainWindow(QMainWindow):
241
243
  self.current_collection = collection_name
242
244
  self.statusBar.showMessage(f"Collection: {collection_name}")
243
245
 
246
+ # Show loading dialog immediately
247
+ self.loading_dialog.show_loading(f"Loading collection '{collection_name}'...")
248
+
249
+ # Update views with new collection - use QTimer to allow loading dialog to appear first
250
+ QTimer.singleShot(10, lambda: self._update_views_for_collection(collection_name))
251
+
252
+ def _update_views_for_collection(self, collection_name: str):
253
+ """Update all views with the selected collection."""
244
254
  # Update all views with new collection
245
255
  self.info_panel.set_collection(collection_name)
246
256
  self.metadata_view.set_collection(collection_name)
@@ -249,6 +259,9 @@ class MainWindow(QMainWindow):
249
259
  if self.visualization_view is not None:
250
260
  self.visualization_view.set_collection(collection_name)
251
261
 
262
+ # Hide loading dialog
263
+ self.loading_dialog.hide_loading()
264
+
252
265
  def _on_refresh_collections(self):
253
266
  """Refresh collection list."""
254
267
  if self.connection.is_connected:
@@ -5,7 +5,7 @@ from PySide6.QtWidgets import (
5
5
  QPushButton, QDialog, QFormLayout, QLineEdit,
6
6
  QRadioButton, QButtonGroup, QGroupBox, QFileDialog, QComboBox, QApplication, QCheckBox
7
7
  )
8
- from PySide6.QtCore import Signal
8
+ from PySide6.QtCore import Signal, QThread
9
9
 
10
10
  from vector_inspector.core.connections.base_connection import VectorDBConnection
11
11
  from vector_inspector.core.connections.chroma_connection import ChromaDBConnection
@@ -14,6 +14,28 @@ from vector_inspector.ui.components.loading_dialog import LoadingDialog
14
14
  from vector_inspector.services.settings_service import SettingsService
15
15
 
16
16
 
17
+ class ConnectionThread(QThread):
18
+ """Background thread for connecting to database."""
19
+
20
+ finished = Signal(bool, list) # success, collections
21
+
22
+ def __init__(self, connection):
23
+ super().__init__()
24
+ self.connection = connection
25
+
26
+ def run(self):
27
+ """Connect to database and get collections."""
28
+ try:
29
+ success = self.connection.connect()
30
+ if success:
31
+ collections = self.connection.list_collections()
32
+ self.finished.emit(True, collections)
33
+ else:
34
+ self.finished.emit(False, [])
35
+ except Exception:
36
+ self.finished.emit(False, [])
37
+
38
+
17
39
  class ConnectionDialog(QDialog):
18
40
  """Dialog for configuring database connection."""
19
41
 
@@ -175,6 +197,9 @@ class ConnectionDialog(QDialog):
175
197
 
176
198
  def get_connection_config(self):
177
199
  """Get connection configuration from dialog."""
200
+ # Get current provider from combo box to ensure it's up to date
201
+ self.provider = self.provider_combo.currentData()
202
+
178
203
  config = {"provider": self.provider}
179
204
 
180
205
  if self.persistent_radio.isChecked():
@@ -294,6 +319,7 @@ class ConnectionView(QWidget):
294
319
  self.connection = connection
295
320
  self.loading_dialog = LoadingDialog("Connecting to database...", self)
296
321
  self.settings_service = SettingsService()
322
+ self.connection_thread = None
297
323
  self._setup_ui()
298
324
 
299
325
  # Try to auto-connect if enabled in settings
@@ -339,7 +365,6 @@ class ConnectionView(QWidget):
339
365
  def _connect_with_config(self, config: dict):
340
366
  """Connect to database with given configuration."""
341
367
  self.loading_dialog.show_loading("Connecting to database...")
342
- QApplication.processEvents()
343
368
 
344
369
  provider = config.get("provider", "chromadb")
345
370
  conn_type = config.get("type")
@@ -367,11 +392,25 @@ class ConnectionView(QWidget):
367
392
  else: # ephemeral
368
393
  self.connection = ChromaDBConnection()
369
394
 
395
+ # Store config for later use
396
+ self._pending_config = config
397
+
370
398
  # Notify parent that connection instance changed
371
399
  self.connection_created.emit(self.connection)
372
- success = self.connection.connect()
373
-
400
+
401
+ # Start background thread to connect
402
+ self.connection_thread = ConnectionThread(self.connection)
403
+ self.connection_thread.finished.connect(self._on_connection_finished)
404
+ self.connection_thread.start()
405
+
406
+ def _on_connection_finished(self, success: bool, collections: list):
407
+ """Handle connection thread completion."""
408
+ self.loading_dialog.hide_loading()
409
+
374
410
  if success:
411
+ config = self._pending_config
412
+ provider = config.get("provider", "chromadb")
413
+
375
414
  # Show provider, path/host + collection count for clarity
376
415
  details = []
377
416
  details.append(f"provider: {provider}")
@@ -380,7 +419,6 @@ class ConnectionView(QWidget):
380
419
  if hasattr(self.connection, 'host') and self.connection.host:
381
420
  port = getattr(self.connection, 'port', None)
382
421
  details.append(f"host: {self.connection.host}:{port}")
383
- collections = self.connection.list_collections()
384
422
  count_text = f"collections: {len(collections)}"
385
423
  info = ", ".join(details)
386
424
  self.status_label.setText(f"Status: Connected ({info}, {count_text})")
@@ -401,9 +439,6 @@ class ConnectionView(QWidget):
401
439
  self.disconnect_button.setEnabled(False)
402
440
  self.connection_changed.emit(False)
403
441
 
404
- # Close loading dialog after everything is complete
405
- self.loading_dialog.hide_loading()
406
-
407
442
  def _disconnect(self):
408
443
  """Disconnect from database."""
409
444
  self.connection.disconnect()
@@ -56,18 +56,6 @@ class InfoPanel(QWidget):
56
56
  self.db_group.setLayout(db_layout)
57
57
  container_layout.addWidget(self.db_group)
58
58
 
59
- # Collections List Section
60
- self.collections_group = QGroupBox("Available Collections")
61
- collections_layout = QVBoxLayout()
62
-
63
- self.collections_list_label = QLabel("No collections")
64
- self.collections_list_label.setWordWrap(True)
65
- self.collections_list_label.setStyleSheet("color: gray; padding: 10px;")
66
- collections_layout.addWidget(self.collections_list_label)
67
-
68
- self.collections_group.setLayout(collections_layout)
69
- container_layout.addWidget(self.collections_group)
70
-
71
59
  # Collection Information Section
72
60
  self.collection_group = QGroupBox("Collection Information")
73
61
  collection_layout = QVBoxLayout()
@@ -141,8 +129,6 @@ class InfoPanel(QWidget):
141
129
  self._update_label(self.api_key_label, "N/A")
142
130
  self._update_label(self.status_label, "Disconnected")
143
131
  self._update_label(self.collections_count_label, "0")
144
- self.collections_list_label.setText("No collections")
145
- self.collections_list_label.setStyleSheet("color: gray; padding: 10px;")
146
132
  # Also clear collection info
147
133
  self._update_label(self.collection_name_label, "No collection selected")
148
134
  self._update_label(self.vector_dim_label, "N/A")
@@ -195,22 +181,12 @@ class InfoPanel(QWidget):
195
181
  # Status
196
182
  self._update_label(self.status_label, "Connected" if self.connection.is_connected else "Disconnected")
197
183
 
198
- # List collections
184
+ # Count collections
199
185
  try:
200
186
  collections = self.connection.list_collections()
201
187
  self._update_label(self.collections_count_label, str(len(collections)))
202
-
203
- if collections:
204
- collections_text = "\n".join([f"• {name}" for name in sorted(collections)])
205
- self.collections_list_label.setText(collections_text)
206
- self.collections_list_label.setStyleSheet("color: white; padding: 10px; font-family: monospace;")
207
- else:
208
- self.collections_list_label.setText("No collections found")
209
- self.collections_list_label.setStyleSheet("color: gray; padding: 10px;")
210
188
  except Exception as e:
211
189
  self._update_label(self.collections_count_label, "Error")
212
- self.collections_list_label.setText(f"Error loading collections: {str(e)}")
213
- self.collections_list_label.setStyleSheet("color: red; padding: 10px;")
214
190
 
215
191
  def refresh_collection_info(self):
216
192
  """Refresh collection-specific information."""
@@ -7,7 +7,7 @@ from PySide6.QtWidgets import (
7
7
  QLineEdit, QComboBox, QGroupBox, QHeaderView, QMessageBox, QDialog,
8
8
  QFileDialog, QMenu
9
9
  )
10
- from PySide6.QtCore import Qt, QTimer
10
+ from PySide6.QtCore import Qt, QTimer, QThread, Signal
11
11
 
12
12
  from vector_inspector.core.connections.base_connection import VectorDBConnection
13
13
  from vector_inspector.ui.components.item_dialog import ItemDialog
@@ -19,6 +19,37 @@ from vector_inspector.services.settings_service import SettingsService
19
19
  from PySide6.QtWidgets import QApplication
20
20
 
21
21
 
22
+ class DataLoadThread(QThread):
23
+ """Background thread for loading collection data."""
24
+
25
+ finished = Signal(dict)
26
+ error = Signal(str)
27
+
28
+ def __init__(self, connection, collection, page_size, offset, server_filter):
29
+ super().__init__()
30
+ self.connection = connection
31
+ self.collection = collection
32
+ self.page_size = page_size
33
+ self.offset = offset
34
+ self.server_filter = server_filter
35
+
36
+ def run(self):
37
+ """Load data from database."""
38
+ try:
39
+ data = self.connection.get_all_items(
40
+ self.collection,
41
+ limit=self.page_size,
42
+ offset=self.offset,
43
+ where=self.server_filter
44
+ )
45
+ if data:
46
+ self.finished.emit(data)
47
+ else:
48
+ self.error.emit("Failed to load data")
49
+ except Exception as e:
50
+ self.error.emit(str(e))
51
+
52
+
22
53
  class MetadataView(QWidget):
23
54
  """View for browsing collection data and metadata."""
24
55
 
@@ -31,6 +62,7 @@ class MetadataView(QWidget):
31
62
  self.current_page = 0
32
63
  self.loading_dialog = LoadingDialog("Loading data...", self)
33
64
  self.settings_service = SettingsService()
65
+ self.load_thread: Optional[DataLoadThread] = None
34
66
 
35
67
  # Debounce timer for filter changes
36
68
  self.filter_reload_timer = QTimer()
@@ -145,21 +177,11 @@ class MetadataView(QWidget):
145
177
  self.current_collection = collection_name
146
178
  self.current_page = 0
147
179
 
148
- # Show loading dialog at the start
149
- self.loading_dialog.show_loading("Loading collection data...")
150
- QApplication.processEvents()
180
+ # Update filter builder with supported operators
181
+ operators = self.connection.get_supported_filter_operators()
182
+ self.filter_builder.set_operators(operators)
151
183
 
152
- try:
153
- # Update filter builder with supported operators
154
- operators = self.connection.get_supported_filter_operators()
155
- self.filter_builder.set_operators(operators)
156
-
157
- self._load_data_internal()
158
-
159
- # Ensure UI is fully updated before hiding loading dialog
160
- QApplication.processEvents()
161
- finally:
162
- self.loading_dialog.hide_loading()
184
+ self._load_data_internal()
163
185
 
164
186
  def _load_data(self):
165
187
  """Load data from current collection (with loading dialog)."""
@@ -182,35 +204,53 @@ class MetadataView(QWidget):
182
204
  self.table.setRowCount(0)
183
205
  return
184
206
 
207
+ # Cancel any existing load thread
208
+ if self.load_thread and self.load_thread.isRunning():
209
+ self.load_thread.quit()
210
+ self.load_thread.wait()
211
+
185
212
  offset = self.current_page * self.page_size
186
213
 
187
214
  # Get filters split into server-side and client-side
188
215
  server_filter = None
189
- client_filters = []
216
+ self.client_filters = []
190
217
  if self.filter_group.isChecked() and self.filter_builder.has_filters():
191
- server_filter, client_filters = self.filter_builder.get_filters_split()
218
+ server_filter, self.client_filters = self.filter_builder.get_filters_split()
192
219
 
193
- data = self.connection.get_all_items(
220
+ # Start background thread to load data
221
+ self.load_thread = DataLoadThread(
222
+ self.connection,
194
223
  self.current_collection,
195
- limit=self.page_size,
196
- offset=offset,
197
- where=server_filter
224
+ self.page_size,
225
+ offset,
226
+ server_filter
198
227
  )
199
-
228
+ self.load_thread.finished.connect(self._on_data_loaded)
229
+ self.load_thread.error.connect(self._on_load_error)
230
+ self.load_thread.start()
231
+
232
+ def _on_data_loaded(self, data: Dict[str, Any]):
233
+ """Handle data loaded from background thread."""
200
234
  # Apply client-side filters if any
201
- if client_filters and data:
202
- data = apply_client_side_filters(data, client_filters)
235
+ if self.client_filters and data:
236
+ data = apply_client_side_filters(data, self.client_filters)
203
237
 
204
238
  if not data:
205
- self.status_label.setText("Failed to load data")
239
+ self.status_label.setText("No data after filtering")
206
240
  self.table.setRowCount(0)
207
241
  return
242
+
208
243
  self.current_data = data
209
244
  self._populate_table(data)
210
245
  self._update_pagination_controls()
211
246
 
212
247
  # Update filter builder with available metadata fields
213
248
  self._update_filter_fields(data)
249
+
250
+ def _on_load_error(self, error_msg: str):
251
+ """Handle error from background thread."""
252
+ self.status_label.setText(f"Failed to load data: {error_msg}")
253
+ self.table.setRowCount(0)
214
254
 
215
255
  def _update_filter_fields(self, data: Dict[str, Any]):
216
256
  """Update filter builder with available metadata field names."""