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