vector-inspector 0.3.7__py3-none-any.whl → 0.3.9__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.
- vector_inspector/core/connections/pgvector_connection.py +26 -1
- vector_inspector/core/provider_factory.py +97 -0
- vector_inspector/extensions/__init__.py +105 -0
- vector_inspector/services/settings_service.py +76 -0
- vector_inspector/ui/components/splash_window.py +14 -2
- vector_inspector/ui/controllers/__init__.py +1 -0
- vector_inspector/ui/controllers/connection_controller.py +177 -0
- vector_inspector/ui/dialogs/settings_dialog.py +124 -0
- vector_inspector/ui/main_window.py +222 -332
- vector_inspector/ui/main_window_shell.py +106 -0
- vector_inspector/ui/services/__init__.py +1 -0
- vector_inspector/ui/services/dialog_service.py +113 -0
- vector_inspector/ui/tabs.py +64 -0
- vector_inspector/ui/views/metadata_view.py +45 -0
- vector_inspector/ui/views/search_view.py +146 -13
- {vector_inspector-0.3.7.dist-info → vector_inspector-0.3.9.dist-info}/METADATA +38 -9
- {vector_inspector-0.3.7.dist-info → vector_inspector-0.3.9.dist-info}/RECORD +19 -10
- {vector_inspector-0.3.7.dist-info → vector_inspector-0.3.9.dist-info}/WHEEL +0 -0
- {vector_inspector-0.3.7.dist-info → vector_inspector-0.3.9.dist-info}/entry_points.txt +0 -0
|
@@ -1,63 +1,29 @@
|
|
|
1
1
|
"""Updated main window with multi-database support."""
|
|
2
2
|
|
|
3
3
|
from PySide6.QtWidgets import (
|
|
4
|
-
QMainWindow,
|
|
5
|
-
QWidget,
|
|
6
|
-
QVBoxLayout,
|
|
7
|
-
QHBoxLayout,
|
|
8
|
-
QSplitter,
|
|
9
|
-
QTabWidget,
|
|
10
|
-
QStatusBar,
|
|
11
|
-
QToolBar,
|
|
12
4
|
QMessageBox,
|
|
13
|
-
QInputDialog,
|
|
14
5
|
QLabel,
|
|
15
|
-
QDockWidget,
|
|
16
6
|
QApplication,
|
|
17
7
|
QDialog,
|
|
8
|
+
QToolBar,
|
|
9
|
+
QStatusBar,
|
|
18
10
|
)
|
|
19
|
-
from PySide6.QtCore import Qt,
|
|
11
|
+
from PySide6.QtCore import Qt, QTimer, QByteArray
|
|
20
12
|
from PySide6.QtGui import QAction
|
|
21
13
|
|
|
22
|
-
from vector_inspector.core.connection_manager import ConnectionManager
|
|
14
|
+
from vector_inspector.core.connection_manager import ConnectionManager
|
|
23
15
|
from vector_inspector.core.connections.base_connection import VectorDBConnection
|
|
24
|
-
from vector_inspector.core.connections.chroma_connection import ChromaDBConnection
|
|
25
|
-
from vector_inspector.core.connections.qdrant_connection import QdrantConnection
|
|
26
|
-
from vector_inspector.core.connections.pinecone_connection import PineconeConnection
|
|
27
|
-
from vector_inspector.core.connections.pgvector_connection import PgVectorConnection
|
|
28
16
|
from vector_inspector.services.profile_service import ProfileService
|
|
29
17
|
from vector_inspector.services.settings_service import SettingsService
|
|
18
|
+
from vector_inspector.ui.main_window_shell import InspectorShell
|
|
30
19
|
from vector_inspector.ui.components.connection_manager_panel import ConnectionManagerPanel
|
|
31
20
|
from vector_inspector.ui.components.profile_manager_panel import ProfileManagerPanel
|
|
32
|
-
from vector_inspector.ui.
|
|
33
|
-
from vector_inspector.ui.
|
|
34
|
-
from vector_inspector.ui.
|
|
35
|
-
from vector_inspector.ui.components.loading_dialog import LoadingDialog
|
|
21
|
+
from vector_inspector.ui.tabs import InspectorTabs
|
|
22
|
+
from vector_inspector.ui.controllers.connection_controller import ConnectionController
|
|
23
|
+
from vector_inspector.ui.services.dialog_service import DialogService
|
|
36
24
|
|
|
37
25
|
|
|
38
|
-
class
|
|
39
|
-
"""Background thread for connecting to database."""
|
|
40
|
-
|
|
41
|
-
finished = Signal(bool, list, str) # success, collections, error_message
|
|
42
|
-
|
|
43
|
-
def __init__(self, connection):
|
|
44
|
-
super().__init__()
|
|
45
|
-
self.connection = connection
|
|
46
|
-
|
|
47
|
-
def run(self):
|
|
48
|
-
"""Connect to database and get collections."""
|
|
49
|
-
try:
|
|
50
|
-
success = self.connection.connect()
|
|
51
|
-
if success:
|
|
52
|
-
collections = self.connection.list_collections()
|
|
53
|
-
self.finished.emit(True, collections, "")
|
|
54
|
-
else:
|
|
55
|
-
self.finished.emit(False, [], "Connection failed")
|
|
56
|
-
except Exception as e:
|
|
57
|
-
self.finished.emit(False, [], str(e))
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
class MainWindow(QMainWindow):
|
|
26
|
+
class MainWindow(InspectorShell):
|
|
61
27
|
"""Main application window with multi-database support."""
|
|
62
28
|
|
|
63
29
|
def __init__(self):
|
|
@@ -67,11 +33,21 @@ class MainWindow(QMainWindow):
|
|
|
67
33
|
self.connection_manager = ConnectionManager()
|
|
68
34
|
self.profile_service = ProfileService()
|
|
69
35
|
self.settings_service = SettingsService()
|
|
70
|
-
|
|
36
|
+
|
|
37
|
+
# Controller for connection operations
|
|
38
|
+
self.connection_controller = ConnectionController(
|
|
39
|
+
self.connection_manager, self.profile_service, self
|
|
40
|
+
)
|
|
71
41
|
|
|
72
42
|
# State
|
|
73
43
|
self.visualization_view = None
|
|
74
|
-
|
|
44
|
+
|
|
45
|
+
# View references (will be set in _setup_ui)
|
|
46
|
+
self.info_panel = None
|
|
47
|
+
self.metadata_view = None
|
|
48
|
+
self.search_view = None
|
|
49
|
+
self.connection_panel = None
|
|
50
|
+
self.profile_panel = None
|
|
75
51
|
|
|
76
52
|
self.setWindowTitle("Vector Inspector")
|
|
77
53
|
self.setGeometry(100, 100, 1600, 900)
|
|
@@ -82,6 +58,29 @@ class MainWindow(QMainWindow):
|
|
|
82
58
|
self._setup_statusbar()
|
|
83
59
|
self._connect_signals()
|
|
84
60
|
self._restore_session()
|
|
61
|
+
# Listen for settings changes so updates apply immediately
|
|
62
|
+
try:
|
|
63
|
+
self.settings_service.signals.setting_changed.connect(self._on_setting_changed)
|
|
64
|
+
except Exception:
|
|
65
|
+
pass
|
|
66
|
+
# Restore window geometry if present
|
|
67
|
+
try:
|
|
68
|
+
geom = self.settings_service.get_window_geometry()
|
|
69
|
+
if geom and self.settings_service.get_window_restore_geometry():
|
|
70
|
+
try:
|
|
71
|
+
# restoreGeometry accepts QByteArray; wrap bytes accordingly
|
|
72
|
+
if isinstance(geom, (bytes, bytearray)):
|
|
73
|
+
self.restoreGeometry(QByteArray(geom))
|
|
74
|
+
else:
|
|
75
|
+
self.restoreGeometry(geom)
|
|
76
|
+
except Exception:
|
|
77
|
+
# fallback: try passing raw bytes
|
|
78
|
+
try:
|
|
79
|
+
self.restoreGeometry(geom)
|
|
80
|
+
except Exception:
|
|
81
|
+
pass
|
|
82
|
+
except Exception:
|
|
83
|
+
pass
|
|
85
84
|
# Show splash after main window is visible
|
|
86
85
|
QTimer.singleShot(0, self._maybe_show_splash)
|
|
87
86
|
|
|
@@ -101,62 +100,36 @@ class MainWindow(QMainWindow):
|
|
|
101
100
|
print(f"[SplashWindow] Failed to show splash: {e}")
|
|
102
101
|
|
|
103
102
|
def _setup_ui(self):
|
|
104
|
-
"""Setup the main UI layout."""
|
|
105
|
-
#
|
|
106
|
-
central_widget = QWidget()
|
|
107
|
-
self.setCentralWidget(central_widget)
|
|
108
|
-
|
|
109
|
-
layout = QHBoxLayout(central_widget)
|
|
110
|
-
layout.setContentsMargins(5, 5, 5, 5)
|
|
111
|
-
|
|
112
|
-
# Main splitter (left panel | right tabs)
|
|
113
|
-
main_splitter = QSplitter(Qt.Horizontal)
|
|
114
|
-
|
|
115
|
-
# Left panel - Connections and Profiles
|
|
116
|
-
left_panel = QWidget()
|
|
117
|
-
left_layout = QVBoxLayout(left_panel)
|
|
118
|
-
left_layout.setContentsMargins(0, 0, 0, 0)
|
|
119
|
-
|
|
120
|
-
# Create tab widget for connections and profiles
|
|
121
|
-
self.left_tabs = QTabWidget()
|
|
122
|
-
|
|
123
|
-
# Connection manager panel
|
|
103
|
+
"""Setup the main UI layout using InspectorShell."""
|
|
104
|
+
# Left panels - Connections and Profiles
|
|
124
105
|
self.connection_panel = ConnectionManagerPanel(self.connection_manager)
|
|
125
|
-
self.
|
|
106
|
+
self.add_left_panel(self.connection_panel, "Active")
|
|
126
107
|
|
|
127
|
-
# Profile manager panel
|
|
128
108
|
self.profile_panel = ProfileManagerPanel(self.profile_service)
|
|
129
|
-
self.
|
|
109
|
+
self.add_left_panel(self.profile_panel, "Profiles")
|
|
130
110
|
|
|
131
|
-
|
|
111
|
+
# Main content tabs using TabRegistry
|
|
112
|
+
tab_defs = InspectorTabs.get_standard_tabs()
|
|
132
113
|
|
|
133
|
-
|
|
134
|
-
|
|
114
|
+
for i, tab_def in enumerate(tab_defs):
|
|
115
|
+
widget = InspectorTabs.create_tab_widget(tab_def, connection=None)
|
|
116
|
+
self.add_main_tab(widget, tab_def.title)
|
|
135
117
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
self.tab_widget.addTab(QWidget(), "Visualization") # Placeholder
|
|
118
|
+
# Store references to views (except placeholder)
|
|
119
|
+
if i == InspectorTabs.INFO_TAB:
|
|
120
|
+
self.info_panel = widget
|
|
121
|
+
elif i == InspectorTabs.DATA_TAB:
|
|
122
|
+
self.metadata_view = widget
|
|
123
|
+
elif i == InspectorTabs.SEARCH_TAB:
|
|
124
|
+
self.search_view = widget
|
|
125
|
+
# Visualization is lazy-loaded, so it's a placeholder for now
|
|
145
126
|
|
|
146
127
|
# Set Info tab as default
|
|
147
|
-
self.
|
|
128
|
+
self.set_main_tab_active(InspectorTabs.INFO_TAB)
|
|
148
129
|
|
|
149
130
|
# Connect to tab change to lazy load visualization
|
|
150
131
|
self.tab_widget.currentChanged.connect(self._on_tab_changed)
|
|
151
132
|
|
|
152
|
-
# Add panels to splitter
|
|
153
|
-
main_splitter.addWidget(left_panel)
|
|
154
|
-
main_splitter.addWidget(self.tab_widget)
|
|
155
|
-
main_splitter.setStretchFactor(0, 1)
|
|
156
|
-
main_splitter.setStretchFactor(1, 4)
|
|
157
|
-
|
|
158
|
-
layout.addWidget(main_splitter)
|
|
159
|
-
|
|
160
133
|
def _setup_menu_bar(self):
|
|
161
134
|
"""Setup application menu bar."""
|
|
162
135
|
menubar = self.menuBar()
|
|
@@ -171,6 +144,13 @@ class MainWindow(QMainWindow):
|
|
|
171
144
|
|
|
172
145
|
file_menu.addSeparator()
|
|
173
146
|
|
|
147
|
+
prefs_action = QAction("Preferences...", self)
|
|
148
|
+
prefs_action.setShortcut("Ctrl+,")
|
|
149
|
+
prefs_action.triggered.connect(self._show_preferences_dialog)
|
|
150
|
+
file_menu.addAction(prefs_action)
|
|
151
|
+
|
|
152
|
+
file_menu.addSeparator()
|
|
153
|
+
|
|
174
154
|
exit_action = QAction("E&xit", self)
|
|
175
155
|
exit_action.setShortcut("Ctrl+Q")
|
|
176
156
|
exit_action.triggered.connect(self.close)
|
|
@@ -299,21 +279,67 @@ class MainWindow(QMainWindow):
|
|
|
299
279
|
|
|
300
280
|
threading.Thread(target=check_updates, daemon=True).start()
|
|
301
281
|
|
|
282
|
+
def _show_preferences_dialog(self):
|
|
283
|
+
try:
|
|
284
|
+
from vector_inspector.ui.dialogs.settings_dialog import SettingsDialog
|
|
285
|
+
|
|
286
|
+
dlg = SettingsDialog(self.settings_service, self)
|
|
287
|
+
if dlg.exec() == QDialog.Accepted:
|
|
288
|
+
self._apply_settings_to_views()
|
|
289
|
+
except Exception as e:
|
|
290
|
+
print(f"Failed to open preferences: {e}")
|
|
291
|
+
|
|
292
|
+
def _apply_settings_to_views(self):
|
|
293
|
+
"""Apply relevant settings to existing views."""
|
|
294
|
+
try:
|
|
295
|
+
# Breadcrumb visibility
|
|
296
|
+
enabled = self.settings_service.get_breadcrumb_enabled()
|
|
297
|
+
if self.search_view is not None and hasattr(self.search_view, "breadcrumb_label"):
|
|
298
|
+
self.search_view.breadcrumb_label.setVisible(enabled)
|
|
299
|
+
# also set elide mode
|
|
300
|
+
mode = self.settings_service.get_breadcrumb_elide_mode()
|
|
301
|
+
try:
|
|
302
|
+
self.search_view.set_elide_mode(mode)
|
|
303
|
+
except Exception:
|
|
304
|
+
pass
|
|
305
|
+
|
|
306
|
+
# Default results
|
|
307
|
+
default_n = self.settings_service.get_default_n_results()
|
|
308
|
+
if self.search_view is not None and hasattr(self.search_view, "n_results_spin"):
|
|
309
|
+
try:
|
|
310
|
+
self.search_view.n_results_spin.setValue(int(default_n))
|
|
311
|
+
except Exception:
|
|
312
|
+
pass
|
|
313
|
+
|
|
314
|
+
except Exception:
|
|
315
|
+
pass
|
|
316
|
+
|
|
317
|
+
def _on_setting_changed(self, key: str, value: object):
|
|
318
|
+
"""Handle granular setting change events."""
|
|
319
|
+
try:
|
|
320
|
+
if key == "breadcrumb.enabled":
|
|
321
|
+
enabled = bool(value)
|
|
322
|
+
if self.search_view is not None and hasattr(self.search_view, "breadcrumb_label"):
|
|
323
|
+
self.search_view.breadcrumb_label.setVisible(enabled)
|
|
324
|
+
elif key == "breadcrumb.elide_mode":
|
|
325
|
+
mode = str(value)
|
|
326
|
+
if self.search_view is not None and hasattr(self.search_view, "set_elide_mode"):
|
|
327
|
+
self.search_view.set_elide_mode(mode)
|
|
328
|
+
elif key == "search.default_n_results":
|
|
329
|
+
try:
|
|
330
|
+
n = int(value)
|
|
331
|
+
if self.search_view is not None and hasattr(self.search_view, "n_results_spin"):
|
|
332
|
+
self.search_view.n_results_spin.setValue(n)
|
|
333
|
+
except Exception:
|
|
334
|
+
pass
|
|
335
|
+
except Exception:
|
|
336
|
+
pass
|
|
337
|
+
|
|
302
338
|
def _on_update_indicator_clicked(self, event):
|
|
303
339
|
# Show update details dialog
|
|
304
340
|
if not hasattr(self, "_latest_release"):
|
|
305
341
|
return
|
|
306
|
-
|
|
307
|
-
from vector_inspector.services.update_service import UpdateService
|
|
308
|
-
|
|
309
|
-
latest = self._latest_release
|
|
310
|
-
version = latest.get("tag_name", "?")
|
|
311
|
-
notes = latest.get("body", "")
|
|
312
|
-
instructions = UpdateService.get_update_instructions()
|
|
313
|
-
pip_cmd = instructions["pip"]
|
|
314
|
-
github_url = instructions["github"]
|
|
315
|
-
dlg = UpdateDetailsDialog(version, notes, pip_cmd, github_url, self)
|
|
316
|
-
dlg.exec()
|
|
342
|
+
DialogService.show_update_details(self._latest_release, self)
|
|
317
343
|
|
|
318
344
|
def _connect_signals(self):
|
|
319
345
|
"""Connect signals between components."""
|
|
@@ -327,6 +353,9 @@ class MainWindow(QMainWindow):
|
|
|
327
353
|
self.connection_manager.collections_updated.connect(self._on_collections_updated)
|
|
328
354
|
self.connection_manager.connection_opened.connect(self._on_connection_opened)
|
|
329
355
|
|
|
356
|
+
# Connection controller signals
|
|
357
|
+
self.connection_controller.connection_completed.connect(self._on_connection_completed)
|
|
358
|
+
|
|
330
359
|
# Connection panel signals
|
|
331
360
|
self.connection_panel.collection_selected.connect(self._on_collection_selected_from_panel)
|
|
332
361
|
self.connection_panel.add_connection_btn.clicked.connect(self._new_connection_from_profile)
|
|
@@ -334,9 +363,20 @@ class MainWindow(QMainWindow):
|
|
|
334
363
|
# Profile panel signals
|
|
335
364
|
self.profile_panel.connect_profile.connect(self._connect_to_profile)
|
|
336
365
|
|
|
366
|
+
def _on_connection_completed(
|
|
367
|
+
self, connection_id: str, success: bool, collections: list, error: str
|
|
368
|
+
):
|
|
369
|
+
"""Handle connection completed event from controller."""
|
|
370
|
+
if success:
|
|
371
|
+
# Switch to Active connections tab
|
|
372
|
+
self.set_left_panel_active(0)
|
|
373
|
+
self.statusBar().showMessage(
|
|
374
|
+
f"Connected successfully ({len(collections)} collections)", 5000
|
|
375
|
+
)
|
|
376
|
+
|
|
337
377
|
def _on_tab_changed(self, index: int):
|
|
338
378
|
"""Handle tab change - lazy load visualization tab."""
|
|
339
|
-
if index ==
|
|
379
|
+
if index == InspectorTabs.VISUALIZATION_TAB and self.visualization_view is None:
|
|
340
380
|
# Lazy load visualization view
|
|
341
381
|
from vector_inspector.ui.views.visualization_view import VisualizationView
|
|
342
382
|
|
|
@@ -346,9 +386,11 @@ class MainWindow(QMainWindow):
|
|
|
346
386
|
|
|
347
387
|
self.visualization_view = VisualizationView(conn)
|
|
348
388
|
# Replace placeholder with actual view
|
|
349
|
-
self.
|
|
350
|
-
self.
|
|
351
|
-
|
|
389
|
+
self.remove_main_tab(InspectorTabs.VISUALIZATION_TAB)
|
|
390
|
+
self.add_main_tab(
|
|
391
|
+
self.visualization_view, "Visualization", InspectorTabs.VISUALIZATION_TAB
|
|
392
|
+
)
|
|
393
|
+
self.set_main_tab_active(InspectorTabs.VISUALIZATION_TAB)
|
|
352
394
|
|
|
353
395
|
# Set collection if one is already selected
|
|
354
396
|
if active and active.active_collection:
|
|
@@ -386,20 +428,22 @@ class MainWindow(QMainWindow):
|
|
|
386
428
|
if connection_id == self.connection_manager.get_active_connection_id():
|
|
387
429
|
# Show loading immediately when collection changes
|
|
388
430
|
if collection_name:
|
|
389
|
-
self.loading_dialog.show_loading(
|
|
431
|
+
self.connection_controller.loading_dialog.show_loading(
|
|
432
|
+
f"Loading collection '{collection_name}'..."
|
|
433
|
+
)
|
|
390
434
|
QApplication.processEvents()
|
|
391
435
|
try:
|
|
392
436
|
self._update_views_for_collection(collection_name)
|
|
393
437
|
finally:
|
|
394
|
-
self.loading_dialog.hide_loading()
|
|
438
|
+
self.connection_controller.loading_dialog.hide_loading()
|
|
395
439
|
else:
|
|
396
440
|
# Clear collection from views
|
|
397
|
-
self.loading_dialog.show_loading("Clearing collection...")
|
|
441
|
+
self.connection_controller.loading_dialog.show_loading("Clearing collection...")
|
|
398
442
|
QApplication.processEvents()
|
|
399
443
|
try:
|
|
400
444
|
self._update_views_for_collection(None)
|
|
401
445
|
finally:
|
|
402
|
-
self.loading_dialog.hide_loading()
|
|
446
|
+
self.connection_controller.loading_dialog.hide_loading()
|
|
403
447
|
|
|
404
448
|
def _on_collections_updated(self, connection_id: str, collections: list):
|
|
405
449
|
"""Handle collections list updated."""
|
|
@@ -417,7 +461,9 @@ class MainWindow(QMainWindow):
|
|
|
417
461
|
def _on_collection_selected_from_panel(self, connection_id: str, collection_name: str):
|
|
418
462
|
"""Handle collection selection from connection panel."""
|
|
419
463
|
# Show loading dialog while switching collections
|
|
420
|
-
self.loading_dialog.show_loading(
|
|
464
|
+
self.connection_controller.loading_dialog.show_loading(
|
|
465
|
+
f"Loading collection '{collection_name}'..."
|
|
466
|
+
)
|
|
421
467
|
QApplication.processEvents()
|
|
422
468
|
|
|
423
469
|
try:
|
|
@@ -425,7 +471,7 @@ class MainWindow(QMainWindow):
|
|
|
425
471
|
# Just update the views
|
|
426
472
|
self._update_views_for_collection(collection_name)
|
|
427
473
|
finally:
|
|
428
|
-
self.loading_dialog.hide_loading()
|
|
474
|
+
self.connection_controller.loading_dialog.hide_loading()
|
|
429
475
|
|
|
430
476
|
def _update_views_with_connection(self, connection: VectorDBConnection):
|
|
431
477
|
"""Update all views with a new connection."""
|
|
@@ -464,176 +510,20 @@ class MainWindow(QMainWindow):
|
|
|
464
510
|
|
|
465
511
|
def _new_connection_from_profile(self):
|
|
466
512
|
"""Show dialog to create new connection (switches to Profiles tab)."""
|
|
467
|
-
self.
|
|
468
|
-
|
|
469
|
-
self,
|
|
470
|
-
"Connect to Profile",
|
|
471
|
-
"Select a profile from the list and click 'Connect', or click '+' to create a new profile.",
|
|
472
|
-
)
|
|
513
|
+
self.set_left_panel_active(1) # Switch to Profiles tab
|
|
514
|
+
DialogService.show_profile_editor_prompt(self)
|
|
473
515
|
|
|
474
516
|
def _show_profile_editor(self):
|
|
475
517
|
"""Show profile editor to create new profile."""
|
|
476
|
-
self.
|
|
518
|
+
self.set_left_panel_active(1) # Switch to Profiles tab
|
|
477
519
|
self.profile_panel._create_profile()
|
|
478
520
|
|
|
479
521
|
def _connect_to_profile(self, profile_id: str):
|
|
480
|
-
"""Connect to a profile."""
|
|
481
|
-
|
|
482
|
-
if not profile_data:
|
|
483
|
-
QMessageBox.warning(self, "Error", "Profile not found.")
|
|
484
|
-
return
|
|
485
|
-
|
|
486
|
-
# Check connection limit
|
|
487
|
-
if self.connection_manager.get_connection_count() >= ConnectionManager.MAX_CONNECTIONS:
|
|
488
|
-
QMessageBox.warning(
|
|
489
|
-
self,
|
|
490
|
-
"Connection Limit",
|
|
491
|
-
f"Maximum number of connections ({ConnectionManager.MAX_CONNECTIONS}) reached. "
|
|
492
|
-
"Please close a connection first.",
|
|
493
|
-
)
|
|
494
|
-
return
|
|
495
|
-
|
|
496
|
-
# Create connection
|
|
497
|
-
provider = profile_data["provider"]
|
|
498
|
-
config = profile_data["config"]
|
|
499
|
-
credentials = profile_data.get("credentials", {})
|
|
500
|
-
|
|
501
|
-
try:
|
|
502
|
-
# Create connection object
|
|
503
|
-
if provider == "chromadb":
|
|
504
|
-
connection = self._create_chroma_connection(config, credentials)
|
|
505
|
-
elif provider == "qdrant":
|
|
506
|
-
connection = self._create_qdrant_connection(config, credentials)
|
|
507
|
-
elif provider == "pinecone":
|
|
508
|
-
connection = self._create_pinecone_connection(config, credentials)
|
|
509
|
-
elif provider == "pgvector":
|
|
510
|
-
connection = self._create_pgvector_connection(config, credentials)
|
|
511
|
-
else:
|
|
512
|
-
QMessageBox.warning(self, "Error", f"Unsupported provider: {provider}")
|
|
513
|
-
return
|
|
514
|
-
|
|
515
|
-
# Register with connection manager, using profile_id as connection_id for persistence
|
|
516
|
-
connection_id = self.connection_manager.create_connection(
|
|
517
|
-
name=profile_data["name"],
|
|
518
|
-
provider=provider,
|
|
519
|
-
connection=connection,
|
|
520
|
-
config=config,
|
|
521
|
-
connection_id=profile_data["id"],
|
|
522
|
-
)
|
|
523
|
-
|
|
524
|
-
# Update state to connecting
|
|
525
|
-
self.connection_manager.update_connection_state(
|
|
526
|
-
connection_id, ConnectionState.CONNECTING
|
|
527
|
-
)
|
|
528
|
-
|
|
529
|
-
# Connect in background thread
|
|
530
|
-
thread = ConnectionThread(connection)
|
|
531
|
-
thread.finished.connect(
|
|
532
|
-
lambda success, collections, error: self._on_connection_finished(
|
|
533
|
-
connection_id, success, collections, error
|
|
534
|
-
)
|
|
535
|
-
)
|
|
536
|
-
self._connection_threads[connection_id] = thread
|
|
537
|
-
thread.start()
|
|
538
|
-
|
|
539
|
-
# Show loading dialog
|
|
540
|
-
self.loading_dialog.show_loading(f"Connecting to {profile_data['name']}...")
|
|
541
|
-
|
|
542
|
-
except Exception as e:
|
|
543
|
-
QMessageBox.critical(self, "Connection Error", f"Failed to create connection: {e}")
|
|
544
|
-
|
|
545
|
-
def _create_chroma_connection(self, config: dict, credentials: dict) -> ChromaDBConnection:
|
|
546
|
-
"""Create a ChromaDB connection."""
|
|
547
|
-
conn_type = config.get("type")
|
|
548
|
-
|
|
549
|
-
if conn_type == "persistent":
|
|
550
|
-
return ChromaDBConnection(path=config.get("path"))
|
|
551
|
-
elif conn_type == "http":
|
|
552
|
-
return ChromaDBConnection(host=config.get("host"), port=config.get("port"))
|
|
553
|
-
else: # ephemeral
|
|
554
|
-
return ChromaDBConnection()
|
|
555
|
-
|
|
556
|
-
def _create_qdrant_connection(self, config: dict, credentials: dict) -> QdrantConnection:
|
|
557
|
-
"""Create a Qdrant connection."""
|
|
558
|
-
conn_type = config.get("type")
|
|
559
|
-
api_key = credentials.get("api_key")
|
|
560
|
-
|
|
561
|
-
if conn_type == "persistent":
|
|
562
|
-
return QdrantConnection(path=config.get("path"))
|
|
563
|
-
elif conn_type == "http":
|
|
564
|
-
return QdrantConnection(
|
|
565
|
-
host=config.get("host"), port=config.get("port"), api_key=api_key
|
|
566
|
-
)
|
|
567
|
-
else: # ephemeral
|
|
568
|
-
return QdrantConnection()
|
|
569
|
-
|
|
570
|
-
def _create_pinecone_connection(self, config: dict, credentials: dict) -> PineconeConnection:
|
|
571
|
-
"""Create a Pinecone connection."""
|
|
572
|
-
api_key = credentials.get("api_key")
|
|
573
|
-
if not api_key:
|
|
574
|
-
raise ValueError("Pinecone requires an API key")
|
|
575
|
-
|
|
576
|
-
return PineconeConnection(api_key=api_key)
|
|
577
|
-
|
|
578
|
-
def _create_pgvector_connection(self, config: dict, credentials: dict) -> PgVectorConnection:
|
|
579
|
-
"""Create a PgVector/Postgres connection from profile config/credentials."""
|
|
580
|
-
conn_type = config.get("type")
|
|
581
|
-
|
|
582
|
-
# We expect HTTP-style profile for pgvector (host/port + db creds)
|
|
583
|
-
if conn_type == "http":
|
|
584
|
-
host = config.get("host", "localhost")
|
|
585
|
-
port = int(config.get("port", 5432))
|
|
586
|
-
database = config.get("database")
|
|
587
|
-
user = config.get("user")
|
|
588
|
-
# Prefer password from credentials
|
|
589
|
-
password = credentials.get("password")
|
|
590
|
-
|
|
591
|
-
return PgVectorConnection(
|
|
592
|
-
host=host, port=port, database=database, user=user, password=password
|
|
593
|
-
)
|
|
594
|
-
|
|
595
|
-
raise ValueError("Unsupported connection type for PgVector profile")
|
|
596
|
-
|
|
597
|
-
def _on_connection_finished(
|
|
598
|
-
self, connection_id: str, success: bool, collections: list, error: str
|
|
599
|
-
):
|
|
600
|
-
"""Handle connection thread completion."""
|
|
601
|
-
self.loading_dialog.hide_loading()
|
|
602
|
-
|
|
603
|
-
# Clean up thread
|
|
604
|
-
thread = self._connection_threads.pop(connection_id, None)
|
|
605
|
-
if thread:
|
|
606
|
-
thread.wait() # Wait for thread to fully finish
|
|
607
|
-
thread.deleteLater()
|
|
608
|
-
|
|
522
|
+
"""Connect to a profile using the connection controller."""
|
|
523
|
+
success = self.connection_controller.connect_to_profile(profile_id)
|
|
609
524
|
if success:
|
|
610
|
-
#
|
|
611
|
-
self.
|
|
612
|
-
connection_id, ConnectionState.CONNECTED
|
|
613
|
-
)
|
|
614
|
-
|
|
615
|
-
# Mark connection as opened first (will show in UI)
|
|
616
|
-
self.connection_manager.mark_connection_opened(connection_id)
|
|
617
|
-
|
|
618
|
-
# Then update collections (UI item now exists to receive them)
|
|
619
|
-
self.connection_manager.update_collections(connection_id, collections)
|
|
620
|
-
|
|
621
|
-
# Switch to Active connections tab
|
|
622
|
-
self.left_tabs.setCurrentIndex(0)
|
|
623
|
-
|
|
624
|
-
self.statusBar().showMessage(
|
|
625
|
-
f"Connected successfully ({len(collections)} collections)", 5000
|
|
626
|
-
)
|
|
627
|
-
else:
|
|
628
|
-
# Update state to error
|
|
629
|
-
self.connection_manager.update_connection_state(
|
|
630
|
-
connection_id, ConnectionState.ERROR, error
|
|
631
|
-
)
|
|
632
|
-
|
|
633
|
-
QMessageBox.warning(self, "Connection Failed", f"Failed to connect: {error}")
|
|
634
|
-
|
|
635
|
-
# Remove the failed connection
|
|
636
|
-
self.connection_manager.close_connection(connection_id)
|
|
525
|
+
# Switch to Active connections tab after initiating connection
|
|
526
|
+
self.set_left_panel_active(0)
|
|
637
527
|
|
|
638
528
|
def _refresh_active_connection(self):
|
|
639
529
|
"""Refresh collections for the active connection."""
|
|
@@ -663,15 +553,12 @@ class MainWindow(QMainWindow):
|
|
|
663
553
|
10000,
|
|
664
554
|
)
|
|
665
555
|
|
|
556
|
+
# Apply settings to views after UI is built
|
|
557
|
+
self._apply_settings_to_views()
|
|
558
|
+
|
|
666
559
|
def _show_about(self):
|
|
667
560
|
"""Show about dialog."""
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
QMessageBox.about(
|
|
671
|
-
self,
|
|
672
|
-
"About Vector Inspector",
|
|
673
|
-
get_about_html(),
|
|
674
|
-
)
|
|
561
|
+
DialogService.show_about(self)
|
|
675
562
|
|
|
676
563
|
def _toggle_cache(self, checked: bool):
|
|
677
564
|
"""Toggle caching on/off."""
|
|
@@ -681,53 +568,54 @@ class MainWindow(QMainWindow):
|
|
|
681
568
|
|
|
682
569
|
def _show_migration_dialog(self):
|
|
683
570
|
"""Show cross-database migration dialog."""
|
|
684
|
-
|
|
685
|
-
QMessageBox.information(
|
|
686
|
-
self,
|
|
687
|
-
"Insufficient Connections",
|
|
688
|
-
"You need at least 2 active connections to migrate data.\n"
|
|
689
|
-
"Please connect to additional databases first.",
|
|
690
|
-
)
|
|
691
|
-
return
|
|
692
|
-
|
|
693
|
-
from vector_inspector.ui.dialogs.cross_db_migration import CrossDatabaseMigrationDialog
|
|
694
|
-
|
|
695
|
-
dialog = CrossDatabaseMigrationDialog(self.connection_manager, self)
|
|
696
|
-
dialog.exec()
|
|
571
|
+
DialogService.show_migration_dialog(self.connection_manager, self)
|
|
697
572
|
|
|
698
573
|
def _show_backup_restore_dialog(self):
|
|
699
574
|
"""Show backup/restore dialog for the active collection."""
|
|
700
|
-
#
|
|
575
|
+
# Get active connection and collection
|
|
701
576
|
connection = self.connection_manager.get_active_connection()
|
|
702
|
-
if not connection:
|
|
703
|
-
QMessageBox.information(self, "No Connection", "Please connect to a database first.")
|
|
704
|
-
return
|
|
705
|
-
|
|
706
|
-
# Get active collection
|
|
707
577
|
collection_name = self.connection_manager.get_active_collection()
|
|
708
|
-
if not collection_name:
|
|
709
|
-
# Allow opening dialog without a collection selected (for restore-only)
|
|
710
|
-
QMessageBox.information(
|
|
711
|
-
self,
|
|
712
|
-
"No Collection Selected",
|
|
713
|
-
"You can restore backups without a collection selected.\n"
|
|
714
|
-
"To create a backup, please select a collection first.",
|
|
715
|
-
)
|
|
716
578
|
|
|
717
|
-
|
|
579
|
+
# Show dialog
|
|
580
|
+
result = DialogService.show_backup_restore_dialog(connection, collection_name or "", self)
|
|
718
581
|
|
|
719
|
-
|
|
720
|
-
if dialog.exec() == QDialog.Accepted:
|
|
582
|
+
if result == QDialog.Accepted:
|
|
721
583
|
# Refresh collections after restore
|
|
722
584
|
self._refresh_active_connection()
|
|
723
585
|
|
|
586
|
+
def show_search_results(self, collection_name: str, results: dict, context_info: str = ""):
|
|
587
|
+
"""Display search results in the Search tab.
|
|
588
|
+
|
|
589
|
+
This is an extension point that allows external code (e.g., pro features)
|
|
590
|
+
to programmatically display search results.
|
|
591
|
+
|
|
592
|
+
Args:
|
|
593
|
+
collection_name: Name of the collection
|
|
594
|
+
results: Search results dictionary
|
|
595
|
+
context_info: Optional context string (e.g., "Similar to: item_123")
|
|
596
|
+
"""
|
|
597
|
+
# Switch to search tab
|
|
598
|
+
self.set_main_tab_active(InspectorTabs.SEARCH_TAB)
|
|
599
|
+
|
|
600
|
+
# Set the collection if needed
|
|
601
|
+
if self.search_view.current_collection != collection_name:
|
|
602
|
+
active = self.connection_manager.get_active_connection()
|
|
603
|
+
database_name = active.id if active else ""
|
|
604
|
+
self.search_view.set_collection(collection_name, database_name)
|
|
605
|
+
|
|
606
|
+
# Display the results
|
|
607
|
+
self.search_view.search_results = results
|
|
608
|
+
self.search_view._display_results(results)
|
|
609
|
+
|
|
610
|
+
# Update status with context if provided
|
|
611
|
+
if context_info:
|
|
612
|
+
num_results = len(results.get("ids", [[]])[0])
|
|
613
|
+
self.search_view.results_status.setText(f"{context_info} - Found {num_results} results")
|
|
614
|
+
|
|
724
615
|
def closeEvent(self, event):
|
|
725
616
|
"""Handle application close."""
|
|
726
|
-
#
|
|
727
|
-
|
|
728
|
-
if thread.isRunning():
|
|
729
|
-
thread.quit()
|
|
730
|
-
thread.wait(1000) # Wait up to 1 second
|
|
617
|
+
# Clean up connection controller
|
|
618
|
+
self.connection_controller.cleanup()
|
|
731
619
|
|
|
732
620
|
# Clean up temp HTML files from visualization view
|
|
733
621
|
if self.visualization_view is not None:
|
|
@@ -738,21 +626,23 @@ class MainWindow(QMainWindow):
|
|
|
738
626
|
# Close all connections
|
|
739
627
|
self.connection_manager.close_all_connections()
|
|
740
628
|
|
|
741
|
-
|
|
742
|
-
|
|
629
|
+
# Save window geometry if enabled
|
|
630
|
+
try:
|
|
631
|
+
if self.settings_service.get_window_restore_geometry():
|
|
632
|
+
geom = self.saveGeometry()
|
|
633
|
+
# geom may be a QByteArray; convert to raw bytes
|
|
634
|
+
try:
|
|
635
|
+
if isinstance(geom, QByteArray):
|
|
636
|
+
b = bytes(geom)
|
|
637
|
+
else:
|
|
638
|
+
b = bytes(geom)
|
|
639
|
+
self.settings_service.set_window_geometry(b)
|
|
640
|
+
except Exception:
|
|
641
|
+
try:
|
|
642
|
+
self.settings_service.set_window_geometry(bytes(geom))
|
|
643
|
+
except Exception:
|
|
644
|
+
pass
|
|
645
|
+
except Exception:
|
|
646
|
+
pass
|
|
743
647
|
|
|
744
|
-
|
|
745
|
-
from vector_inspector.utils.version import get_app_version
|
|
746
|
-
|
|
747
|
-
version = get_app_version()
|
|
748
|
-
version_html = (
|
|
749
|
-
f"<h2>Vector Inspector {version}</h2>" if version else "<h2>Vector Inspector</h2>"
|
|
750
|
-
)
|
|
751
|
-
return (
|
|
752
|
-
version_html + "<p>A comprehensive desktop application for visualizing, "
|
|
753
|
-
"querying, and managing multiple vector databases simultaneously.</p>"
|
|
754
|
-
'<p><a href="https://github.com/anthonypdawson/vector-inspector" style="color:#2980b9;">GitHub Project Page</a></p>'
|
|
755
|
-
"<hr />"
|
|
756
|
-
"<p>Built with PySide6</p>"
|
|
757
|
-
"<p><b>New:</b> Pinecone support!</p>"
|
|
758
|
-
)
|
|
648
|
+
event.accept()
|