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