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