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.
@@ -0,0 +1,518 @@
1
+ """Profile management UI for saved connection profiles."""
2
+
3
+ from typing import Optional
4
+ from PySide6.QtWidgets import (
5
+ QWidget, QVBoxLayout, QHBoxLayout, QListWidget, QListWidgetItem,
6
+ QPushButton, QMenu, QMessageBox, QLabel, QDialog, QFormLayout,
7
+ QLineEdit, QComboBox, QRadioButton, QButtonGroup, QGroupBox,
8
+ QFileDialog, QCheckBox, QProgressDialog
9
+ )
10
+ from PySide6.QtCore import Qt, Signal
11
+
12
+ from vector_inspector.services.profile_service import ProfileService, ConnectionProfile
13
+
14
+
15
+ class ProfileManagerPanel(QWidget):
16
+ """Panel for managing saved connection profiles.
17
+
18
+ Signals:
19
+ connect_profile: Emitted when user wants to connect to a profile (profile_id)
20
+ """
21
+
22
+ connect_profile = Signal(str) # profile_id
23
+
24
+ def __init__(self, profile_service: ProfileService, parent=None):
25
+ """
26
+ Initialize profile manager panel.
27
+
28
+ Args:
29
+ profile_service: The ProfileService instance
30
+ parent: Parent widget
31
+ """
32
+ super().__init__(parent)
33
+ self.profile_service = profile_service
34
+
35
+ self._setup_ui()
36
+ self._connect_signals()
37
+ self._refresh_profiles()
38
+
39
+ def _setup_ui(self):
40
+ """Setup the UI."""
41
+ layout = QVBoxLayout(self)
42
+ layout.setContentsMargins(0, 0, 0, 0)
43
+
44
+ # Header
45
+ header_layout = QHBoxLayout()
46
+ header_label = QLabel("Saved Profiles")
47
+ header_label.setStyleSheet("font-weight: bold; font-size: 12px;")
48
+ header_layout.addWidget(header_label)
49
+ header_layout.addStretch()
50
+
51
+ # New profile button
52
+ self.new_profile_btn = QPushButton("+")
53
+ self.new_profile_btn.setMaximumWidth(30)
54
+ self.new_profile_btn.setToolTip("Create new profile")
55
+ self.new_profile_btn.clicked.connect(self._create_profile)
56
+ header_layout.addWidget(self.new_profile_btn)
57
+
58
+ layout.addLayout(header_layout)
59
+
60
+ # Profile list
61
+ self.profile_list = QListWidget()
62
+ self.profile_list.setContextMenuPolicy(Qt.CustomContextMenu)
63
+ self.profile_list.customContextMenuRequested.connect(self._show_context_menu)
64
+ self.profile_list.itemDoubleClicked.connect(self._on_profile_double_clicked)
65
+ layout.addWidget(self.profile_list)
66
+
67
+ # Action buttons
68
+ button_layout = QHBoxLayout()
69
+
70
+ self.connect_btn = QPushButton("Connect")
71
+ self.connect_btn.clicked.connect(self._connect_selected_profile)
72
+ button_layout.addWidget(self.connect_btn)
73
+
74
+ self.edit_btn = QPushButton("Edit")
75
+ self.edit_btn.clicked.connect(self._edit_selected_profile)
76
+ button_layout.addWidget(self.edit_btn)
77
+
78
+ self.delete_btn = QPushButton("Delete")
79
+ self.delete_btn.clicked.connect(self._delete_selected_profile)
80
+ button_layout.addWidget(self.delete_btn)
81
+
82
+ layout.addLayout(button_layout)
83
+
84
+ def _connect_signals(self):
85
+ """Connect to profile service signals."""
86
+ self.profile_service.profile_added.connect(self._refresh_profiles)
87
+ self.profile_service.profile_updated.connect(self._refresh_profiles)
88
+ self.profile_service.profile_deleted.connect(self._refresh_profiles)
89
+
90
+ def _refresh_profiles(self):
91
+ """Refresh the profile list."""
92
+ self.profile_list.clear()
93
+
94
+ profiles = self.profile_service.get_all_profiles()
95
+ for profile in profiles:
96
+ item = QListWidgetItem(f"{profile.name} ({profile.provider})")
97
+ item.setData(Qt.UserRole, profile.id)
98
+ self.profile_list.addItem(item)
99
+
100
+ def _on_profile_double_clicked(self, item: QListWidgetItem):
101
+ """Handle profile double-click to connect."""
102
+ profile_id = item.data(Qt.UserRole)
103
+ if profile_id:
104
+ self.connect_profile.emit(profile_id)
105
+
106
+ def _connect_selected_profile(self):
107
+ """Connect to the selected profile."""
108
+ current_item = self.profile_list.currentItem()
109
+ if not current_item:
110
+ QMessageBox.warning(self, "No Selection", "Please select a profile to connect.")
111
+ return
112
+
113
+ profile_id = current_item.data(Qt.UserRole)
114
+ self.connect_profile.emit(profile_id)
115
+
116
+ def _edit_selected_profile(self):
117
+ """Edit the selected profile."""
118
+ current_item = self.profile_list.currentItem()
119
+ if not current_item:
120
+ QMessageBox.warning(self, "No Selection", "Please select a profile to edit.")
121
+ return
122
+
123
+ profile_id = current_item.data(Qt.UserRole)
124
+ profile = self.profile_service.get_profile(profile_id)
125
+ if not profile:
126
+ return
127
+
128
+ dialog = ProfileEditorDialog(self.profile_service, profile, parent=self)
129
+ dialog.exec()
130
+
131
+ def _delete_selected_profile(self):
132
+ """Delete the selected profile."""
133
+ current_item = self.profile_list.currentItem()
134
+ if not current_item:
135
+ QMessageBox.warning(self, "No Selection", "Please select a profile to delete.")
136
+ return
137
+
138
+ profile_id = current_item.data(Qt.UserRole)
139
+ profile = self.profile_service.get_profile(profile_id)
140
+ if not profile:
141
+ return
142
+
143
+ reply = QMessageBox.question(
144
+ self,
145
+ "Delete Profile",
146
+ f"Delete profile '{profile.name}'?\n\nThis will also delete any saved credentials.",
147
+ QMessageBox.Yes | QMessageBox.No
148
+ )
149
+
150
+ if reply == QMessageBox.Yes:
151
+ self.profile_service.delete_profile(profile_id)
152
+
153
+ def _create_profile(self):
154
+ """Create a new profile."""
155
+ dialog = ProfileEditorDialog(self.profile_service, parent=self)
156
+ dialog.exec()
157
+
158
+ def _show_context_menu(self, pos):
159
+ """Show context menu for profile."""
160
+ item = self.profile_list.itemAt(pos)
161
+ if not item:
162
+ return
163
+
164
+ profile_id = item.data(Qt.UserRole)
165
+ profile = self.profile_service.get_profile(profile_id)
166
+ if not profile:
167
+ return
168
+
169
+ menu = QMenu(self)
170
+
171
+ connect_action = menu.addAction("Connect")
172
+ connect_action.triggered.connect(lambda: self.connect_profile.emit(profile_id))
173
+
174
+ menu.addSeparator()
175
+
176
+ edit_action = menu.addAction("Edit")
177
+ edit_action.triggered.connect(lambda: self._edit_profile(profile_id))
178
+
179
+ duplicate_action = menu.addAction("Duplicate")
180
+ duplicate_action.triggered.connect(lambda: self._duplicate_profile(profile_id))
181
+
182
+ menu.addSeparator()
183
+
184
+ delete_action = menu.addAction("Delete")
185
+ delete_action.triggered.connect(lambda: self._delete_profile(profile_id))
186
+
187
+ menu.exec_(self.profile_list.mapToGlobal(pos))
188
+
189
+ def _edit_profile(self, profile_id: str):
190
+ """Edit a profile."""
191
+ profile = self.profile_service.get_profile(profile_id)
192
+ if profile:
193
+ dialog = ProfileEditorDialog(self.profile_service, profile, parent=self)
194
+ dialog.exec()
195
+
196
+ def _duplicate_profile(self, profile_id: str):
197
+ """Duplicate a profile."""
198
+ profile = self.profile_service.get_profile(profile_id)
199
+ if not profile:
200
+ return
201
+
202
+ from PySide6.QtWidgets import QInputDialog
203
+ new_name, ok = QInputDialog.getText(
204
+ self,
205
+ "Duplicate Profile",
206
+ "Enter name for duplicated profile:",
207
+ text=f"{profile.name} (Copy)"
208
+ )
209
+
210
+ if ok and new_name:
211
+ self.profile_service.duplicate_profile(profile_id, new_name)
212
+
213
+ def _delete_profile(self, profile_id: str):
214
+ """Delete a profile."""
215
+ profile = self.profile_service.get_profile(profile_id)
216
+ if not profile:
217
+ return
218
+
219
+ reply = QMessageBox.question(
220
+ self,
221
+ "Delete Profile",
222
+ f"Delete profile '{profile.name}'?",
223
+ QMessageBox.Yes | QMessageBox.No
224
+ )
225
+
226
+ if reply == QMessageBox.Yes:
227
+ self.profile_service.delete_profile(profile_id)
228
+
229
+
230
+ class ProfileEditorDialog(QDialog):
231
+ """Dialog for creating/editing connection profiles."""
232
+
233
+ def __init__(self, profile_service: ProfileService, profile: Optional[ConnectionProfile] = None, parent=None):
234
+ """
235
+ Initialize profile editor dialog.
236
+
237
+ Args:
238
+ profile_service: The ProfileService instance
239
+ profile: Existing profile to edit (None for new profile)
240
+ parent: Parent widget
241
+ """
242
+ super().__init__(parent)
243
+ self.profile_service = profile_service
244
+ self.profile = profile
245
+ self.is_edit_mode = profile is not None
246
+
247
+ self.setWindowTitle("Edit Profile" if self.is_edit_mode else "New Profile")
248
+ self.setMinimumWidth(500)
249
+
250
+ self._setup_ui()
251
+
252
+ if self.is_edit_mode:
253
+ self._load_profile_data()
254
+
255
+ def _setup_ui(self):
256
+ """Setup the UI."""
257
+ layout = QVBoxLayout(self)
258
+
259
+ form_layout = QFormLayout()
260
+
261
+ # Profile name
262
+ self.name_input = QLineEdit()
263
+ form_layout.addRow("Profile Name:", self.name_input)
264
+
265
+ # Provider
266
+ self.provider_combo = QComboBox()
267
+ self.provider_combo.addItem("ChromaDB", "chromadb")
268
+ self.provider_combo.addItem("Qdrant", "qdrant")
269
+ self.provider_combo.currentIndexChanged.connect(self._on_provider_changed)
270
+ form_layout.addRow("Provider:", self.provider_combo)
271
+
272
+ layout.addLayout(form_layout)
273
+
274
+ # Connection type group
275
+ type_group = QGroupBox("Connection Type")
276
+ type_layout = QVBoxLayout()
277
+
278
+ self.button_group = QButtonGroup()
279
+
280
+ self.persistent_radio = QRadioButton("Persistent (Local File)")
281
+ self.persistent_radio.setChecked(True)
282
+ self.persistent_radio.toggled.connect(self._on_type_changed)
283
+
284
+ self.http_radio = QRadioButton("HTTP (Remote Server)")
285
+
286
+ self.ephemeral_radio = QRadioButton("Ephemeral (In-Memory)")
287
+
288
+ self.button_group.addButton(self.persistent_radio)
289
+ self.button_group.addButton(self.http_radio)
290
+ self.button_group.addButton(self.ephemeral_radio)
291
+
292
+ type_layout.addWidget(self.persistent_radio)
293
+ type_layout.addWidget(self.http_radio)
294
+ type_layout.addWidget(self.ephemeral_radio)
295
+ type_group.setLayout(type_layout)
296
+
297
+ layout.addWidget(type_group)
298
+
299
+ # Connection details
300
+ details_group = QGroupBox("Connection Details")
301
+ details_layout = QFormLayout()
302
+
303
+ # Persistent path
304
+ self.path_layout = QHBoxLayout()
305
+ self.path_input = QLineEdit()
306
+ self.path_browse_btn = QPushButton("Browse...")
307
+ self.path_browse_btn.clicked.connect(self._browse_for_path)
308
+ self.path_layout.addWidget(self.path_input)
309
+ self.path_layout.addWidget(self.path_browse_btn)
310
+ details_layout.addRow("Path:", self.path_layout)
311
+
312
+ # HTTP settings
313
+ self.host_input = QLineEdit("localhost")
314
+ details_layout.addRow("Host:", self.host_input)
315
+
316
+ self.port_input = QLineEdit("8000")
317
+ details_layout.addRow("Port:", self.port_input)
318
+
319
+ self.api_key_input = QLineEdit()
320
+ self.api_key_input.setEchoMode(QLineEdit.Password)
321
+ details_layout.addRow("API Key:", self.api_key_input)
322
+
323
+ details_group.setLayout(details_layout)
324
+ layout.addWidget(details_group)
325
+
326
+ # Test connection button
327
+ self.test_btn = QPushButton("Test Connection")
328
+ self.test_btn.clicked.connect(self._test_connection)
329
+ layout.addWidget(self.test_btn)
330
+
331
+ # Buttons
332
+ button_layout = QHBoxLayout()
333
+
334
+ self.save_btn = QPushButton("Save")
335
+ self.save_btn.clicked.connect(self._save_profile)
336
+ button_layout.addWidget(self.save_btn)
337
+
338
+ self.cancel_btn = QPushButton("Cancel")
339
+ self.cancel_btn.clicked.connect(self.reject)
340
+ button_layout.addWidget(self.cancel_btn)
341
+
342
+ layout.addLayout(button_layout)
343
+
344
+ # Initial state
345
+ self._on_type_changed()
346
+
347
+ def _on_provider_changed(self):
348
+ """Handle provider change."""
349
+ provider = self.provider_combo.currentData()
350
+
351
+ # Update default port
352
+ if provider == "qdrant":
353
+ if self.port_input.text() == "8000":
354
+ self.port_input.setText("6333")
355
+ elif provider == "chromadb":
356
+ if self.port_input.text() == "6333":
357
+ self.port_input.setText("8000")
358
+
359
+ # Show/hide API key field
360
+ is_http = self.http_radio.isChecked()
361
+ self.api_key_input.setEnabled(is_http and provider == "qdrant")
362
+
363
+ def _on_type_changed(self):
364
+ """Handle connection type change."""
365
+ is_persistent = self.persistent_radio.isChecked()
366
+ is_http = self.http_radio.isChecked()
367
+
368
+ # Show/hide relevant fields
369
+ self.path_input.setEnabled(is_persistent)
370
+ self.path_browse_btn.setEnabled(is_persistent)
371
+ self.host_input.setEnabled(is_http)
372
+ self.port_input.setEnabled(is_http)
373
+
374
+ provider = self.provider_combo.currentData()
375
+ self.api_key_input.setEnabled(is_http and provider == "qdrant")
376
+
377
+ def _browse_for_path(self):
378
+ """Browse for persistent storage path."""
379
+ path = QFileDialog.getExistingDirectory(
380
+ self,
381
+ "Select Database Directory",
382
+ self.path_input.text()
383
+ )
384
+ if path:
385
+ self.path_input.setText(path)
386
+
387
+ def _load_profile_data(self):
388
+ """Load existing profile data into form."""
389
+ if not self.profile:
390
+ return
391
+
392
+ # Get profile with credentials
393
+ profile_data = self.profile_service.get_profile_with_credentials(self.profile.id)
394
+ if not profile_data:
395
+ return
396
+
397
+ self.name_input.setText(profile_data["name"])
398
+
399
+ # Set provider
400
+ index = self.provider_combo.findData(profile_data["provider"])
401
+ if index >= 0:
402
+ self.provider_combo.setCurrentIndex(index)
403
+
404
+ config = profile_data.get("config", {})
405
+ conn_type = config.get("type", "persistent")
406
+
407
+ # Set connection type
408
+ if conn_type == "persistent":
409
+ self.persistent_radio.setChecked(True)
410
+ self.path_input.setText(config.get("path", ""))
411
+ elif conn_type == "http":
412
+ self.http_radio.setChecked(True)
413
+ self.host_input.setText(config.get("host", "localhost"))
414
+ self.port_input.setText(str(config.get("port", "8000")))
415
+ else:
416
+ self.ephemeral_radio.setChecked(True)
417
+
418
+ # Load credentials
419
+ credentials = profile_data.get("credentials", {})
420
+ if "api_key" in credentials:
421
+ self.api_key_input.setText(credentials["api_key"])
422
+
423
+ def _test_connection(self):
424
+ """Test the connection with current settings."""
425
+ # Get config
426
+ config = self._get_config()
427
+ provider = self.provider_combo.currentData()
428
+
429
+ # Create connection
430
+ from vector_inspector.core.connections.chroma_connection import ChromaDBConnection
431
+ from vector_inspector.core.connections.qdrant_connection import QdrantConnection
432
+
433
+ try:
434
+ if provider == "chromadb":
435
+ conn = ChromaDBConnection(**self._get_connection_kwargs(config))
436
+ else:
437
+ conn = QdrantConnection(**self._get_connection_kwargs(config))
438
+
439
+ # Test connection
440
+ progress = QProgressDialog("Testing connection...", None, 0, 0, self)
441
+ progress.setWindowModality(Qt.WindowModal)
442
+ progress.show()
443
+
444
+ success = conn.connect()
445
+ progress.close()
446
+
447
+ if success:
448
+ QMessageBox.information(self, "Success", "Connection test successful!")
449
+ conn.disconnect()
450
+ else:
451
+ QMessageBox.warning(self, "Failed", "Connection test failed.")
452
+ except Exception as e:
453
+ QMessageBox.critical(self, "Error", f"Connection test error: {e}")
454
+
455
+ def _get_config(self) -> dict:
456
+ """Get configuration from form."""
457
+ config = {}
458
+
459
+ if self.persistent_radio.isChecked():
460
+ config["type"] = "persistent"
461
+ config["path"] = self.path_input.text()
462
+ elif self.http_radio.isChecked():
463
+ config["type"] = "http"
464
+ config["host"] = self.host_input.text()
465
+ config["port"] = int(self.port_input.text())
466
+ else:
467
+ config["type"] = "ephemeral"
468
+
469
+ return config
470
+
471
+ def _get_connection_kwargs(self, config: dict) -> dict:
472
+ """Get kwargs for creating connection."""
473
+ kwargs = {}
474
+
475
+ if config["type"] == "persistent":
476
+ kwargs["path"] = config["path"]
477
+ elif config["type"] == "http":
478
+ kwargs["host"] = config["host"]
479
+ kwargs["port"] = config["port"]
480
+ if self.api_key_input.text():
481
+ kwargs["api_key"] = self.api_key_input.text()
482
+
483
+ return kwargs
484
+
485
+ def _save_profile(self):
486
+ """Save the profile."""
487
+ name = self.name_input.text().strip()
488
+ if not name:
489
+ QMessageBox.warning(self, "Invalid Input", "Please enter a profile name.")
490
+ return
491
+
492
+ provider = self.provider_combo.currentData()
493
+ config = self._get_config()
494
+
495
+ # Get credentials
496
+ credentials = {}
497
+ if self.api_key_input.text() and self.http_radio.isChecked():
498
+ credentials["api_key"] = self.api_key_input.text()
499
+
500
+ if self.is_edit_mode:
501
+ # Update existing profile
502
+ self.profile_service.update_profile(
503
+ self.profile.id,
504
+ name=name,
505
+ config=config,
506
+ credentials=credentials if credentials else None
507
+ )
508
+ else:
509
+ # Create new profile
510
+ self.profile_service.create_profile(
511
+ name=name,
512
+ provider=provider,
513
+ config=config,
514
+ credentials=credentials if credentials else None
515
+ )
516
+
517
+ self.accept()
518
+
@@ -0,0 +1,5 @@
1
+ """UI Dialogs for vector-inspector."""
2
+
3
+ from .embedding_config_dialog import EmbeddingConfigDialog
4
+
5
+ __all__ = ['EmbeddingConfigDialog']