vector-inspector 0.2.6__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 +19 -0
- 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 +425 -190
- vector_inspector/ui/views/info_panel.py +225 -55
- vector_inspector/ui/views/metadata_view.py +71 -3
- vector_inspector/ui/views/search_view.py +43 -3
- {vector_inspector-0.2.6.dist-info → vector_inspector-0.2.7.dist-info}/METADATA +3 -1
- vector_inspector-0.2.7.dist-info/RECORD +45 -0
- vector_inspector-0.2.6.dist-info/RECORD +0 -35
- {vector_inspector-0.2.6.dist-info → vector_inspector-0.2.7.dist-info}/WHEEL +0 -0
- {vector_inspector-0.2.6.dist-info → vector_inspector-0.2.7.dist-info}/entry_points.txt +0 -0
|
@@ -1,43 +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, QTimer
|
|
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.backup_restore_dialog import BackupRestoreDialog
|
|
19
22
|
from vector_inspector.ui.components.loading_dialog import LoadingDialog
|
|
20
23
|
|
|
21
24
|
|
|
22
|
-
class
|
|
23
|
-
"""
|
|
25
|
+
class ConnectionThread(QThread):
|
|
26
|
+
"""Background thread for connecting to database."""
|
|
24
27
|
|
|
25
|
-
|
|
28
|
+
finished = Signal(bool, list, str) # success, collections, error_message
|
|
29
|
+
|
|
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."""
|
|
26
49
|
|
|
27
50
|
def __init__(self):
|
|
28
51
|
super().__init__()
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
self.
|
|
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
|
|
32
62
|
|
|
33
63
|
self.setWindowTitle("Vector Inspector")
|
|
34
|
-
self.setGeometry(100, 100,
|
|
64
|
+
self.setGeometry(100, 100, 1600, 900)
|
|
35
65
|
|
|
36
66
|
self._setup_ui()
|
|
37
67
|
self._setup_menu_bar()
|
|
38
68
|
self._setup_toolbar()
|
|
39
69
|
self._setup_statusbar()
|
|
40
70
|
self._connect_signals()
|
|
71
|
+
self._restore_session()
|
|
41
72
|
|
|
42
73
|
def _setup_ui(self):
|
|
43
74
|
"""Setup the main UI layout."""
|
|
@@ -51,24 +82,31 @@ class MainWindow(QMainWindow):
|
|
|
51
82
|
# Main splitter (left panel | right tabs)
|
|
52
83
|
main_splitter = QSplitter(Qt.Horizontal)
|
|
53
84
|
|
|
54
|
-
# Left panel -
|
|
85
|
+
# Left panel - Connections and Profiles
|
|
55
86
|
left_panel = QWidget()
|
|
56
87
|
left_layout = QVBoxLayout(left_panel)
|
|
57
88
|
left_layout.setContentsMargins(0, 0, 0, 0)
|
|
58
89
|
|
|
59
|
-
|
|
60
|
-
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")
|
|
96
|
+
|
|
97
|
+
# Profile manager panel
|
|
98
|
+
self.profile_panel = ProfileManagerPanel(self.profile_service)
|
|
99
|
+
self.left_tabs.addTab(self.profile_panel, "Profiles")
|
|
61
100
|
|
|
62
|
-
left_layout.addWidget(self.
|
|
63
|
-
left_layout.addWidget(self.collection_browser)
|
|
101
|
+
left_layout.addWidget(self.left_tabs)
|
|
64
102
|
|
|
65
103
|
# Right panel - Tabbed views
|
|
66
104
|
self.tab_widget = QTabWidget()
|
|
67
105
|
|
|
68
|
-
|
|
69
|
-
self.
|
|
70
|
-
self.
|
|
71
|
-
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
|
|
72
110
|
|
|
73
111
|
self.tab_widget.addTab(self.info_panel, "Info")
|
|
74
112
|
self.tab_widget.addTab(self.metadata_view, "Data Browser")
|
|
@@ -85,7 +123,7 @@ class MainWindow(QMainWindow):
|
|
|
85
123
|
main_splitter.addWidget(left_panel)
|
|
86
124
|
main_splitter.addWidget(self.tab_widget)
|
|
87
125
|
main_splitter.setStretchFactor(0, 1)
|
|
88
|
-
main_splitter.setStretchFactor(1,
|
|
126
|
+
main_splitter.setStretchFactor(1, 4)
|
|
89
127
|
|
|
90
128
|
layout.addWidget(main_splitter)
|
|
91
129
|
|
|
@@ -96,14 +134,10 @@ class MainWindow(QMainWindow):
|
|
|
96
134
|
# File menu
|
|
97
135
|
file_menu = menubar.addMenu("&File")
|
|
98
136
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
file_menu.addAction(
|
|
103
|
-
|
|
104
|
-
disconnect_action = QAction("&Disconnect", self)
|
|
105
|
-
disconnect_action.triggered.connect(self._on_disconnect)
|
|
106
|
-
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)
|
|
107
141
|
|
|
108
142
|
file_menu.addSeparator()
|
|
109
143
|
|
|
@@ -112,25 +146,38 @@ class MainWindow(QMainWindow):
|
|
|
112
146
|
exit_action.triggered.connect(self.close)
|
|
113
147
|
file_menu.addAction(exit_action)
|
|
114
148
|
|
|
115
|
-
#
|
|
116
|
-
|
|
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)
|
|
117
155
|
|
|
118
|
-
|
|
119
|
-
new_collection_action.setShortcut("Ctrl+N")
|
|
120
|
-
new_collection_action.triggered.connect(self._on_new_collection)
|
|
121
|
-
collection_menu.addAction(new_collection_action)
|
|
156
|
+
connection_menu.addSeparator()
|
|
122
157
|
|
|
123
158
|
refresh_action = QAction("&Refresh Collections", self)
|
|
124
159
|
refresh_action.setShortcut("F5")
|
|
125
|
-
refresh_action.triggered.connect(self.
|
|
126
|
-
|
|
160
|
+
refresh_action.triggered.connect(self._refresh_active_connection)
|
|
161
|
+
connection_menu.addAction(refresh_action)
|
|
127
162
|
|
|
128
|
-
|
|
163
|
+
connection_menu.addSeparator()
|
|
129
164
|
|
|
130
165
|
backup_action = QAction("&Backup/Restore...", self)
|
|
131
|
-
backup_action.
|
|
132
|
-
|
|
133
|
-
|
|
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)
|
|
134
181
|
|
|
135
182
|
# Help menu
|
|
136
183
|
help_menu = menubar.addMenu("&Help")
|
|
@@ -145,200 +192,388 @@ class MainWindow(QMainWindow):
|
|
|
145
192
|
toolbar.setMovable(False)
|
|
146
193
|
self.addToolBar(toolbar)
|
|
147
194
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
toolbar.addAction(
|
|
151
|
-
|
|
152
|
-
disconnect_action = QAction("Disconnect", self)
|
|
153
|
-
disconnect_action.triggered.connect(self._on_disconnect)
|
|
154
|
-
toolbar.addAction(disconnect_action)
|
|
155
|
-
toolbar.addSeparator()
|
|
156
|
-
|
|
157
|
-
backup_action = QAction("Backup/Restore", self)
|
|
158
|
-
backup_action.triggered.connect(self._on_backup_restore)
|
|
159
|
-
toolbar.addAction(backup_action)
|
|
160
|
-
|
|
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)
|
|
161
198
|
|
|
162
199
|
toolbar.addSeparator()
|
|
163
200
|
|
|
164
201
|
refresh_action = QAction("Refresh", self)
|
|
165
|
-
refresh_action.triggered.connect(self.
|
|
202
|
+
refresh_action.triggered.connect(self._refresh_active_connection)
|
|
166
203
|
toolbar.addAction(refresh_action)
|
|
167
204
|
|
|
168
205
|
def _setup_statusbar(self):
|
|
169
|
-
"""Setup status bar."""
|
|
170
|
-
|
|
171
|
-
self.setStatusBar(
|
|
172
|
-
|
|
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")
|
|
173
215
|
|
|
174
216
|
def _connect_signals(self):
|
|
175
217
|
"""Connect signals between components."""
|
|
176
|
-
|
|
177
|
-
self.
|
|
178
|
-
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)
|
|
179
229
|
|
|
180
230
|
def _on_tab_changed(self, index: int):
|
|
181
231
|
"""Handle tab change - lazy load visualization tab."""
|
|
182
232
|
if index == 3 and self.visualization_view is None:
|
|
183
233
|
# Lazy load visualization view
|
|
184
234
|
from vector_inspector.ui.views.visualization_view import VisualizationView
|
|
185
|
-
|
|
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)
|
|
186
241
|
# Replace placeholder with actual view
|
|
187
242
|
self.tab_widget.removeTab(3)
|
|
188
243
|
self.tab_widget.insertTab(3, self.visualization_view, "Visualization")
|
|
189
244
|
self.tab_widget.setCurrentIndex(3)
|
|
245
|
+
|
|
190
246
|
# Set collection if one is already selected
|
|
191
|
-
if
|
|
192
|
-
self.visualization_view.set_collection(
|
|
247
|
+
if active and active.active_collection:
|
|
248
|
+
self.visualization_view.set_collection(active.active_collection)
|
|
193
249
|
|
|
194
|
-
def
|
|
195
|
-
"""Handle
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
self.connection_view.show_connection_dialog()
|
|
212
|
-
|
|
213
|
-
def _on_disconnect(self):
|
|
214
|
-
"""Handle disconnect action."""
|
|
215
|
-
if self.connection.is_connected:
|
|
216
|
-
self.connection.disconnect()
|
|
217
|
-
self.statusBar.showMessage("Disconnected")
|
|
218
|
-
self.connection_changed.emit(False)
|
|
219
|
-
self.collection_browser.clear()
|
|
220
|
-
# Clear info panel
|
|
221
|
-
self.info_panel.refresh_database_info()
|
|
222
|
-
else:
|
|
223
|
-
# Always clear collection browser on disconnect
|
|
224
|
-
self.collection_browser.clear()
|
|
225
|
-
|
|
226
|
-
def _on_connection_status_changed(self, connected: bool):
|
|
227
|
-
"""Handle connection status change."""
|
|
228
|
-
if connected:
|
|
229
|
-
self.statusBar.showMessage("Connected")
|
|
230
|
-
self.connection_changed.emit(True)
|
|
231
|
-
self._on_refresh_collections()
|
|
232
|
-
# Refresh info panel with new connection
|
|
233
|
-
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)
|
|
234
267
|
else:
|
|
235
|
-
self.
|
|
236
|
-
self.
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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())
|
|
240
277
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
|
247
294
|
self.loading_dialog.show_loading(f"Loading collection '{collection_name}'...")
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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()
|
|
251
303
|
|
|
252
|
-
def
|
|
253
|
-
"""Update all views with
|
|
254
|
-
#
|
|
255
|
-
self.info_panel.
|
|
256
|
-
self.metadata_view.
|
|
257
|
-
self.search_view.
|
|
258
|
-
# Only update visualization if it's been created
|
|
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
|
|
259
310
|
if self.visualization_view is not None:
|
|
260
|
-
self.visualization_view.
|
|
311
|
+
self.visualization_view.current_collection = None
|
|
261
312
|
|
|
262
|
-
#
|
|
263
|
-
self.
|
|
313
|
+
# Update connection references
|
|
314
|
+
self.info_panel.connection = connection
|
|
315
|
+
self.metadata_view.connection = connection
|
|
316
|
+
self.search_view.connection = connection
|
|
264
317
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
318
|
+
if self.visualization_view is not None:
|
|
319
|
+
self.visualization_view.connection = connection
|
|
320
|
+
|
|
321
|
+
# Refresh info panel (will show no collection selected)
|
|
322
|
+
if connection:
|
|
270
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 ""
|
|
271
331
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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)
|
|
335
|
+
|
|
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.")
|
|
276
358
|
return
|
|
277
359
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
|
284
369
|
|
|
285
|
-
|
|
286
|
-
|
|
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
|
|
287
384
|
|
|
288
|
-
#
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
+
)
|
|
292
392
|
|
|
293
|
-
#
|
|
294
|
-
|
|
295
|
-
# Ask for vector size (required for Qdrant)
|
|
296
|
-
vector_size, ok = QInputDialog.getInt(
|
|
297
|
-
self,
|
|
298
|
-
"Vector Size",
|
|
299
|
-
"Enter vector dimension size:",
|
|
300
|
-
value=384, # Default for sentence transformers
|
|
301
|
-
min=1,
|
|
302
|
-
max=10000
|
|
303
|
-
)
|
|
304
|
-
if ok:
|
|
305
|
-
success = self.connection.create_collection(name, vector_size)
|
|
393
|
+
# Update state to connecting
|
|
394
|
+
self.connection_manager.update_connection_state(connection_id, ConnectionState.CONNECTING)
|
|
306
395
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
else:
|
|
313
|
-
QMessageBox.warning(
|
|
314
|
-
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
|
|
315
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}")
|
|
316
411
|
|
|
317
|
-
def
|
|
318
|
-
"""
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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)
|
|
322
455
|
|
|
323
|
-
|
|
324
|
-
self.
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
|
329
481
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
+
|
|
333
503
|
def _show_about(self):
|
|
334
504
|
"""Show about dialog."""
|
|
335
505
|
QMessageBox.about(
|
|
336
506
|
self,
|
|
337
507
|
"About Vector Inspector",
|
|
338
|
-
"<h2>Vector Inspector 0.
|
|
508
|
+
"<h2>Vector Inspector 0.3.0</h2>"
|
|
339
509
|
"<p>A comprehensive desktop application for visualizing, "
|
|
340
|
-
"querying, and managing vector
|
|
510
|
+
"querying, and managing multiple vector databases simultaneously.</p>"
|
|
341
511
|
'<p><a href="https://github.com/anthonypdawson/vector-inspector" style="color:#2980b9;">GitHub Project Page</a></p>'
|
|
342
512
|
"<hr />"
|
|
343
|
-
"<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>"
|
|
344
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
|
+
|