vector-inspector 0.3.6__py3-none-any.whl → 0.3.8__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/chroma_connection.py +4 -1
- vector_inspector/core/connections/pgvector_connection.py +108 -93
- vector_inspector/core/connections/pinecone_connection.py +4 -0
- vector_inspector/core/connections/qdrant_connection.py +13 -0
- vector_inspector/core/provider_factory.py +97 -0
- vector_inspector/services/update_service.py +73 -0
- vector_inspector/ui/components/splash_window.py +14 -2
- vector_inspector/ui/components/update_details_dialog.py +47 -0
- vector_inspector/ui/controllers/__init__.py +1 -0
- vector_inspector/ui/controllers/connection_controller.py +177 -0
- vector_inspector/ui/main_window.py +148 -323
- 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 +72 -0
- vector_inspector/utils/version.py +11 -4
- {vector_inspector-0.3.6.dist-info → vector_inspector-0.3.8.dist-info}/METADATA +27 -5
- {vector_inspector-0.3.6.dist-info → vector_inspector-0.3.8.dist-info}/RECORD +21 -12
- {vector_inspector-0.3.6.dist-info → vector_inspector-0.3.8.dist-info}/WHEEL +0 -0
- {vector_inspector-0.3.6.dist-info → vector_inspector-0.3.8.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
|
|
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
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
class ConnectionThread(QThread):
|
|
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))
|
|
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
|
|
58
24
|
|
|
59
25
|
|
|
60
|
-
class MainWindow(
|
|
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)
|
|
@@ -101,62 +77,36 @@ class MainWindow(QMainWindow):
|
|
|
101
77
|
print(f"[SplashWindow] Failed to show splash: {e}")
|
|
102
78
|
|
|
103
79
|
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
|
|
80
|
+
"""Setup the main UI layout using InspectorShell."""
|
|
81
|
+
# Left panels - Connections and Profiles
|
|
124
82
|
self.connection_panel = ConnectionManagerPanel(self.connection_manager)
|
|
125
|
-
self.
|
|
83
|
+
self.add_left_panel(self.connection_panel, "Active")
|
|
126
84
|
|
|
127
|
-
# Profile manager panel
|
|
128
85
|
self.profile_panel = ProfileManagerPanel(self.profile_service)
|
|
129
|
-
self.
|
|
130
|
-
|
|
131
|
-
left_layout.addWidget(self.left_tabs)
|
|
86
|
+
self.add_left_panel(self.profile_panel, "Profiles")
|
|
132
87
|
|
|
133
|
-
#
|
|
134
|
-
|
|
88
|
+
# Main content tabs using TabRegistry
|
|
89
|
+
tab_defs = InspectorTabs.get_standard_tabs()
|
|
135
90
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
self.search_view = SearchView(None) # Will be set later
|
|
91
|
+
for i, tab_def in enumerate(tab_defs):
|
|
92
|
+
widget = InspectorTabs.create_tab_widget(tab_def, connection=None)
|
|
93
|
+
self.add_main_tab(widget, tab_def.title)
|
|
140
94
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
95
|
+
# Store references to views (except placeholder)
|
|
96
|
+
if i == InspectorTabs.INFO_TAB:
|
|
97
|
+
self.info_panel = widget
|
|
98
|
+
elif i == InspectorTabs.DATA_TAB:
|
|
99
|
+
self.metadata_view = widget
|
|
100
|
+
elif i == InspectorTabs.SEARCH_TAB:
|
|
101
|
+
self.search_view = widget
|
|
102
|
+
# Visualization is lazy-loaded, so it's a placeholder for now
|
|
145
103
|
|
|
146
104
|
# Set Info tab as default
|
|
147
|
-
self.
|
|
105
|
+
self.set_main_tab_active(InspectorTabs.INFO_TAB)
|
|
148
106
|
|
|
149
107
|
# Connect to tab change to lazy load visualization
|
|
150
108
|
self.tab_widget.currentChanged.connect(self._on_tab_changed)
|
|
151
109
|
|
|
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
110
|
def _setup_menu_bar(self):
|
|
161
111
|
"""Setup application menu bar."""
|
|
162
112
|
menubar = self.menuBar()
|
|
@@ -211,10 +161,28 @@ class MainWindow(QMainWindow):
|
|
|
211
161
|
|
|
212
162
|
# Help menu
|
|
213
163
|
help_menu = menubar.addMenu("&Help")
|
|
214
|
-
|
|
215
164
|
about_action = QAction("&About", self)
|
|
216
165
|
about_action.triggered.connect(self._show_about)
|
|
217
166
|
help_menu.addAction(about_action)
|
|
167
|
+
check_update_action = QAction("Check for Update", self)
|
|
168
|
+
check_update_action.triggered.connect(self._check_for_update_from_menu)
|
|
169
|
+
help_menu.addAction(check_update_action)
|
|
170
|
+
|
|
171
|
+
def _check_for_update_from_menu(self):
|
|
172
|
+
from vector_inspector.services.update_service import UpdateService
|
|
173
|
+
from vector_inspector.utils.version import get_app_version
|
|
174
|
+
from PySide6.QtWidgets import QMessageBox
|
|
175
|
+
|
|
176
|
+
latest = UpdateService.get_latest_release(force_refresh=True)
|
|
177
|
+
if latest:
|
|
178
|
+
current_version = get_app_version()
|
|
179
|
+
latest_version = latest.get("tag_name")
|
|
180
|
+
if latest_version and UpdateService.compare_versions(current_version, latest_version):
|
|
181
|
+
# Show update modal
|
|
182
|
+
self._latest_release = latest
|
|
183
|
+
self._on_update_indicator_clicked(None)
|
|
184
|
+
return
|
|
185
|
+
QMessageBox.information(self, "Check for Update", "No update available.")
|
|
218
186
|
|
|
219
187
|
def _setup_toolbar(self):
|
|
220
188
|
"""Setup application toolbar."""
|
|
@@ -233,7 +201,7 @@ class MainWindow(QMainWindow):
|
|
|
233
201
|
toolbar.addAction(refresh_action)
|
|
234
202
|
|
|
235
203
|
def _setup_statusbar(self):
|
|
236
|
-
"""Setup status bar with connection breadcrumb."""
|
|
204
|
+
"""Setup status bar with connection breadcrumb and update indicator."""
|
|
237
205
|
status_bar = QStatusBar()
|
|
238
206
|
self.setStatusBar(status_bar)
|
|
239
207
|
|
|
@@ -241,8 +209,52 @@ class MainWindow(QMainWindow):
|
|
|
241
209
|
self.breadcrumb_label = QLabel("No active connection")
|
|
242
210
|
self.statusBar().addPermanentWidget(self.breadcrumb_label)
|
|
243
211
|
|
|
212
|
+
# Update indicator label (hidden by default)
|
|
213
|
+
self.update_indicator = QLabel()
|
|
214
|
+
self.update_indicator.setText("")
|
|
215
|
+
self.update_indicator.setStyleSheet(
|
|
216
|
+
"color: #2980b9; font-weight: bold; text-decoration: underline;"
|
|
217
|
+
)
|
|
218
|
+
self.update_indicator.setVisible(False)
|
|
219
|
+
self.update_indicator.setCursor(Qt.PointingHandCursor)
|
|
220
|
+
self.statusBar().addPermanentWidget(self.update_indicator)
|
|
221
|
+
|
|
244
222
|
self.statusBar().showMessage("Ready")
|
|
245
223
|
|
|
224
|
+
# Connect click event
|
|
225
|
+
self.update_indicator.mousePressEvent = self._on_update_indicator_clicked
|
|
226
|
+
|
|
227
|
+
# Check for updates on launch
|
|
228
|
+
from vector_inspector.services.update_service import UpdateService
|
|
229
|
+
from vector_inspector.utils.version import get_app_version
|
|
230
|
+
import threading
|
|
231
|
+
|
|
232
|
+
from PySide6.QtCore import QTimer
|
|
233
|
+
|
|
234
|
+
def check_updates():
|
|
235
|
+
latest = UpdateService.get_latest_release()
|
|
236
|
+
if latest:
|
|
237
|
+
current_version = get_app_version()
|
|
238
|
+
latest_version = latest.get("tag_name")
|
|
239
|
+
if latest_version and UpdateService.compare_versions(
|
|
240
|
+
current_version, latest_version
|
|
241
|
+
):
|
|
242
|
+
|
|
243
|
+
def show_update():
|
|
244
|
+
self._latest_release = latest
|
|
245
|
+
self.update_indicator.setText(f"Update available: v{latest_version}")
|
|
246
|
+
self.update_indicator.setVisible(True)
|
|
247
|
+
|
|
248
|
+
QTimer.singleShot(0, show_update)
|
|
249
|
+
|
|
250
|
+
threading.Thread(target=check_updates, daemon=True).start()
|
|
251
|
+
|
|
252
|
+
def _on_update_indicator_clicked(self, event):
|
|
253
|
+
# Show update details dialog
|
|
254
|
+
if not hasattr(self, "_latest_release"):
|
|
255
|
+
return
|
|
256
|
+
DialogService.show_update_details(self._latest_release, self)
|
|
257
|
+
|
|
246
258
|
def _connect_signals(self):
|
|
247
259
|
"""Connect signals between components."""
|
|
248
260
|
# Connection manager signals
|
|
@@ -255,6 +267,9 @@ class MainWindow(QMainWindow):
|
|
|
255
267
|
self.connection_manager.collections_updated.connect(self._on_collections_updated)
|
|
256
268
|
self.connection_manager.connection_opened.connect(self._on_connection_opened)
|
|
257
269
|
|
|
270
|
+
# Connection controller signals
|
|
271
|
+
self.connection_controller.connection_completed.connect(self._on_connection_completed)
|
|
272
|
+
|
|
258
273
|
# Connection panel signals
|
|
259
274
|
self.connection_panel.collection_selected.connect(self._on_collection_selected_from_panel)
|
|
260
275
|
self.connection_panel.add_connection_btn.clicked.connect(self._new_connection_from_profile)
|
|
@@ -262,9 +277,20 @@ class MainWindow(QMainWindow):
|
|
|
262
277
|
# Profile panel signals
|
|
263
278
|
self.profile_panel.connect_profile.connect(self._connect_to_profile)
|
|
264
279
|
|
|
280
|
+
def _on_connection_completed(
|
|
281
|
+
self, connection_id: str, success: bool, collections: list, error: str
|
|
282
|
+
):
|
|
283
|
+
"""Handle connection completed event from controller."""
|
|
284
|
+
if success:
|
|
285
|
+
# Switch to Active connections tab
|
|
286
|
+
self.set_left_panel_active(0)
|
|
287
|
+
self.statusBar().showMessage(
|
|
288
|
+
f"Connected successfully ({len(collections)} collections)", 5000
|
|
289
|
+
)
|
|
290
|
+
|
|
265
291
|
def _on_tab_changed(self, index: int):
|
|
266
292
|
"""Handle tab change - lazy load visualization tab."""
|
|
267
|
-
if index ==
|
|
293
|
+
if index == InspectorTabs.VISUALIZATION_TAB and self.visualization_view is None:
|
|
268
294
|
# Lazy load visualization view
|
|
269
295
|
from vector_inspector.ui.views.visualization_view import VisualizationView
|
|
270
296
|
|
|
@@ -274,9 +300,11 @@ class MainWindow(QMainWindow):
|
|
|
274
300
|
|
|
275
301
|
self.visualization_view = VisualizationView(conn)
|
|
276
302
|
# Replace placeholder with actual view
|
|
277
|
-
self.
|
|
278
|
-
self.
|
|
279
|
-
|
|
303
|
+
self.remove_main_tab(InspectorTabs.VISUALIZATION_TAB)
|
|
304
|
+
self.add_main_tab(
|
|
305
|
+
self.visualization_view, "Visualization", InspectorTabs.VISUALIZATION_TAB
|
|
306
|
+
)
|
|
307
|
+
self.set_main_tab_active(InspectorTabs.VISUALIZATION_TAB)
|
|
280
308
|
|
|
281
309
|
# Set collection if one is already selected
|
|
282
310
|
if active and active.active_collection:
|
|
@@ -314,20 +342,22 @@ class MainWindow(QMainWindow):
|
|
|
314
342
|
if connection_id == self.connection_manager.get_active_connection_id():
|
|
315
343
|
# Show loading immediately when collection changes
|
|
316
344
|
if collection_name:
|
|
317
|
-
self.loading_dialog.show_loading(
|
|
345
|
+
self.connection_controller.loading_dialog.show_loading(
|
|
346
|
+
f"Loading collection '{collection_name}'..."
|
|
347
|
+
)
|
|
318
348
|
QApplication.processEvents()
|
|
319
349
|
try:
|
|
320
350
|
self._update_views_for_collection(collection_name)
|
|
321
351
|
finally:
|
|
322
|
-
self.loading_dialog.hide_loading()
|
|
352
|
+
self.connection_controller.loading_dialog.hide_loading()
|
|
323
353
|
else:
|
|
324
354
|
# Clear collection from views
|
|
325
|
-
self.loading_dialog.show_loading("Clearing collection...")
|
|
355
|
+
self.connection_controller.loading_dialog.show_loading("Clearing collection...")
|
|
326
356
|
QApplication.processEvents()
|
|
327
357
|
try:
|
|
328
358
|
self._update_views_for_collection(None)
|
|
329
359
|
finally:
|
|
330
|
-
self.loading_dialog.hide_loading()
|
|
360
|
+
self.connection_controller.loading_dialog.hide_loading()
|
|
331
361
|
|
|
332
362
|
def _on_collections_updated(self, connection_id: str, collections: list):
|
|
333
363
|
"""Handle collections list updated."""
|
|
@@ -345,7 +375,9 @@ class MainWindow(QMainWindow):
|
|
|
345
375
|
def _on_collection_selected_from_panel(self, connection_id: str, collection_name: str):
|
|
346
376
|
"""Handle collection selection from connection panel."""
|
|
347
377
|
# Show loading dialog while switching collections
|
|
348
|
-
self.loading_dialog.show_loading(
|
|
378
|
+
self.connection_controller.loading_dialog.show_loading(
|
|
379
|
+
f"Loading collection '{collection_name}'..."
|
|
380
|
+
)
|
|
349
381
|
QApplication.processEvents()
|
|
350
382
|
|
|
351
383
|
try:
|
|
@@ -353,7 +385,7 @@ class MainWindow(QMainWindow):
|
|
|
353
385
|
# Just update the views
|
|
354
386
|
self._update_views_for_collection(collection_name)
|
|
355
387
|
finally:
|
|
356
|
-
self.loading_dialog.hide_loading()
|
|
388
|
+
self.connection_controller.loading_dialog.hide_loading()
|
|
357
389
|
|
|
358
390
|
def _update_views_with_connection(self, connection: VectorDBConnection):
|
|
359
391
|
"""Update all views with a new connection."""
|
|
@@ -392,176 +424,20 @@ class MainWindow(QMainWindow):
|
|
|
392
424
|
|
|
393
425
|
def _new_connection_from_profile(self):
|
|
394
426
|
"""Show dialog to create new connection (switches to Profiles tab)."""
|
|
395
|
-
self.
|
|
396
|
-
|
|
397
|
-
self,
|
|
398
|
-
"Connect to Profile",
|
|
399
|
-
"Select a profile from the list and click 'Connect', or click '+' to create a new profile.",
|
|
400
|
-
)
|
|
427
|
+
self.set_left_panel_active(1) # Switch to Profiles tab
|
|
428
|
+
DialogService.show_profile_editor_prompt(self)
|
|
401
429
|
|
|
402
430
|
def _show_profile_editor(self):
|
|
403
431
|
"""Show profile editor to create new profile."""
|
|
404
|
-
self.
|
|
432
|
+
self.set_left_panel_active(1) # Switch to Profiles tab
|
|
405
433
|
self.profile_panel._create_profile()
|
|
406
434
|
|
|
407
435
|
def _connect_to_profile(self, profile_id: str):
|
|
408
|
-
"""Connect to a profile."""
|
|
409
|
-
|
|
410
|
-
if not profile_data:
|
|
411
|
-
QMessageBox.warning(self, "Error", "Profile not found.")
|
|
412
|
-
return
|
|
413
|
-
|
|
414
|
-
# Check connection limit
|
|
415
|
-
if self.connection_manager.get_connection_count() >= ConnectionManager.MAX_CONNECTIONS:
|
|
416
|
-
QMessageBox.warning(
|
|
417
|
-
self,
|
|
418
|
-
"Connection Limit",
|
|
419
|
-
f"Maximum number of connections ({ConnectionManager.MAX_CONNECTIONS}) reached. "
|
|
420
|
-
"Please close a connection first.",
|
|
421
|
-
)
|
|
422
|
-
return
|
|
423
|
-
|
|
424
|
-
# Create connection
|
|
425
|
-
provider = profile_data["provider"]
|
|
426
|
-
config = profile_data["config"]
|
|
427
|
-
credentials = profile_data.get("credentials", {})
|
|
428
|
-
|
|
429
|
-
try:
|
|
430
|
-
# Create connection object
|
|
431
|
-
if provider == "chromadb":
|
|
432
|
-
connection = self._create_chroma_connection(config, credentials)
|
|
433
|
-
elif provider == "qdrant":
|
|
434
|
-
connection = self._create_qdrant_connection(config, credentials)
|
|
435
|
-
elif provider == "pinecone":
|
|
436
|
-
connection = self._create_pinecone_connection(config, credentials)
|
|
437
|
-
elif provider == "pgvector":
|
|
438
|
-
connection = self._create_pgvector_connection(config, credentials)
|
|
439
|
-
else:
|
|
440
|
-
QMessageBox.warning(self, "Error", f"Unsupported provider: {provider}")
|
|
441
|
-
return
|
|
442
|
-
|
|
443
|
-
# Register with connection manager, using profile_id as connection_id for persistence
|
|
444
|
-
connection_id = self.connection_manager.create_connection(
|
|
445
|
-
name=profile_data["name"],
|
|
446
|
-
provider=provider,
|
|
447
|
-
connection=connection,
|
|
448
|
-
config=config,
|
|
449
|
-
connection_id=profile_data["id"],
|
|
450
|
-
)
|
|
451
|
-
|
|
452
|
-
# Update state to connecting
|
|
453
|
-
self.connection_manager.update_connection_state(
|
|
454
|
-
connection_id, ConnectionState.CONNECTING
|
|
455
|
-
)
|
|
456
|
-
|
|
457
|
-
# Connect in background thread
|
|
458
|
-
thread = ConnectionThread(connection)
|
|
459
|
-
thread.finished.connect(
|
|
460
|
-
lambda success, collections, error: self._on_connection_finished(
|
|
461
|
-
connection_id, success, collections, error
|
|
462
|
-
)
|
|
463
|
-
)
|
|
464
|
-
self._connection_threads[connection_id] = thread
|
|
465
|
-
thread.start()
|
|
466
|
-
|
|
467
|
-
# Show loading dialog
|
|
468
|
-
self.loading_dialog.show_loading(f"Connecting to {profile_data['name']}...")
|
|
469
|
-
|
|
470
|
-
except Exception as e:
|
|
471
|
-
QMessageBox.critical(self, "Connection Error", f"Failed to create connection: {e}")
|
|
472
|
-
|
|
473
|
-
def _create_chroma_connection(self, config: dict, credentials: dict) -> ChromaDBConnection:
|
|
474
|
-
"""Create a ChromaDB connection."""
|
|
475
|
-
conn_type = config.get("type")
|
|
476
|
-
|
|
477
|
-
if conn_type == "persistent":
|
|
478
|
-
return ChromaDBConnection(path=config.get("path"))
|
|
479
|
-
elif conn_type == "http":
|
|
480
|
-
return ChromaDBConnection(host=config.get("host"), port=config.get("port"))
|
|
481
|
-
else: # ephemeral
|
|
482
|
-
return ChromaDBConnection()
|
|
483
|
-
|
|
484
|
-
def _create_qdrant_connection(self, config: dict, credentials: dict) -> QdrantConnection:
|
|
485
|
-
"""Create a Qdrant connection."""
|
|
486
|
-
conn_type = config.get("type")
|
|
487
|
-
api_key = credentials.get("api_key")
|
|
488
|
-
|
|
489
|
-
if conn_type == "persistent":
|
|
490
|
-
return QdrantConnection(path=config.get("path"))
|
|
491
|
-
elif conn_type == "http":
|
|
492
|
-
return QdrantConnection(
|
|
493
|
-
host=config.get("host"), port=config.get("port"), api_key=api_key
|
|
494
|
-
)
|
|
495
|
-
else: # ephemeral
|
|
496
|
-
return QdrantConnection()
|
|
497
|
-
|
|
498
|
-
def _create_pinecone_connection(self, config: dict, credentials: dict) -> PineconeConnection:
|
|
499
|
-
"""Create a Pinecone connection."""
|
|
500
|
-
api_key = credentials.get("api_key")
|
|
501
|
-
if not api_key:
|
|
502
|
-
raise ValueError("Pinecone requires an API key")
|
|
503
|
-
|
|
504
|
-
return PineconeConnection(api_key=api_key)
|
|
505
|
-
|
|
506
|
-
def _create_pgvector_connection(self, config: dict, credentials: dict) -> PgVectorConnection:
|
|
507
|
-
"""Create a PgVector/Postgres connection from profile config/credentials."""
|
|
508
|
-
conn_type = config.get("type")
|
|
509
|
-
|
|
510
|
-
# We expect HTTP-style profile for pgvector (host/port + db creds)
|
|
511
|
-
if conn_type == "http":
|
|
512
|
-
host = config.get("host", "localhost")
|
|
513
|
-
port = int(config.get("port", 5432))
|
|
514
|
-
database = config.get("database")
|
|
515
|
-
user = config.get("user")
|
|
516
|
-
# Prefer password from credentials
|
|
517
|
-
password = credentials.get("password")
|
|
518
|
-
|
|
519
|
-
return PgVectorConnection(
|
|
520
|
-
host=host, port=port, database=database, user=user, password=password
|
|
521
|
-
)
|
|
522
|
-
|
|
523
|
-
raise ValueError("Unsupported connection type for PgVector profile")
|
|
524
|
-
|
|
525
|
-
def _on_connection_finished(
|
|
526
|
-
self, connection_id: str, success: bool, collections: list, error: str
|
|
527
|
-
):
|
|
528
|
-
"""Handle connection thread completion."""
|
|
529
|
-
self.loading_dialog.hide_loading()
|
|
530
|
-
|
|
531
|
-
# Clean up thread
|
|
532
|
-
thread = self._connection_threads.pop(connection_id, None)
|
|
533
|
-
if thread:
|
|
534
|
-
thread.wait() # Wait for thread to fully finish
|
|
535
|
-
thread.deleteLater()
|
|
536
|
-
|
|
436
|
+
"""Connect to a profile using the connection controller."""
|
|
437
|
+
success = self.connection_controller.connect_to_profile(profile_id)
|
|
537
438
|
if success:
|
|
538
|
-
#
|
|
539
|
-
self.
|
|
540
|
-
connection_id, ConnectionState.CONNECTED
|
|
541
|
-
)
|
|
542
|
-
|
|
543
|
-
# Mark connection as opened first (will show in UI)
|
|
544
|
-
self.connection_manager.mark_connection_opened(connection_id)
|
|
545
|
-
|
|
546
|
-
# Then update collections (UI item now exists to receive them)
|
|
547
|
-
self.connection_manager.update_collections(connection_id, collections)
|
|
548
|
-
|
|
549
|
-
# Switch to Active connections tab
|
|
550
|
-
self.left_tabs.setCurrentIndex(0)
|
|
551
|
-
|
|
552
|
-
self.statusBar().showMessage(
|
|
553
|
-
f"Connected successfully ({len(collections)} collections)", 5000
|
|
554
|
-
)
|
|
555
|
-
else:
|
|
556
|
-
# Update state to error
|
|
557
|
-
self.connection_manager.update_connection_state(
|
|
558
|
-
connection_id, ConnectionState.ERROR, error
|
|
559
|
-
)
|
|
560
|
-
|
|
561
|
-
QMessageBox.warning(self, "Connection Failed", f"Failed to connect: {error}")
|
|
562
|
-
|
|
563
|
-
# Remove the failed connection
|
|
564
|
-
self.connection_manager.close_connection(connection_id)
|
|
439
|
+
# Switch to Active connections tab after initiating connection
|
|
440
|
+
self.set_left_panel_active(0)
|
|
565
441
|
|
|
566
442
|
def _refresh_active_connection(self):
|
|
567
443
|
"""Refresh collections for the active connection."""
|
|
@@ -593,13 +469,7 @@ class MainWindow(QMainWindow):
|
|
|
593
469
|
|
|
594
470
|
def _show_about(self):
|
|
595
471
|
"""Show about dialog."""
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
QMessageBox.about(
|
|
599
|
-
self,
|
|
600
|
-
"About Vector Inspector",
|
|
601
|
-
get_about_html(),
|
|
602
|
-
)
|
|
472
|
+
DialogService.show_about(self)
|
|
603
473
|
|
|
604
474
|
def _toggle_cache(self, checked: bool):
|
|
605
475
|
"""Toggle caching on/off."""
|
|
@@ -609,53 +479,25 @@ class MainWindow(QMainWindow):
|
|
|
609
479
|
|
|
610
480
|
def _show_migration_dialog(self):
|
|
611
481
|
"""Show cross-database migration dialog."""
|
|
612
|
-
|
|
613
|
-
QMessageBox.information(
|
|
614
|
-
self,
|
|
615
|
-
"Insufficient Connections",
|
|
616
|
-
"You need at least 2 active connections to migrate data.\n"
|
|
617
|
-
"Please connect to additional databases first.",
|
|
618
|
-
)
|
|
619
|
-
return
|
|
620
|
-
|
|
621
|
-
from vector_inspector.ui.dialogs.cross_db_migration import CrossDatabaseMigrationDialog
|
|
622
|
-
|
|
623
|
-
dialog = CrossDatabaseMigrationDialog(self.connection_manager, self)
|
|
624
|
-
dialog.exec()
|
|
482
|
+
DialogService.show_migration_dialog(self.connection_manager, self)
|
|
625
483
|
|
|
626
484
|
def _show_backup_restore_dialog(self):
|
|
627
485
|
"""Show backup/restore dialog for the active collection."""
|
|
628
|
-
#
|
|
486
|
+
# Get active connection and collection
|
|
629
487
|
connection = self.connection_manager.get_active_connection()
|
|
630
|
-
if not connection:
|
|
631
|
-
QMessageBox.information(self, "No Connection", "Please connect to a database first.")
|
|
632
|
-
return
|
|
633
|
-
|
|
634
|
-
# Get active collection
|
|
635
488
|
collection_name = self.connection_manager.get_active_collection()
|
|
636
|
-
if not collection_name:
|
|
637
|
-
# Allow opening dialog without a collection selected (for restore-only)
|
|
638
|
-
QMessageBox.information(
|
|
639
|
-
self,
|
|
640
|
-
"No Collection Selected",
|
|
641
|
-
"You can restore backups without a collection selected.\n"
|
|
642
|
-
"To create a backup, please select a collection first.",
|
|
643
|
-
)
|
|
644
489
|
|
|
645
|
-
|
|
490
|
+
# Show dialog
|
|
491
|
+
result = DialogService.show_backup_restore_dialog(connection, collection_name or "", self)
|
|
646
492
|
|
|
647
|
-
|
|
648
|
-
if dialog.exec() == QDialog.Accepted:
|
|
493
|
+
if result == QDialog.Accepted:
|
|
649
494
|
# Refresh collections after restore
|
|
650
495
|
self._refresh_active_connection()
|
|
651
496
|
|
|
652
497
|
def closeEvent(self, event):
|
|
653
498
|
"""Handle application close."""
|
|
654
|
-
#
|
|
655
|
-
|
|
656
|
-
if thread.isRunning():
|
|
657
|
-
thread.quit()
|
|
658
|
-
thread.wait(1000) # Wait up to 1 second
|
|
499
|
+
# Clean up connection controller
|
|
500
|
+
self.connection_controller.cleanup()
|
|
659
501
|
|
|
660
502
|
# Clean up temp HTML files from visualization view
|
|
661
503
|
if self.visualization_view is not None:
|
|
@@ -667,20 +509,3 @@ class MainWindow(QMainWindow):
|
|
|
667
509
|
self.connection_manager.close_all_connections()
|
|
668
510
|
|
|
669
511
|
event.accept()
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
def get_about_html():
|
|
673
|
-
from vector_inspector.utils.version import get_app_version
|
|
674
|
-
|
|
675
|
-
version = get_app_version()
|
|
676
|
-
version_html = (
|
|
677
|
-
f"<h2>Vector Inspector {version}</h2>" if version else "<h2>Vector Inspector</h2>"
|
|
678
|
-
)
|
|
679
|
-
return (
|
|
680
|
-
version_html + "<p>A comprehensive desktop application for visualizing, "
|
|
681
|
-
"querying, and managing multiple vector databases simultaneously.</p>"
|
|
682
|
-
'<p><a href="https://github.com/anthonypdawson/vector-inspector" style="color:#2980b9;">GitHub Project Page</a></p>'
|
|
683
|
-
"<hr />"
|
|
684
|
-
"<p>Built with PySide6</p>"
|
|
685
|
-
"<p><b>New:</b> Pinecone support!</p>"
|
|
686
|
-
)
|