vector-inspector 0.2.5__py3-none-any.whl → 0.2.7__py3-none-any.whl

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 (26) hide show
  1. vector_inspector/core/cache_manager.py +159 -0
  2. vector_inspector/core/connection_manager.py +277 -0
  3. vector_inspector/core/connections/chroma_connection.py +90 -5
  4. vector_inspector/core/connections/qdrant_connection.py +62 -8
  5. vector_inspector/core/embedding_utils.py +140 -0
  6. vector_inspector/services/backup_restore_service.py +3 -29
  7. vector_inspector/services/credential_service.py +130 -0
  8. vector_inspector/services/filter_service.py +1 -1
  9. vector_inspector/services/profile_service.py +409 -0
  10. vector_inspector/services/settings_service.py +20 -1
  11. vector_inspector/services/visualization_service.py +11 -7
  12. vector_inspector/ui/components/connection_manager_panel.py +320 -0
  13. vector_inspector/ui/components/profile_manager_panel.py +518 -0
  14. vector_inspector/ui/dialogs/__init__.py +5 -0
  15. vector_inspector/ui/dialogs/cross_db_migration.py +364 -0
  16. vector_inspector/ui/dialogs/embedding_config_dialog.py +176 -0
  17. vector_inspector/ui/main_window.py +429 -181
  18. vector_inspector/ui/views/connection_view.py +43 -8
  19. vector_inspector/ui/views/info_panel.py +226 -80
  20. vector_inspector/ui/views/metadata_view.py +136 -28
  21. vector_inspector/ui/views/search_view.py +43 -3
  22. {vector_inspector-0.2.5.dist-info → vector_inspector-0.2.7.dist-info}/METADATA +5 -3
  23. vector_inspector-0.2.7.dist-info/RECORD +45 -0
  24. vector_inspector-0.2.5.dist-info/RECORD +0 -35
  25. {vector_inspector-0.2.5.dist-info → vector_inspector-0.2.7.dist-info}/WHEEL +0 -0
  26. {vector_inspector-0.2.5.dist-info → vector_inspector-0.2.7.dist-info}/entry_points.txt +0 -0
@@ -1,41 +1,74 @@
1
- """Main application window."""
1
+ """Updated main window with multi-database support."""
2
2
 
3
3
  from PySide6.QtWidgets import (
4
4
  QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
5
5
  QSplitter, QTabWidget, QStatusBar, QToolBar,
6
- QMessageBox, QInputDialog, QFileDialog
6
+ QMessageBox, QInputDialog, QLabel, QDockWidget, QApplication, QDialog
7
7
  )
8
- from PySide6.QtCore import Qt, Signal
8
+ from PySide6.QtCore import Qt, Signal, QTimer, QThread
9
9
  from PySide6.QtGui import QAction
10
10
 
11
+ from vector_inspector.core.connection_manager import ConnectionManager, ConnectionState
11
12
  from vector_inspector.core.connections.base_connection import VectorDBConnection
12
13
  from vector_inspector.core.connections.chroma_connection import ChromaDBConnection
13
- from vector_inspector.ui.views.connection_view import ConnectionView
14
- from vector_inspector.ui.views.collection_browser import CollectionBrowser
14
+ from vector_inspector.core.connections.qdrant_connection import QdrantConnection
15
+ from vector_inspector.services.profile_service import ProfileService
16
+ from vector_inspector.services.settings_service import SettingsService
17
+ from vector_inspector.ui.components.connection_manager_panel import ConnectionManagerPanel
18
+ from vector_inspector.ui.components.profile_manager_panel import ProfileManagerPanel
15
19
  from vector_inspector.ui.views.info_panel import InfoPanel
16
20
  from vector_inspector.ui.views.metadata_view import MetadataView
17
21
  from vector_inspector.ui.views.search_view import SearchView
18
- from vector_inspector.ui.components.backup_restore_dialog import BackupRestoreDialog
22
+ from vector_inspector.ui.components.loading_dialog import LoadingDialog
19
23
 
20
24
 
21
- class MainWindow(QMainWindow):
22
- """Main application window with all views and controls."""
25
+ class ConnectionThread(QThread):
26
+ """Background thread for connecting to database."""
27
+
28
+ finished = Signal(bool, list, str) # success, collections, error_message
23
29
 
24
- connection_changed = Signal(bool) # Emits True when connected, False when disconnected
30
+ def __init__(self, connection):
31
+ super().__init__()
32
+ self.connection = connection
33
+
34
+ def run(self):
35
+ """Connect to database and get collections."""
36
+ try:
37
+ success = self.connection.connect()
38
+ if success:
39
+ collections = self.connection.list_collections()
40
+ self.finished.emit(True, collections, "")
41
+ else:
42
+ self.finished.emit(False, [], "Connection failed")
43
+ except Exception as e:
44
+ self.finished.emit(False, [], str(e))
45
+
46
+
47
+ class MainWindow(QMainWindow):
48
+ """Main application window with multi-database support."""
25
49
 
26
50
  def __init__(self):
27
51
  super().__init__()
28
- self.connection: VectorDBConnection = ChromaDBConnection()
29
- self.current_collection: str = ""
52
+
53
+ # Core services
54
+ self.connection_manager = ConnectionManager()
55
+ self.profile_service = ProfileService()
56
+ self.settings_service = SettingsService()
57
+ self.loading_dialog = LoadingDialog("Loading...", self)
58
+
59
+ # State
60
+ self.visualization_view = None
61
+ self._connection_threads = {} # Track connection threads
30
62
 
31
63
  self.setWindowTitle("Vector Inspector")
32
- self.setGeometry(100, 100, 1400, 900)
64
+ self.setGeometry(100, 100, 1600, 900)
33
65
 
34
66
  self._setup_ui()
35
67
  self._setup_menu_bar()
36
68
  self._setup_toolbar()
37
69
  self._setup_statusbar()
38
70
  self._connect_signals()
71
+ self._restore_session()
39
72
 
40
73
  def _setup_ui(self):
41
74
  """Setup the main UI layout."""
@@ -49,24 +82,31 @@ class MainWindow(QMainWindow):
49
82
  # Main splitter (left panel | right tabs)
50
83
  main_splitter = QSplitter(Qt.Horizontal)
51
84
 
52
- # Left panel - Connection and Collections
85
+ # Left panel - Connections and Profiles
53
86
  left_panel = QWidget()
54
87
  left_layout = QVBoxLayout(left_panel)
55
88
  left_layout.setContentsMargins(0, 0, 0, 0)
56
89
 
57
- self.connection_view = ConnectionView(self.connection)
58
- self.collection_browser = CollectionBrowser(self.connection)
90
+ # Create tab widget for connections and profiles
91
+ self.left_tabs = QTabWidget()
92
+
93
+ # Connection manager panel
94
+ self.connection_panel = ConnectionManagerPanel(self.connection_manager)
95
+ self.left_tabs.addTab(self.connection_panel, "Active")
59
96
 
60
- left_layout.addWidget(self.connection_view)
61
- left_layout.addWidget(self.collection_browser)
97
+ # Profile manager panel
98
+ self.profile_panel = ProfileManagerPanel(self.profile_service)
99
+ self.left_tabs.addTab(self.profile_panel, "Profiles")
100
+
101
+ left_layout.addWidget(self.left_tabs)
62
102
 
63
103
  # Right panel - Tabbed views
64
104
  self.tab_widget = QTabWidget()
65
105
 
66
- self.info_panel = InfoPanel(self.connection)
67
- self.metadata_view = MetadataView(self.connection)
68
- self.search_view = SearchView(self.connection)
69
- self.visualization_view = None # Lazy loaded
106
+ # Create views (they'll be updated when collection changes)
107
+ self.info_panel = InfoPanel(None) # Will be set later
108
+ self.metadata_view = MetadataView(None) # Will be set later
109
+ self.search_view = SearchView(None) # Will be set later
70
110
 
71
111
  self.tab_widget.addTab(self.info_panel, "Info")
72
112
  self.tab_widget.addTab(self.metadata_view, "Data Browser")
@@ -83,7 +123,7 @@ class MainWindow(QMainWindow):
83
123
  main_splitter.addWidget(left_panel)
84
124
  main_splitter.addWidget(self.tab_widget)
85
125
  main_splitter.setStretchFactor(0, 1)
86
- main_splitter.setStretchFactor(1, 3)
126
+ main_splitter.setStretchFactor(1, 4)
87
127
 
88
128
  layout.addWidget(main_splitter)
89
129
 
@@ -94,14 +134,10 @@ class MainWindow(QMainWindow):
94
134
  # File menu
95
135
  file_menu = menubar.addMenu("&File")
96
136
 
97
- connect_action = QAction("&Connect to Database...", self)
98
- connect_action.setShortcut("Ctrl+O")
99
- connect_action.triggered.connect(self._on_connect)
100
- file_menu.addAction(connect_action)
101
-
102
- disconnect_action = QAction("&Disconnect", self)
103
- disconnect_action.triggered.connect(self._on_disconnect)
104
- file_menu.addAction(disconnect_action)
137
+ new_connection_action = QAction("&New Connection...", self)
138
+ new_connection_action.setShortcut("Ctrl+N")
139
+ new_connection_action.triggered.connect(self._new_connection_from_profile)
140
+ file_menu.addAction(new_connection_action)
105
141
 
106
142
  file_menu.addSeparator()
107
143
 
@@ -110,25 +146,38 @@ class MainWindow(QMainWindow):
110
146
  exit_action.triggered.connect(self.close)
111
147
  file_menu.addAction(exit_action)
112
148
 
113
- # Collection menu
114
- collection_menu = menubar.addMenu("&Collection")
149
+ # Connection menu
150
+ connection_menu = menubar.addMenu("&Connection")
151
+
152
+ new_profile_action = QAction("New &Profile...", self)
153
+ new_profile_action.triggered.connect(self._show_profile_editor)
154
+ connection_menu.addAction(new_profile_action)
115
155
 
116
- new_collection_action = QAction("&New Collection...", self)
117
- new_collection_action.setShortcut("Ctrl+N")
118
- new_collection_action.triggered.connect(self._on_new_collection)
119
- collection_menu.addAction(new_collection_action)
156
+ connection_menu.addSeparator()
120
157
 
121
158
  refresh_action = QAction("&Refresh Collections", self)
122
159
  refresh_action.setShortcut("F5")
123
- refresh_action.triggered.connect(self._on_refresh_collections)
124
- collection_menu.addAction(refresh_action)
160
+ refresh_action.triggered.connect(self._refresh_active_connection)
161
+ connection_menu.addAction(refresh_action)
125
162
 
126
- collection_menu.addSeparator()
163
+ connection_menu.addSeparator()
127
164
 
128
165
  backup_action = QAction("&Backup/Restore...", self)
129
- backup_action.setShortcut("Ctrl+B")
130
- backup_action.triggered.connect(self._on_backup_restore)
131
- collection_menu.addAction(backup_action)
166
+ backup_action.triggered.connect(self._show_backup_restore_dialog)
167
+ connection_menu.addAction(backup_action)
168
+
169
+ migrate_action = QAction("&Migrate Data...", self)
170
+ migrate_action.triggered.connect(self._show_migration_dialog)
171
+ connection_menu.addAction(migrate_action)
172
+
173
+ # View menu
174
+ view_menu = menubar.addMenu("&View")
175
+
176
+ self.cache_action = QAction("Enable &Caching", self)
177
+ self.cache_action.setCheckable(True)
178
+ self.cache_action.setChecked(self.settings_service.get_cache_enabled())
179
+ self.cache_action.triggered.connect(self._toggle_cache)
180
+ view_menu.addAction(self.cache_action)
132
181
 
133
182
  # Help menu
134
183
  help_menu = menubar.addMenu("&Help")
@@ -143,189 +192,388 @@ class MainWindow(QMainWindow):
143
192
  toolbar.setMovable(False)
144
193
  self.addToolBar(toolbar)
145
194
 
146
- connect_action = QAction("Connect", self)
147
- connect_action.triggered.connect(self._on_connect)
148
- toolbar.addAction(connect_action)
149
-
150
- disconnect_action = QAction("Disconnect", self)
151
- disconnect_action.triggered.connect(self._on_disconnect)
152
- toolbar.addAction(disconnect_action)
153
- toolbar.addSeparator()
154
-
155
- backup_action = QAction("Backup/Restore", self)
156
- backup_action.triggered.connect(self._on_backup_restore)
157
- toolbar.addAction(backup_action)
158
-
195
+ new_connection_action = QAction("New Connection", self)
196
+ new_connection_action.triggered.connect(self._new_connection_from_profile)
197
+ toolbar.addAction(new_connection_action)
159
198
 
160
199
  toolbar.addSeparator()
161
200
 
162
201
  refresh_action = QAction("Refresh", self)
163
- refresh_action.triggered.connect(self._on_refresh_collections)
202
+ refresh_action.triggered.connect(self._refresh_active_connection)
164
203
  toolbar.addAction(refresh_action)
165
204
 
166
205
  def _setup_statusbar(self):
167
- """Setup status bar."""
168
- self.statusBar = QStatusBar()
169
- self.setStatusBar(self.statusBar)
170
- self.statusBar.showMessage("Not connected")
206
+ """Setup status bar with connection breadcrumb."""
207
+ status_bar = QStatusBar()
208
+ self.setStatusBar(status_bar)
209
+
210
+ # Breadcrumb label
211
+ self.breadcrumb_label = QLabel("No active connection")
212
+ self.statusBar().addPermanentWidget(self.breadcrumb_label)
213
+
214
+ self.statusBar().showMessage("Ready")
171
215
 
172
216
  def _connect_signals(self):
173
217
  """Connect signals between components."""
174
- self.connection_view.connection_changed.connect(self._on_connection_status_changed)
175
- self.connection_view.connection_created.connect(self._on_connection_created)
176
- self.collection_browser.collection_selected.connect(self._on_collection_selected)
218
+ # Connection manager signals
219
+ self.connection_manager.active_connection_changed.connect(self._on_active_connection_changed)
220
+ self.connection_manager.active_collection_changed.connect(self._on_active_collection_changed)
221
+ self.connection_manager.collections_updated.connect(self._on_collections_updated)
222
+
223
+ # Connection panel signals
224
+ self.connection_panel.collection_selected.connect(self._on_collection_selected_from_panel)
225
+ self.connection_panel.add_connection_btn.clicked.connect(self._new_connection_from_profile)
226
+
227
+ # Profile panel signals
228
+ self.profile_panel.connect_profile.connect(self._connect_to_profile)
177
229
 
178
230
  def _on_tab_changed(self, index: int):
179
231
  """Handle tab change - lazy load visualization tab."""
180
232
  if index == 3 and self.visualization_view is None:
181
233
  # Lazy load visualization view
182
234
  from vector_inspector.ui.views.visualization_view import VisualizationView
183
- self.visualization_view = VisualizationView(self.connection)
235
+
236
+ # Get active connection
237
+ active = self.connection_manager.get_active_connection()
238
+ conn = active.connection if active else None
239
+
240
+ self.visualization_view = VisualizationView(conn)
184
241
  # Replace placeholder with actual view
185
242
  self.tab_widget.removeTab(3)
186
243
  self.tab_widget.insertTab(3, self.visualization_view, "Visualization")
187
244
  self.tab_widget.setCurrentIndex(3)
245
+
188
246
  # Set collection if one is already selected
189
- if self.current_collection:
190
- self.visualization_view.set_collection(self.current_collection)
247
+ if active and active.active_collection:
248
+ self.visualization_view.set_collection(active.active_collection)
191
249
 
192
- def _on_connection_created(self, new_connection: VectorDBConnection):
193
- """Handle when a new connection instance is created."""
194
- self.connection = new_connection
195
- # Update all views with new connection
196
- self.collection_browser.connection = new_connection
197
- self.info_panel.connection = new_connection
198
- self.metadata_view.connection = new_connection
199
- self.search_view.connection = new_connection
200
- # Only update visualization if it's been created
201
- if self.visualization_view is not None:
202
- self.visualization_view.connection = new_connection
203
- # Refresh the collection browser to show new database's collections
204
- if new_connection.is_connected:
205
- self.collection_browser.refresh()
206
-
207
- def _on_connect(self):
208
- """Handle connect action."""
209
- self.connection_view.show_connection_dialog()
210
-
211
- def _on_disconnect(self):
212
- """Handle disconnect action."""
213
- if self.connection.is_connected:
214
- self.connection.disconnect()
215
- self.statusBar.showMessage("Disconnected")
216
- self.connection_changed.emit(False)
217
- self.collection_browser.clear()
218
- # Clear info panel
219
- self.info_panel.refresh_database_info()
220
- else:
221
- # Always clear collection browser on disconnect
222
- self.collection_browser.clear()
223
-
224
- def _on_connection_status_changed(self, connected: bool):
225
- """Handle connection status change."""
226
- if connected:
227
- self.statusBar.showMessage("Connected")
228
- self.connection_changed.emit(True)
229
- self._on_refresh_collections()
230
- # Refresh info panel with new connection
231
- self.info_panel.refresh_database_info()
250
+ def _on_active_connection_changed(self, connection_id):
251
+ """Handle active connection change."""
252
+ if connection_id:
253
+ instance = self.connection_manager.get_connection(connection_id)
254
+ if instance:
255
+ # Update breadcrumb
256
+ self.breadcrumb_label.setText(instance.get_breadcrumb())
257
+
258
+ # Update all views with new connection
259
+ self._update_views_with_connection(instance.connection)
260
+
261
+ # If there's an active collection, update views with it
262
+ if instance.active_collection:
263
+ self._update_views_for_collection(instance.active_collection)
264
+ else:
265
+ self.breadcrumb_label.setText("No active connection")
266
+ self._update_views_with_connection(None)
232
267
  else:
233
- self.statusBar.showMessage("Connection failed")
234
- self.connection_changed.emit(False)
235
- # Clear info panel and collection browser
236
- self.info_panel.refresh_database_info()
237
- self.collection_browser.clear()
268
+ self.breadcrumb_label.setText("No active connection")
269
+ self._update_views_with_connection(None)
270
+
271
+ def _on_active_collection_changed(self, connection_id: str, collection_name):
272
+ """Handle active collection change."""
273
+ instance = self.connection_manager.get_connection(connection_id)
274
+ if instance:
275
+ # Update breadcrumb
276
+ self.breadcrumb_label.setText(instance.get_breadcrumb())
238
277
 
239
- def _on_collection_selected(self, collection_name: str):
240
- """Handle collection selection."""
241
- self.current_collection = collection_name
242
- self.statusBar.showMessage(f"Collection: {collection_name}")
243
-
244
- # Update all views with new collection
245
- self.info_panel.set_collection(collection_name)
246
- self.metadata_view.set_collection(collection_name)
247
- self.search_view.set_collection(collection_name)
248
- # Only update visualization if it's been created
278
+ # Update views if this is the active connection
279
+ if connection_id == self.connection_manager.get_active_connection_id():
280
+ if collection_name:
281
+ self._update_views_for_collection(collection_name)
282
+ else:
283
+ # Clear collection from views
284
+ self._update_views_for_collection(None)
285
+
286
+ def _on_collections_updated(self, connection_id: str, collections: list):
287
+ """Handle collections list updated."""
288
+ # UI automatically updates via connection_manager_panel
289
+ pass
290
+
291
+ def _on_collection_selected_from_panel(self, connection_id: str, collection_name: str):
292
+ """Handle collection selection from connection panel."""
293
+ # Show loading dialog while switching collections
294
+ self.loading_dialog.show_loading(f"Loading collection '{collection_name}'...")
295
+ QApplication.processEvents()
296
+
297
+ try:
298
+ # The connection manager already handled setting active collection
299
+ # Just update the views
300
+ self._update_views_for_collection(collection_name)
301
+ finally:
302
+ self.loading_dialog.hide_loading()
303
+
304
+ def _update_views_with_connection(self, connection: VectorDBConnection):
305
+ """Update all views with a new connection."""
306
+ # Clear current collection when switching connections
307
+ self.info_panel.current_collection = None
308
+ self.metadata_view.current_collection = None
309
+ self.search_view.current_collection = None
310
+ if self.visualization_view is not None:
311
+ self.visualization_view.current_collection = None
312
+
313
+ # Update connection references
314
+ self.info_panel.connection = connection
315
+ self.metadata_view.connection = connection
316
+ self.search_view.connection = connection
317
+
249
318
  if self.visualization_view is not None:
250
- self.visualization_view.set_collection(collection_name)
319
+ self.visualization_view.connection = connection
251
320
 
252
- def _on_refresh_collections(self):
253
- """Refresh collection list."""
254
- if self.connection.is_connected:
255
- self.collection_browser.refresh()
256
- # Also refresh database info (collection count may have changed)
321
+ # Refresh info panel (will show no collection selected)
322
+ if connection:
257
323
  self.info_panel.refresh_database_info()
324
+
325
+ def _update_views_for_collection(self, collection_name: str):
326
+ """Update all views with the selected collection."""
327
+ if collection_name:
328
+ # Get active connection ID to use as database identifier
329
+ active = self.connection_manager.get_active_connection()
330
+ database_name = active.id if active else ""
331
+
332
+ self.info_panel.set_collection(collection_name, database_name)
333
+ self.metadata_view.set_collection(collection_name, database_name)
334
+ self.search_view.set_collection(collection_name, database_name)
258
335
 
259
- def _on_new_collection(self):
260
- """Create a new collection."""
261
- if not self.connection.is_connected:
262
- QMessageBox.warning(self, "Not Connected", "Please connect to a database first.")
336
+ if self.visualization_view is not None:
337
+ self.visualization_view.set_collection(collection_name)
338
+
339
+ def _new_connection_from_profile(self):
340
+ """Show dialog to create new connection (switches to Profiles tab)."""
341
+ self.left_tabs.setCurrentIndex(1) # Switch to Profiles tab
342
+ QMessageBox.information(
343
+ self,
344
+ "Connect to Profile",
345
+ "Select a profile from the list and click 'Connect', or click '+' to create a new profile."
346
+ )
347
+
348
+ def _show_profile_editor(self):
349
+ """Show profile editor to create new profile."""
350
+ self.left_tabs.setCurrentIndex(1) # Switch to Profiles tab
351
+ self.profile_panel._create_profile()
352
+
353
+ def _connect_to_profile(self, profile_id: str):
354
+ """Connect to a profile."""
355
+ profile_data = self.profile_service.get_profile_with_credentials(profile_id)
356
+ if not profile_data:
357
+ QMessageBox.warning(self, "Error", "Profile not found.")
263
358
  return
264
359
 
265
- from vector_inspector.core.connections.chroma_connection import ChromaDBConnection
266
- from vector_inspector.core.connections.qdrant_connection import QdrantConnection
267
-
268
- name, ok = QInputDialog.getText(
269
- self, "New Collection", "Enter collection name:"
270
- )
360
+ # Check connection limit
361
+ if self.connection_manager.get_connection_count() >= ConnectionManager.MAX_CONNECTIONS:
362
+ QMessageBox.warning(
363
+ self,
364
+ "Connection Limit",
365
+ f"Maximum number of connections ({ConnectionManager.MAX_CONNECTIONS}) reached. "
366
+ "Please close a connection first."
367
+ )
368
+ return
271
369
 
272
- if ok and name:
273
- success = False
370
+ # Create connection
371
+ provider = profile_data["provider"]
372
+ config = profile_data["config"]
373
+ credentials = profile_data.get("credentials", {})
374
+
375
+ try:
376
+ # Create connection object
377
+ if provider == "chromadb":
378
+ connection = self._create_chroma_connection(config, credentials)
379
+ elif provider == "qdrant":
380
+ connection = self._create_qdrant_connection(config, credentials)
381
+ else:
382
+ QMessageBox.warning(self, "Error", f"Unsupported provider: {provider}")
383
+ return
274
384
 
275
- # Handle ChromaDB
276
- if isinstance(self.connection, ChromaDBConnection):
277
- collection = self.connection.get_collection(name)
278
- success = collection is not None
385
+ # Register with connection manager
386
+ connection_id = self.connection_manager.create_connection(
387
+ name=profile_data["name"],
388
+ provider=provider,
389
+ connection=connection,
390
+ config=config
391
+ )
279
392
 
280
- # Handle Qdrant
281
- elif isinstance(self.connection, QdrantConnection):
282
- # Ask for vector size (required for Qdrant)
283
- vector_size, ok = QInputDialog.getInt(
284
- self,
285
- "Vector Size",
286
- "Enter vector dimension size:",
287
- value=384, # Default for sentence transformers
288
- min=1,
289
- max=10000
290
- )
291
- if ok:
292
- success = self.connection.create_collection(name, vector_size)
393
+ # Update state to connecting
394
+ self.connection_manager.update_connection_state(connection_id, ConnectionState.CONNECTING)
293
395
 
294
- if success:
295
- QMessageBox.information(
296
- self, "Success", f"Collection '{name}' created successfully."
297
- )
298
- self._on_refresh_collections()
299
- else:
300
- QMessageBox.warning(
301
- self, "Error", f"Failed to create collection '{name}'."
396
+ # Connect in background thread
397
+ thread = ConnectionThread(connection)
398
+ thread.finished.connect(
399
+ lambda success, collections, error: self._on_connection_finished(
400
+ connection_id, success, collections, error
302
401
  )
402
+ )
403
+ self._connection_threads[connection_id] = thread
404
+ thread.start()
405
+
406
+ # Show loading dialog
407
+ self.loading_dialog.show_loading(f"Connecting to {profile_data['name']}...")
408
+
409
+ except Exception as e:
410
+ QMessageBox.critical(self, "Connection Error", f"Failed to create connection: {e}")
303
411
 
304
- def _on_backup_restore(self):
305
- """Open backup/restore dialog."""
306
- if not self.connection.is_connected:
307
- QMessageBox.warning(self, "Not Connected", "Please connect to a database first.")
308
- return
412
+ def _create_chroma_connection(self, config: dict, credentials: dict) -> ChromaDBConnection:
413
+ """Create a ChromaDB connection."""
414
+ conn_type = config.get("type")
415
+
416
+ if conn_type == "persistent":
417
+ return ChromaDBConnection(path=config.get("path"))
418
+ elif conn_type == "http":
419
+ return ChromaDBConnection(
420
+ host=config.get("host"),
421
+ port=config.get("port")
422
+ )
423
+ else: # ephemeral
424
+ return ChromaDBConnection()
425
+
426
+ def _create_qdrant_connection(self, config: dict, credentials: dict) -> QdrantConnection:
427
+ """Create a Qdrant connection."""
428
+ conn_type = config.get("type")
429
+ api_key = credentials.get("api_key")
430
+
431
+ if conn_type == "persistent":
432
+ return QdrantConnection(path=config.get("path"))
433
+ elif conn_type == "http":
434
+ return QdrantConnection(
435
+ host=config.get("host"),
436
+ port=config.get("port"),
437
+ api_key=api_key
438
+ )
439
+ else: # ephemeral
440
+ return QdrantConnection()
441
+
442
+ def _on_connection_finished(self, connection_id: str, success: bool, collections: list, error: str):
443
+ """Handle connection thread completion."""
444
+ self.loading_dialog.hide_loading()
445
+
446
+ # Clean up thread
447
+ thread = self._connection_threads.pop(connection_id, None)
448
+ if thread:
449
+ thread.wait() # Wait for thread to fully finish
450
+ thread.deleteLater()
451
+
452
+ if success:
453
+ # Update state to connected
454
+ self.connection_manager.update_connection_state(connection_id, ConnectionState.CONNECTED)
309
455
 
310
- dialog = BackupRestoreDialog(
311
- self.connection,
312
- self.current_collection,
313
- self
314
- )
315
- dialog.exec()
456
+ # Mark connection as opened first (will show in UI)
457
+ self.connection_manager.mark_connection_opened(connection_id)
458
+
459
+ # Then update collections (UI item now exists to receive them)
460
+ self.connection_manager.update_collections(connection_id, collections)
461
+
462
+ # Switch to Active connections tab
463
+ self.left_tabs.setCurrentIndex(0)
464
+
465
+ self.statusBar().showMessage(f"Connected successfully ({len(collections)} collections)", 5000)
466
+ else:
467
+ # Update state to error
468
+ self.connection_manager.update_connection_state(connection_id, ConnectionState.ERROR, error)
469
+
470
+ QMessageBox.warning(self, "Connection Failed", f"Failed to connect: {error}")
471
+
472
+ # Remove the failed connection
473
+ self.connection_manager.close_connection(connection_id)
474
+
475
+ def _refresh_active_connection(self):
476
+ """Refresh collections for the active connection."""
477
+ active = self.connection_manager.get_active_connection()
478
+ if not active or not active.connection.is_connected:
479
+ QMessageBox.information(self, "No Connection", "No active connection to refresh.")
480
+ return
316
481
 
317
- # Refresh collections after dialog closes (in case something was restored)
318
- self._on_refresh_collections()
319
-
482
+ try:
483
+ collections = active.connection.list_collections()
484
+ self.connection_manager.update_collections(active.id, collections)
485
+ self.statusBar().showMessage(f"Refreshed collections ({len(collections)} found)", 3000)
486
+
487
+ # Also refresh info panel
488
+ self.info_panel.refresh_database_info()
489
+ except Exception as e:
490
+ QMessageBox.warning(self, "Refresh Failed", f"Failed to refresh collections: {e}")
491
+
492
+ def _restore_session(self):
493
+ """Restore previously active connections on startup."""
494
+ # TODO: Implement session restore
495
+ # For now, we'll just show a message if there are saved profiles
496
+ profiles = self.profile_service.get_all_profiles()
497
+ if profiles:
498
+ self.statusBar().showMessage(
499
+ f"{len(profiles)} saved profile(s) available. Switch to Profiles tab to connect.",
500
+ 10000
501
+ )
502
+
320
503
  def _show_about(self):
321
504
  """Show about dialog."""
322
505
  QMessageBox.about(
323
506
  self,
324
507
  "About Vector Inspector",
325
- "<h2>Vector Inspector 0.1.0</h2>"
508
+ "<h2>Vector Inspector 0.3.0</h2>"
326
509
  "<p>A comprehensive desktop application for visualizing, "
327
- "querying, and managing vector database data.</p>"
510
+ "querying, and managing multiple vector databases simultaneously.</p>"
328
511
  '<p><a href="https://github.com/anthonypdawson/vector-inspector" style="color:#2980b9;">GitHub Project Page</a></p>'
329
512
  "<hr />"
330
- "<p>Built with PySide6 and ChromaDB</p>"
513
+ "<p>Built with PySide6, ChromaDB, and Qdrant</p>"
514
+ "<p><b>New:</b> Multi-database support with saved connection profiles</p>"
331
515
  )
516
+
517
+ def _toggle_cache(self, checked: bool):
518
+ """Toggle caching on/off."""
519
+ self.settings_service.set_cache_enabled(checked)
520
+ status = "enabled" if checked else "disabled"
521
+ self.statusBar().showMessage(f"Caching {status}", 3000)
522
+
523
+ def _show_migration_dialog(self):
524
+ """Show cross-database migration dialog."""
525
+ if self.connection_manager.get_connection_count() < 2:
526
+ QMessageBox.information(
527
+ self,
528
+ "Insufficient Connections",
529
+ "You need at least 2 active connections to migrate data.\n"
530
+ "Please connect to additional databases first."
531
+ )
532
+ return
533
+
534
+ from vector_inspector.ui.dialogs.cross_db_migration import CrossDatabaseMigrationDialog
535
+ dialog = CrossDatabaseMigrationDialog(self.connection_manager, self)
536
+ dialog.exec()
537
+
538
+ def _show_backup_restore_dialog(self):
539
+ """Show backup/restore dialog for the active collection."""
540
+ # Check if there's an active connection
541
+ connection = self.connection_manager.get_active_connection()
542
+ if not connection:
543
+ QMessageBox.information(
544
+ self,
545
+ "No Connection",
546
+ "Please connect to a database first."
547
+ )
548
+ return
549
+
550
+ # Get active collection
551
+ collection_name = self.connection_manager.get_active_collection()
552
+ if not collection_name:
553
+ # Allow opening dialog without a collection selected (for restore-only)
554
+ QMessageBox.information(
555
+ self,
556
+ "No Collection Selected",
557
+ "You can restore backups without a collection selected.\n"
558
+ "To create a backup, please select a collection first."
559
+ )
560
+
561
+ from vector_inspector.ui.components.backup_restore_dialog import BackupRestoreDialog
562
+ dialog = BackupRestoreDialog(connection, collection_name or "", self)
563
+ if dialog.exec() == QDialog.Accepted:
564
+ # Refresh collections after restore
565
+ self._refresh_active_connection()
566
+
567
+ def closeEvent(self, event):
568
+ """Handle application close."""
569
+ # Wait for all connection threads to finish
570
+ for thread in list(self._connection_threads.values()):
571
+ if thread.isRunning():
572
+ thread.quit()
573
+ thread.wait(1000) # Wait up to 1 second
574
+
575
+ # Close all connections
576
+ self.connection_manager.close_all_connections()
577
+
578
+ event.accept()
579
+